Closures : 코드 내에 전달되거나 사용될 수 있는 기능을 포함한 self-contained blocks
Closing over constants/variables : Closure가 자신이 정의된 context 의 constants/variables 을 capture 하고 참조를 저장할 수 있는 것
Swift 는 capture 된 것들의 메모리 관리를 대신 처리해줌
클로저는 아래의 세 개의 형태 중 하나를 띔
Full 정의와 이름 없이 함수의 기능을 하는 짧은 버전의 무언가를 작성하는 것이 유용한 경우가 있음. 특히 함수/메서드가 하나 이상의 argument 로 함수를 받을 때.
Closure expressions : inline closure 를 간결한 문법으로 작성할 수 있는 방법. 의도가 분명히 드러나면서도 간결하게 작성할 수 있는 syntax optimization 을 제공함.
e.g. sorted(by:) 메서드는 배열을 사용자가 제공한 sorting closure 를 바탕으로 결과를 연산함
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
// normal function
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
{ (<#parameters#>) -> <#return type#> in
<#statements#>
}
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
Closure 가 method 의 argument 로 전달되었기 때문에 Swift 는 parameter 의 type 과 return type 을 추론할 수 있음.
sorted(by:) method 가 string 배열에서 호출되고 있으니 argument 는 (String, String) -> Bool 의 function type 을 가져야 함을 추론. 따라서 (String, String), Bool 은 closure expression 의 정의 부분에 써지지 않아도 됨
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })
Closure 를 함수/메서드에 inline closure expression 으로 전달할 경우 항상 parameter / return type 을 추론할 수 있음. 따라서 inline closure 를 작성할 때 closure 가 function/method 의 argument 으로 사용된다면 full form 으로 작성하지 않아도 됨
Single-expression closure 는 해당 expression 의 결과를 return 키워드 없이 암묵적으로 return 가능
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 })
Swift 는 inline closure 에 자동으로 shorthand argument name 을 제공함. ($0, $1, $2, …)
Shorthand argument name 을 사용할 경우 closure 의 argument list 를 정의부분에서 뺄 수 있음.
reversedNames = names.sorted(by: { $0 > $1 } )
Swift 의 String type 은 greater-than operator > 를 두 개의 String parameter 를 받고, Bool 을 return 하는 메서드로 구현했고, 이는 sorted(by:) method 가 필요한 function type 과 일치하기 때문에 단순히 operator 를 전달할 수 있음
reversedNames = names.sorted(by: >)
함수의 마지막 argument 로 closure expression 을 전달해야 하고 closure expression 이 긴 경우, trailing closure 로 작성할 수 있음. Trailing closure 가 함수의 argument 임에도 불구하고 trailing closure 는 함수 호출의 괄호 다음에 쓸 수 있음.
Trailing closure 문법에서는 첫 번째 closure 의 argument label 을 쓰지 않음.
Closure 가 길지만 한 줄로 inline 으로 작성하기 어려울 때 유용.
func someFunctionThatTakesAClosure(closure: () -> Void) {
// function body goes here
}
// Here's how you call this function without using a trailing closure:
someFunctionThatTakesAClosure(closure: {
// closure's body goes here
})
// Here's how you call this function with a trailing closure instead:
someFunctionThatTakesAClosure() {
// trailing closure's body goes here
}
// sorted(by:) 를 trailing closure 로 작성한 버전
reversedNames = names.sorted() { $0 > $1 }
// closure expression 이 함수의 유일한 argument 이고 trailing closure 로 작성할 때 () 를 붙이지 않아도 됨
reversedNames = names.sorted { $0 > $1 }
함수가 여러 closure 를 받는 경우 첫 번째 trailing closure 의 arguement label 을 뺌. 아래 예시와 같이 함수를 두 개의 completion handler 를 받게 할 경우 분명하게 의도별로 code 를 나눌 수 있음
func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
if let picture = download("photo.jpg", from: server) {
completion(picture)
} else {
onFailure()
}
}
loadPicture(from: someServer) { picture in
someView.currentPicture = picture
} onFailure: {
print("Couldn't download the next picture.")
}
Completion handler 는 여러 handler 를 nesting 해야 할 때 가독성이 떨어지므로 이런 경우 asynchronous code 를 쓰는 것이 대안
Closure 는 정의된 context 내의 constant/variable 을 capture 할 수 있음. Closure 는 해당 constant/variable 을 정의한 scope 가 더 이상 존재하지 않더라도 body 내에서 해당 constant/variable 을 참조하고 수정할 수 있음
e.g. Closure 가 value 를 capture 한 가장 간단한 형태 : nested function. Nested function 은 outer function 의 argument, 정의된 constant/variable 을 capture 할 수 있음
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
incrementer() 는 runningTotal, amount 의 reference 를 capture 해서 makeIncrementer 가 끝나더라도 incrementer 가 호출되더라도 runningTotal 이 available 하도록 보장함
최적화로 Swift 는 closure 에 의해 값이 수정되지 않았고, closure 가 생성된 이후에 값이 수정되지 않은 경우 그 값을 capture 하고 복사본을 저장함. (mutable variable 에 대한 reference 를 tracking 하는 것보다 copy 가 가끔 더 빠르고 안전할 수 있기 때문)
var x = 10
let closure = {
print(x)
}
x = 20
closure() // prints 20
// Closure 는 x 를 copy 하지 않고 variable 자체를 capture
let x = 10
let closure = {
print(x)
}
// 이 경우 x 는 변하지 않았기 때문에 copy(10) 을 저장함
이게 왜 최적화냐?
최적화 : closure → copied value
Functions, closure 는 reference types.
Closure 를 constant/variable 에 할당할 때 실제로는 그 constant/variable 를 function / closure 의 reference 로 삼는 것.
// 새로운 상수를 할당했지만 같은 closure 를 참조하고 있음
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50
incrementByTen()
// returns a value of 60
Closure 는 closure 가 함수의 argument 로 전달되었지만, 함수가 return 된 이후에 호출될 때 실행될 때 escape a function 이라고 함. Closure 가 escape 되는 것을 허용하는 것을 나타내기 위해 parameter type 앞에 @escaping 을 작성.
var completionHandlers: [() -> Void] = []
// @escaping 키워드가 없을 경우 compile-time error
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
self 를 참조하는 escaping closure 는 self 가 class 의 인스턴스를 참조하는 경우 주의가 필요. Escaping closure 에서 self 를 capture 하는 것은 강한 참조를 만들기 쉬움.
Closure 는 일반적으로 body 에서 variable 을 사용해서 암묵적으로 capture 를 하지만, 위의 경우 명시적으로 표기해야 함. self 를 capture 하고 싶은 경우 self 를 명시적으로 쓰거나 closure 의 capture list 에 self 를 포함.
self 를 명시적으로 작성하면 의도를 나타낼 수 있고 reference cycle 이 있는지 확인해야 함을 명시적으로 드러냄.
// nonescaping closure 이기 때문에 self 를 암묵적으로 참조할 수 있음
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
class SomeClass {
var x = 10
func doSomething() {
// self 없을 경우 compile-time error
someFunctionWithEscapingClosure { self.x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200".
completionHandlers.first?()
print(instance.x)
// Prints "100".
class SomeOtherClass {
var x = 10
func doSomething() {
// closure 의 capture list 에 추가
someFunctionWithEscapingClosure { [self] in x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
self 가 struct / enum 의 인스턴스인 경우 항상 self 를 암묵적으로 참조할 수 있지만 escaping closure 는 self 의 struct / enum 의 mutable reference 는 암묵적으로 참조할 수 없음. Struct, enum 은 shared mutability 를 허용하지 않기 때문
struct SomeStruct {
var x = 10
mutating func doSomething() {
someFunctionWithNonescapingClosure { x = 200 } // OK
someFunctionWithEscapingClosure { x = 100 } // Error. self 를 붙이든 안 붙이든 상관 없음
}
}
Autoclosure 는 함수에 argument 로 전달된 expression 을 wrap 하기 위해 자동으로 생성된 closure. Argument 를 받지 않고, 호출됐을 때 wrapping 한 expression 의 값을 return. 이는 명시적인 closure 대신에 일반적인 표현을 써서 함수의 parameter 주변의 괄호를 제거할 수 있는 문법적인 이점을 제공함
Autoclosure 를 받는 함수를 호출하는 것은 흔하지만 해당 함수를 구현하는 것은 흔하지 않음.
Autoclosure 는 evaluation 을 연기시켜주는데, 내부 코드는 closure 를 호출하기 전까지 실행되지 않기 때문. Delaying evalution 은 코드가 언제 evaluated 되는지를 제어할 수 있게 해주기 때문에 연산적으로 부하가 있을 때나 side effect 가 있는 코드에 유용함.
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5".
// expression is not evaluated yet
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5".
// 실제로 closure 가 호출되기 전까지는 원소가 제거되지 않음
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4".
// 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!"
// autoclosure 로 마킹해서 explicit closure 대신에 autoclosure 를 받을 수 있게 함.
// closure 대신에 String argument 를 받는 것처럼 호출 가능
// 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!"
Autoclosure 를 escape 할 수 있게 하고 싶으면 @autoclosure, @escaping attribute 를 같이 쓸 수 있음
// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))
print("Collected \(customerProviders.count) closures.")
// Prints "Collected 2 closures."
for customerProvider in customerProviders {
print("Now serving \(customerProvider())!")
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"