클로저에서 공부해야할 개념들이 여러가지가 있습니다만,
그 중에서 좀 까다로울 수 있는 "Capture Values" 부분을 작성하겠습니다.
직역하자면, "값을 잡아둔다" 정도로 해석이 되겠죠.
네, 맞습니다. 값을 잡아둡니다.
A closure can capture constants and variables from the surrounding context in which it’s defined. The closure can then refer to and modify the values of those constants and variables from within its body, even if the original scope that defined the constants and variables no longer exists. - 공식문서 -
해석)
클로저는 상수나 변수를 캡쳐할 수 있습니다. "surrounding context"로 부터요.
그 클로저는 그리고 값을 참조하거나 변경할 수 있습니다. 해당 코드 블럭 내에서요. 심지어 "original scope엔 존재하지 않다고 하더라도요.
정리해보자면, 다음처럼 해석할 수 있겠네요.
특정 context에 해당하는 범위에 값을 캡쳐해둘 수 있고, 값을 참조하거나 변경이 가능하다!
좀 더 설명울 붙여서 정의해볼게요.
(클로저에서) capture values란, "로직 수행을 위해 context를 참조한다."
== Capturing by reference
공식문서의 예제는 다음과 같이 코드를 보여줍니다.
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
// 변수를 선언한다.
var runnungTotal = 0
// 함수를 선언한다.
func increment() -> Int {
runnungTotal += amount
return runnungTotal
}
// 함수를 리턴한다.
return increment
}
makeIncrementer
메소드를 보면 runningTotal 이라는 변수가 있죠.
그런데 해당 메소드 내부에 보면 increment
라는 메소드가 또 있고, 내부에는 없는 프로퍼티인 runningTotal 값에 접근하고 있습니다.
위에 나오던 영단어 surrounding
이 왜 있는지 느낌이 오시나요?
(해당 메소드를 둘러싸고 있는 context를 캡쳐한다)
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen() // 10
incrementByTen() // 20
incrementByTen() // 30
이 코드를 보시면 조금 생소하실 수 도 있습니다.
makeIncrementer
메소드는 한 번 호출되면 내부에 있는 메소드의 변수인 runningTotal은 메모리에서 해제되어야 합니다.
그런데 계속 값이 추가되고 있죠?
이 의미는 값이 해제되지 않았다는 뜻입니다.
왜? why?
→ capturing Values...
값을 잡아두고 있기 떄문이죠. 그러면 왜 잡아둘까요?
→ 참조하고 있는 대상이 있기 때문입니다. 참조가 없으면 해제되겠죠.
클로저는 참조타입이므로 값을 공유하게 됩니다.
// 클로저는 참조타입임을 보여주는 예시
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen() // 40
incrementByTen() // 50
그래서 이전에 추가된 값 30에서 한 번더 호출하게되면, 그 값에 10을 더해 40이 됩니다.
여기서 질문
그러면, value type의 경우는 값이 복사된다고 하는데, 어떻게 값을 참조하지?
이에 대한 대답은 훌륭하신 "kimdo2297" 블로그 링크에서 답변하주시고 있습니다.
https://velog.io/@kimdo2297/클로져-캡쳐에-대해서-about-closure-capture
해당 블로그에서 다음과 같이 답을 주고 있습니다.
그러므로,
value Type 도 동일하게 캡쳐될 것이고, 동일하게 동작하게 될 것이다.
로 결론 지을 수 있을 것 같네요.
이후 글은
"https://alisoftware.github.io/swift/closures/2016/07/25/closure-capture-1/"
을 참조해서 작성했습니다.
값이 어떻게 캡쳐되고 저장되는 지에 대한 예제 코드입니다.
먼저 class를 정의합니다.
class Person {
let name: String
init(name: String) {
self.name = name
}
deinit { print("\(self.name) 메모리에서 해제 ") }
}
시간 차이를 두어 값이 어떻게 변화하는지 살피기 위해 DisPatch 메소드를 활용합니다.
func delay(_ seconds: Int, closure: @escaping () -> ()) {
let time = DispatchTime.now() + .seconds(seconds)
DispatchQueue.main.asyncAfter(deadline: time) {
print("타이머 끝")
closure()
}
}
본격적으로 테스트를 해보겠습니다.
class를 상수에 할당하고, 1 초뒤에 값을 확인합니다.
func capturingValueTest() {
let person = Person(name: "Uno")
print("최초 closure \(person.name)")
delay(1) {
print("내부 closure \(person.name)")
}
print("종료")
}
// <Console Result>
// 최초 closure Uno
// 종료
// 타이머 끝
// 내부 closure Uno
// Uno 메모리에서 해제
콘솔을 보면
→ 이 예제를 통해서 reference Count가 남아있다면, 메모리에서 해제되지 않음을 알 수 있습니다.
영어를 직역하니 어떤 의미인지 판단하기 어려워 개인 의견을 조금 더했습니다.
코드를 보겠습니다.
func capturingValueTest02() {
var person = Person(name: "Moya")
print("최초 closure \(person.name)")
delay(1) {
print("내부 closure \(person.name)")
}
person = Person(name: "Swift")
print("변경이후 closure \(person.name)")
}
// <Console Result>
// 최초 closure Moya
// Moya 메모리에서 해제
// 변경이후 closure Swift
// 타이머 끝
// 내부 closure Swift
// Swift 메모리에서 해제
코드를 보면, 이전과 다른 점이 있습니다.
최초에는 "Moya" 를 멤버변수에 할당했고,
함수의 마지막 부분에서는 "Swift"를 할당했습니다.
그리고 1초가 지난 시점에서 어떤 값이 호출되는지 출력하고 있습니다.
이 때, 타이머가 종료된 이후에 호출된 closure의 값이 "Swift" 죠.
타이머가 종료된 시점에서 아직 메모리에 class 멤버변수의 값을 참조하고 있었고, 제일 마지막에 변경해준 값을 출력했습니다.
→ 참조를 하는 시점에서의 가장 마지막 값을 출력하게됨을 알 수 있습니다!
그러면 의문이 생깁니다.
그러면, delay가 된 이후의 시점이 아니라 delay가 시작된 시점에서의 값으로 출력할 수는 없나?
먼저 CaptureList에 대해 짧게 설명드리면 다음과 같습니다.
코드를 보겠습니다.
func capturingValueTest03() {
var person = Person(name: "Uno")
print("최초 closure \(person.name)")
delay(1) { [person] in
print("내부 closure \(person.name)")
}
person = Person(name: "Moya")
print("변경이후 closure \(person.name)")
}
capturingValueTest03()
// <Console Result>
// 최초 closure Uno
// 변경이후 closure Moya
// Moya 메모리에서 해제
// 타이머 끝
// 내부 closure Uno
// Uno 메모리에서 해제
바로 이전 코드와 달라진 점은,
delay 클로저 코드블럭의 값이 클로저를 호출할 때의 값인 "Uno" 라는 점입니다.