우선 공식 문서에서는 클로저 부분에서 값 캡처를 언급한다.
클로저는 정의된 주변 context에서 상수/변수를 캡처할 수 있다고 말한다.
이렇게 캡처가 되면 상수/변수를 정의한 scope가 존재하지 않아도 그 값을 참조하고 수정할 수 있다.
🤔 오호 ... 어렵다..뭔 말인지...
아주 쉬운 예시를 들어보자.
func some() {
var aaa = "A"
var bbb = "B"
let closure = {
print(bbb)
}
}
이런 함수가 있다고 가정하면
클로저는 외부 변수 bbb
를 내부에서 사용한다.
이때, 클로저는 외부의 값을 내부적으로 저장하는데 !!! -> 아주 간단하게 이것을 값 캡처라고 부른다.
(aaa
는 클로저 내부에서 사용되지 않았으니 당연히 캡처되지 않는다!!!)
이번에는 좀더 이해를 돕기 위해 공식문서에서 제공하는 예시를 살펴보자.
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
자! 이제 결과를 확인해보자.
let incrementByTen = makeIncrementer(forIncrement: 10)
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementByTen() // 10
incrementByTen() // 20
incrementByTen() // 30
incrementBySeven() // 7
incrementBySeven() // 14
incrementBySeven() // 21
runningTotal과 amount가 캡쳐되서 그 변수를 공유하기 때문!!!
아니 그런데... 앞선 예제를 살펴보면 let incrementByTen
분명 상수(let)으로 할당했는데 값이 증가하는(변하는) 것을 볼 수 있다.
어떻게 이게 가능할까?🤔 답은 함수와 클로저는 참조 타입이기 때문에 가능하다!
함수와 클로저를 상수나 변수에 할당할 때 실제로는 상수와 변수에 해당 함수나 클로저의 reference가 할당된다. (reference를 할당할 뿐 진짜 값을 할당한게 아니다.)
만약에 한 클로저를 두 상/변수에 할당하면 두 상/변수를 같은 클로저를 참조하고있다.(같은 referance를 갖게된다.)
"클로저는 참조 타입"이라는 말이 크게 와닿지 않는다. 메모리 영역에선 어떻게 움직이는지 전혀 감이 잡히지 않는다. 😭
이해를 최대한 해보고자
이번에는 위 예시들이 메모리에선 어떻게 움직이는 지 도식화해보자.
1️⃣ makeIncrementer가 호출되면, incrementByTen에는 heap의 주소값이 stack에 저장된다.
2️⃣ 그리고 heap에는 값을 저장할 공간이 할당된다.
let incrementByTen = makeIncrementer(forIncrement: 10)
3️⃣ 클로저를 호출하는 과정이다. 이때, 비로소 선언만 되었던 클로저가 검증단계에 들어선다. makeIncrementer 내부의 runningTotal 초기값이 클로저 내부로 들어가면서 heap영역에 할당된다. (runningTotal = 0)
incrementByTen()
4️⃣ 클로저 내부의 연산을 통해 runningTotal이 증가하고 heap영역의 runningTotal 값이 바뀐다.
runningTotal += amount
return runningTotal
5️⃣ incrementBySeven 상수를 선언하면 다른 클로저가 할당되고, 이전 주소값과 다른 주소값이 stack에 저장된다.
6️⃣ 이전 과정과 같지만 다른 주소에 값을 저장한다.
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()