inout 파라미터 기본 동작
Swift에서는 기본적으로 함수 인자는 value로 전달한다.
함수 내에서 이뤄진 변경 사항은 호출부에서 확인할 수 없고, 변경을 원한다면 inout 파라미터를 적용해야 한다.
inout 파라미터를 포함하는 함수를 호출할 때, inout 인자는 &를 사용하여 전달한다.
이를 통해 함수 내부에서 변경된 값이 함수 외부에도 반영이 된다.
func someFunction(a: inout Int) {
a += 1
}
var x = 7
someFunction(&x)
print(x) // Prints "8"
inout 파라미터는 다음과 같이 전달된다.
- 함수가 호출될 때, 인자의 값이 복사된다.
- 함수의 본문에서, 복사된 값이 수정된다.
- 함수가 반환될 때, 복사된 값이 원래 인자에 할당된다.
이 동작을 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
- 예상대로 modify 함수 호출과 함께 computedPropertyNumber의 Getter가 호출된다.
- 함수가 호출될 때, 인자의 값이 복사된다.
- 함수가 반환될 때 computedPropertyNumber의 Setter가 호출된다.
- 함수가 반환될 때, 복사된 값이 원래 인자에 할당된다.
- 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 |