오늘은 저번에 다뤘던 클로저에 대해서 더 알아보기로 하자.
클로저
는 정의된 둘러싸인 컨텍스트에서 상수와 변수를 캡쳐 할 수 있다. 즉, 클로저는 상수와 변수를 정의한 원래 범위가 더이상 존재하지 않더라도 본문 내에서 해당 상수와 변수의 값을 참조하고 수정할 수 있다.
캡쳐리스트
라고 하며, 메모리 누수를 방지하기위해서 weak, unwoned
를 캡쳐 리스트로 사용하여 순환 참조를 방지하는 것이 중요하다.func upCount() -> () -> Int {
var count = 0
let total = {
count += 1 // 클로저 밖에 있는 count 변수를 캡처(저장)하여 사용
return count
}
return total
}
let incrementer = upCount()
print(incrementer()) // 캡쳐한 count = 0 -> total = 1 (count값도 1로 증가)
print(incrementer()) // 캡쳐한 count = 1 -> total = 2 (count값도 2로 증가)
print(incrementer()) // 캡쳐한 count = 2 -> total = 3 (count값도 3로 증가)
클로저가 참조하는 변수의 값(count)은 클로저가 생성될 당시의 값을 복사해오는 것이 아니라, 변수 자체에 대한 참조를 한다. 그렇기에 count값이 변하더라도 그대로 유지되어 있다.
func makeIncrementer(amount: Int) -> () -> Int {
var total = 0
return {
total += amount
return total
}
}
let incrementByTen = makeIncrementer(amount: 10)
let incrementBySeven = makeIncrementer(amount: 7)
// incrementByTen은 10씩 증가시키고,total은 incrementByTen의 클로저 내부에 독립적으로 존재
incrementByTen() // returns 10
incrementByTen() // returns 20
// incrementBySeven은 7씩 증가시키고, 그만의 total을 사용
incrementBySeven() // returns 7
// 다시 incrementByTen을 호출하면, 이전 total에서 이어서 증가
incrementByTen() // returns 30
값 캡처
var number = 42
// 값 타입으로 캡처 (캡처 시점의 값을 복사) -> [] 안에 넣은 값은 클로저 생성 당시의 값을 복사(캡처리스트)
let closure = { [number] in
print("Number is: \(number)") // 캡처된 시점의 42를 사용
}
number = 100
closure() // "Number is: 42" 출력
참조 캡처
var number = 42
// 참조 타입으로 캡처(외부에 있는 값을 복사)
let closure = {
print("Number is: \(number)") // 클로저 실행 시점의 number 값 사용
}
number = 100 // 값을 참조하였기에 값이 변동되면 바뀐다.
closure() // "Number is: 100" 출력
클로저의 실행이 본래 함수를 벗어나서도 실행되도록 하는 키워드
1) 내부 클로저를 외부 변수에 저장
2) 보통 GCD(비동기코드 사용) 비동기 작업에 흔히 사용된다. 함수 내부에서 선언된 클로저는 함수가 종료될 때 자동으로 해제되도록 하는데 @escaping이 붙은 클로저는 함수의 생명 주기를 넘어 함수 밖에서도 계속해서 참조될 수 있도록 만든것이다.
@escaping 클로저
에서는 self
를 명시적으로 참조해야한다. 왜냐하면 @escaping 클로저
는 클래스 인스턴스의 생명주기를 벗어나서 실행되기 때문이다.
@escaping클로저
는 클로저가 실행되기까지 클래스 인스턴스가 메모리에 유지되도록 하기위해 강하게 self
를 참조를 하게 된다. 이 경우 메모리 누수가 일어날 수 있기에 이를 방지하기 위해 [weak self]
를 캡쳐 목록을 사용하는게 좋다.
var completionHandlers: [() -> Void] = []
// 클로저를 배열에 추가
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
// 클로저를 바로 실행
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 } // 나중에 실행
someFunctionWithNonescapingClosure { x = 200 } // 바로 실행
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"
completionHandlers.first?()
print(instance.x)
// Prints "100"
@escaping함수
다른 하나는 nonescaping함수
이 두개의 차이는 실행 시점
에 있다. @escaping함수
는 함수가 반환 되더라도 나중에 실행 할 수 있다. 그렇기에 함수가 끝난 뒤에도 인스턴스를 계속 참조하기 위해서 명확히 클로저가 어떤 인스턴스를 참조하는지 알리기 위해 self
키워드를 통해 알려준다.class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
}
}
@escaping 클로저
는 함수 실행 범위를 벗어나 실행될 수 있기 때문에 self
를 강하게 참조하여 인스턴스의 메모리 유지가 필요하다.[weak self]
나 [unowned self]
를 사용해 순환 참조를 방지할 수 있다.struct SomeStruct {
var x = 10
mutating func doSomething() {
someFunctionWithEscapingClosure { x = 100 } // Error 발생
}
}
@escaping 클로저
는 함수의 실행 범위를 벗어나므로, 변경 가능한 참조를 통해 구조체 내부 값을 수정할 수 없다.@escaping 클로저
에서 self
를 가변 참조로 캡처할 수 없으며, 이는 Swift의 값 타입 관리와 일관성을 지키기 위한 제한이된다.
그럼 @escaping으로 남아있던 함수가 메모리에서 해제되는 시점은 언젠가여?? 만약 계속해서 해당 클로저를 안 사용한다면 어떻게 되나여
또 weak self로 해서 참조카운트를 증가시키지 않았을 때, 본래 객체가 사라지면 @escaping 클로저는 어떻게 self에 대해 처리하나요?