본문 바로가기

iOS/Swift

Swift - In-Out 파라미터

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/declarations#In-Out-Parameters

Documentation

docs.swift.org

 

inout 파라미터 기본 동작

Swift에서는 기본적으로 함수 인자는 value로 전달한다.  
함수 내에서 이뤄진 변경 사항은 호출부에서 확인할 수 없고, 변경을 원한다면 inout 파라미터를 적용해야 한다.
 
inout 파라미터를 포함하는 함수를 호출할 때, inout 인자는 &를 사용하여 전달한다.
이를 통해 함수 내부에서 변경된 값이 함수 외부에도 반영이 된다.

func someFunction(a: inout Int) {
    a += 1
}

var x = 7
someFunction(&x)
print(x)  // Prints "8"

 
inout 파라미터는 다음과 같이 전달된다.

  1. 함수가 호출될 때, 인자의 값이 복사된다.
  2. 함수의 본문에서, 복사된 값이 수정된다.
  3. 함수가 반환될 때, 복사된 값이 원래 인자에 할당된다.

 
이 동작을 copy-in-copy-out 또는 call by value라고 한다. 
 

Computed Property,  Property Observer로 동작 확인하기

계산 프로퍼티(computed property)나 옵저버가 있는 프로퍼티를 inout파라미터로 전달할 때

함수 호출 시 그 프로퍼티의 getter가 호출되고, 함수 반환 시 setter가 호출된다고 한다.

직접 동작을 확인해 보자.
 
[계산 프로퍼티를 inout 파라미터로 전달]

var computedPropertyNumber: Int {
    get {
        print("✅", #function, "Getter called")
        return 10
    }
    set {
        print("⬅️", #function, "Setter called with value \(newValue)")
    }
}

func modify(_ value: inout Int) {
    print("---", #function, "was Called")
    value += 5
    print("---", #function, "will return")
}

print("------ modify(_:) will be called")
modify(&computedPropertyNumber)
print("------ modify(_:) was returned")

// ------ modify(_:) will be called
// ✅ computedPropertyNumber Getter called (1)
// --- modify(_:) was Called
// --- modify(_:) will return
// ⬅️ computedPropertyNumber Setter called with value 15 (2)
// ✅ computedPropertyNumber Getter called (3)
// ------ modify(_:) was returned

 

  1. 예상대로 modify 함수 호출과 함께 computedPropertyNumber의 Getter가 호출된다.
    • 함수가 호출될 때, 인자의 값이 복사된다.
  2. 함수가 반환될 때 computedPropertyNumber의 Setter가 호출된다.
    • 함수가 반환될 때, 복사된 값이 원래 인자에 할당된다.
  3. Getter는 왜 한번 더 불리는가 ??
    • 일단 잘 모르겠다. 나중에 알게 되면 수정.
    • 추측이긴 하지만, inout 파라미터는 메모리 안전성을 확보하기 위해서 exclusive access 규칙을 갖는다.  
    • 수정된 값이 올바르게 설정되었는지 추가적인 확인 절차가 아닐까.

 
[옵저버가 있는 프로퍼티를 inout 파라미터로 전달]

var numberWithPropertyObservers: Int = 10 {
    willSet {
        print("⬅️", #function, "willSet called with value \(newValue)")
    }
    
    didSet {
        print("⬅️", #function, "didSet called wit value \(oldValue)")
    }
}

func modify(_ value: inout Int) {
    print("---", #function, "was Called")
    value += 5
    print("---", #function, "will return")
}

print("------ modify(_:) will be called")
modify(&numberWithPropertyObservers)
print("------ modify(_:) was returned")

// ------ modify(_:) will be called
// --- modify(_:) was Called
// --- modify(_:) will return
// ⬅️ numberWithPropertyObservers willSet called with value 15 
// ⬅️ numberWithPropertyObservers didSet called wit value 10
//------ modify(_:) was returned

 
마찬가지로 함수가 반환될 때 Setter가 잘 호출된다.
 

Swift Optimization

Swift에서 inout 파라미터는 기본적으로 copy-in copy-out 모델을 따른다.
위에서 이야기한 것처럼 값이 함수로 전달될 때 복사가 이뤄진다.
 
그러나 Swift 컴파일러는 성능 최적화를 위해 실제 메모리 주소를 사용하는 call by referecne 방식을 적용할 수도 있다.
 
이 최적화는 메모리 복사 오버헤드를 줄이기 위해 사용되지만,
그럼에도 코드를 작성할 때는 copy-in copy-out 모델에서 올바르게 동작하는 것을 보장해야 한다.
 
[정말 call by reference로 동작하는지 확인해 보자]
위에서 작성한 computedPropertyNumber와 numberWithPropertyObservers를 MyNumber 구조체 안에 넣었다.

struct MyNumber {
    var computedPropertyNumber: Int {
        get {
            print("✅", #function, "Getter called")
            return 10
        }
        set {
            print("⬅️", #function, "Setter called with value \(newValue)")
        }
    }
    
    var numberWithPropertyObservers: Int = 10 {
        willSet {
            print("⬅️", #function, "willSet called with value \(newValue)")
        }
        
        didSet {
            print("⬅️", #function, "didSet called wit value \(oldValue)")
        }
    }
}



func modify(_ myNumber: inout MyNumber) {
    print("---", #function, "was Called")
    myNumber.computedPropertyNumber = 5
    myNumber.numberWithPropertyObservers = 5
    print("---", #function, "will return")
}

var myNumber = MyNumber()

print("------ modify(_:) will be called")
modify(&myNumber)
print("------ modify(_:) was returned")

// ------ modify(_:) will be called
// --- modify(_:) was Called
// ⬅️ computedPropertyNumber Setter called with value 5
// ⬅️ numberWithPropertyObservers willSet called with value 5
// ⬅️ numberWithPropertyObservers didSet called wit value 10
// --- modify(_:) will return
// ------ modify(_:) was returned

 
이전 예시에서는 명확하게 copy-in copy-out이 이뤄졌었다.
 
구조체에 넣고 돌려본 결과에서는 call by reference로 동작함을 유추해 볼 수 있다.

  • Getter가 전혀 호출되지 않고 Setter만 호출된다.
    • copy-in 동작 없음
  • 반환과 함께 값이 설정되는 것이 아니라 inout 파라미터를 수정한 즉시 setter들이 호출된다.
    • 함수가 반환될 때가 아닌 원본 값에 바로 반영

Memory Safety

memory exclusivity : 특정 메모리 위치에 동시에 두 개 이상의 읽기 또는 쓰기 접근이 발생하지 않도록 보장하는 규칙이다.
 
자세한건 참고
 
1. 함수 내에서 intout 파라미터로 전달된 값을 처리하는 동안, 해당 값의 원본 변수에 동시에 접근해서는 안된다.

var someValue: Int = 1

func someFunction(a: inout Int) {
    a += someValue
}


// Error: This causes a runtime exclusivity violation
someFunction(a: &someValue)

 
[결과] 런타임 에러 발생

 
2. 동일한 값을 복수 개의 inout 파라미터에 전달해서는 안된다.

var someValue: Int = 1

func someFunction(a: inout Int, b: inout Int) {
    a += b
    b += 1
}


// Error: Cannot pass the same value to multiple in-out parameters
someFunction(a: &someValue, b: &someValue)

 
[결과] 컴파일 에러 발생

 
 
3. inout 파라미터를 캡처하는 클로저나 중첩 함수는 non-escpaing이어야 한다.
 
inout 파라미터를 수정하지 않고 캡처해야 한다면, 캡처 리스트를 사용하여 해당 파라미터를 변경 불가능하게 만들어야 한다.

func someFunction(a: inout Int) -> () -> Int {
    return { [a] in return a + 1 } // a를 변경하지 않음, 캡처 리스트 사용
}

 
 
4. inout 매개변수를 캡처하고 수정해야 한다면, 명시적인 로컬 복사본을 만들면 된다.
 
예를 들어 함수가 반환되기 전에 모든 수정이 완료되었음을 보장하는 멀티스레드 코드에서 로컬 복사본을 사용할 수 있다.

import Foundation

func multithreadedFunction(queue: DispatchQueue, x: inout Int) {
    var localX = x // 로컬 복사본
    defer { x = localX }  // 함수 종료 시 로컬 복사본을 원래 변수에 할당

    queue.async {
        someMutatingOperation(&localX) // // 비동기 작업에서 localX를 수정
    }
    
    queue.sync {} // 작업이 완료될 때까지 기다림
}

func someMutatingOperation(_ x: inout Int) {
    x += 1
}

var x = 0
multithreadedFunction(queue: DispatchQueue.global(), x: &x)

print(x) // 1

'iOS > Swift' 카테고리의 다른 글

Swift 공식문서 7. Closures  (3) 2024.09.26
Swift 공식문서 6. Functions  (0) 2024.09.20
Swift 공식문서 5. Control Flow  (0) 2024.09.19
Swift - Strideable Protocol  (0) 2024.09.19
Swift 공식문서 4. Collection Types  (0) 2024.09.08