클로저와 함수는 모두 참조 타입으로 캡처 중인 변수 값을 바꿀 수 있다.
클로저나 함수에 상수나 변수를 할당한다는 말은 곧 그 값이 함수나 클로저에 대한 참조가 되도록 세팅하는 것이다. 따라서 클로저를 두 개의 상수나 변수에 할당한다면, 할당을 받은 이 값들이 동일한 클로저를 참조하게 된다.
클로저가 함수 아규먼트로 넘어갈 때 함수를 탈출하고, 함수가 리턴되면 호출된다. 클로저를 파라미터로 받는 함수를 선언할 때, @escaping
이라는 코드를 파라미터 타입 전에 적으면 이스케이프할 수 있다는 뜻.
함수 밖에 정의된 변수에 클로저 값을 저장하는 방법으로 클로저는 이스케이프할 수 있다.
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
함수 바깥의 completionHandlers
에 함수 someFunctionWithEscapingClosure
안에서 처리한 completionHandler
값이 저장되는데, 비동기적 처리를 할 때 유용한 기법이다. 이때 @escaping
을 붙여야 한다.
자기 자신을 참조하는 이스케이핑 클로저는 클래스 인스턴스를 참조할 때 참조 사이클(reference cycle
)을 만들 수도 있기 때문에 주의해야 한다. self
를 직접 작성하거나 클로저 캡처 리스트에 포함시켜서 참조 사이클이 없다고 확인해주어야 한다.
completionHandlers
를 호출한 뒤에 instance.x
값이 100
으로 바뀐 건 someFunctionWithEscapingClosure
가 실행되었다는 뜻. self
를 체크하자.
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"
캡처 리스트에 self
를 포함시키는 doSomething()
은 암묵적으로 self
를 참조한다.
class SomeOtherClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { [self] in x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
함수에 아규먼트로 전달되는 표현을 감쌀 때 자동으로 만들어지는 클로저를 오토 클로저라고 한다. 오토 클로저는 아무 아규먼트도 받아들이지 않고, 호출되면 바로 자기 내부에 들어 있는 값을 리턴한다. 오토 클로저는 클로저 표현 대신 일반적인 표현으로도 사용 가능하다는 점에서 코딩할 때 편리하다.
오토 클로저는 클로저를 호출하기 전까지 코드가 작동하지 않는다는 점에서 값 계산을 늦춰준다. 따라서 부작용이 있거나 연산량이 많은 코딩을 넘겨줄 때 이 계산을 늦춰주는 게 유용하다.
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"
remove
를 통해 0
번째 인덱스 원소를 삭제해야 하지만, 클로저가 실제로 호출될 때까지는 연산이 이루어지지 않는다. 이때 customerProvider
는 () -> String
형 함수로 파라미터 없이 문자열을 리턴한다.
클로저를 함수에 아규먼트로 패스할 때 값 연산을 늦추는 걸 볼 수 있다.
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"
serve
함수를 호출할 때 파라미터로 넘겨주는 클로저 안에 remove
를 실행하는데, 이때 0
번 인덱스의 원소를 파라미터로 넘겨준다.
오토 클로저를 사용할 수도 있다.
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"