Swift - Inout 파라미터

Swift의 inout 파라미터에 대해 알아봅시다.

In-Out Parameters

Swift에서 함수의 파라미터는 상수(Constant)이므로 함수 내부에서 파라미터의 값을 변경할 수 없다. 이는 우리가 파라미터를 실수로라도 변경할 수 없다는 뜻이기도 하다. 만약 함수에서 파라미터의 값을 변경하고, 변경된 값이 함수 호출이 종료된 후에도 지속되길 원한다면 inout 파라미터를 사용하면 된다.

in-out 파라미터는 함수 정의시 파라미터의 타입 전에 inout 키워드를 추가하면 된다. in-out 파라미터는 변수(variable)만을 취급하며 함수의 인자로 전달할 때 &를 사용하여 해당 값이 함수내부에서 변경될 것임을 나타내야 한다.

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
	let temp = a
	a = b
	b = temp
}

var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
// someInt = 107, anotherInt = 3

inout의 원리

inout 매개변수는 다음의 과정을 거친다.

  1. 함수가 호출되면, 매개변수로 넘겨진 변수가 복사된다.
  2. 함수 몸체에서, 복사한 값을 수정한다.
  3. 함수가 반환될 때, 변화된 값을 원본 변수에 재할당한다.

이 동작을 copy-in copy-out 혹은 call by value result 라고 부르며 inout은 실제로 copy-in copy-out의 줄임말 이다.

in-out 최적화

SWIFT는 in-out 파라미터에 대한 최적화를 위해 값을 복사하는 것이 아니라 값이 저장된 메모리 주소값을 함수 내,외부에서 사용한다. 이 최적화를 call by reference 라고 부른다. 이를 통해 복사로 인한 오버헤드를 줄일 수 있다. inout을 사용할 때 개발자가 따로 최적화를 고려할 필요는 없다.

메모리 접근

inout을 사용할 때는 메모리 접근에 주의해야 한다. 아래와 같은 상황에서 stepSizenumber는 같은 메모리 주소를 참조하기 때문에 읽기, 쓰기가 동시에 이루어져 충돌이 발생할 수 있기 때문이다.

var stepSize = 1

func increment(_ number: inout Int) {
    number += stepSize
}

increment(&stepSize)
// Error: conflicting accesses to stepSize

inout 파라미터를 캡쳐할 수 있는 클로저, 중첩 함수는 반드시 nonescaping이여야 한다. inout 파라미터를 캡쳐하길 원한다면, 반드시 캡쳐리스트를 사용해서 해당 파라미터를 불변값으로 명시해야한다.

func someFunction(a: inout Int) -> () -> Int {
    return { [a] in return a + 1 }
}

만약 값을 캡쳐하고, 변경시키길 원한다면 local copy를 사용하여 copy-in copy-out을 직접 구현해야한다. 멀티 스레드 코드에서 모든 변경이 함수 반환 전에 끝나야함을 보장하는 경우를 예로 들 수 있다.

func multithreadedFunction(queue: DispatchQueue, x: inout Int) {
    // Make a local copy and manually copy it back.
    var localX = x
    defer { x = localX }

    // Operate on localX asynchronously, then wait before returning.
    queue.async { someMutatingOperation(&localX) }
    queue.sync {}
}

Reference

https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID545