Swift 공식 문서의 일곱번 째 단원인 Closures을 읽고 정리를 해보려고 합니다.
Swift Apple 공식 문서 7챕터 Closures
Closure(클로저)는 코드에서 함수적인 것을 독립적으로 사용할 수 있는 코드입니다.
다른 프로그래밍 언어의 lambda와 비슷한 역할을 합니다.
이러한 클로저는 정의된 상수나 변수에 대해 값을 저장하고 캡처할 수 있습니다.
Swift는 이러한 동작을 위해 모든 메모리 관리를 처리해줍니다.
전역 함수, 중첩 함수(Nested function)가 Function 단원에서 설명되었었는데 이는 클로저의 특별한 케이스 중 하나입니다. 그럼 클로저가 어떤 모양을 가질 수 있는지부터 살펴보겠습니다. 클로저는 다음 3개의 모양 중 하나를 가지게 됩니다.
전역 함수는 이름을 가지고 어떠한 값도 캡처를 하지 않는 클로저입니다.
중첩 함수는 이름을 가지고 그것을 포함하는 enclosing 함수의 값들을 캡처할 수 있는 클로저입니다.
클로저 표현식은 이름이 없는 클로저로 주변의 코드에서 값들을 캡처할 수 있는 클로저입니다.
Swift의 클로저는 깔끔한 구문을 위해 최적화 기능이 있습니다.
구문에서 매개 변수, 리턴 값 타입 유추를 할 수 있습니다.
클로저에서 암시적으로 반환 값을 반환할 수 있습니다.
매개변수의 인수 이름을 간단히 할 수 있습니다.
Trailing closure syntax (함수의 마지막을 클로저로 마무리할 수 있습니다.)
중첩 함수는 그것을 감싸고 있는 함수 안에서 함수를 정의하여 정의한 함수를 이름으로 쉽게 사용할 수 있는 수단입니다.
Closure Expressions(클로저 표현식)은 간단하게 클로저를 작성하는 방법입니다.
이렇게 간단하게 작성하기 위해서는 몇 가지 최적화 방법을 제공합니다.
Swift의 기본 라이브러리에서 제공하는 sorted(by:)
메서드는 사용자가 정의한 값들을 Array 타입으로 반환해 주는 클로저입니다.
sorted(by:)
메서드는 Array를 반환해 주는 메서드이기 때문에 Array를 정렬하여 저장하고 싶다면 새로운 변수나 상수에 할당해 줘야 합니다.
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
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
})
여기서 보면 아까 따로 작성한 backward(_: _:)
라는 함수를 아예 매개변수를 써주는 () 속에 집어넣었습니다. 클로저의 실행문은 in
키워드 다음에 쓰면 됩니다.
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
reversedNames = names.sorted(by: { $0 > $1 } )
in
키워드 마저 생략해버린 클로저입니다. reversedNames = names.sorted(by: >)
// 처음 코드
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
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
}
만약 아까 만든 정렬 함수에 후행 클로저로 사용하면 다음과 같아집니다.
reversedNames = names.sorted() { $0 > $1 }
let digitNames = [
0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]
우리는 numbers라는 Array를 digitNames를 사용해서 숫자를 String 타입으로 바꾸고 싶습니다.
let strings = numbers.map { (number) -> String in
var number = number
var output = ""
repeat {
output = digitNames[number % 10]! + output
number /= 10
} while number > 0
return output
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]
digitNames[number % 10]!
에 보면 dictionary의 Value는 옵셔널 값이기 때문에 이와 같이 강제로 추출해 준 것입니다. 그렇다면 만약 하나의 함수에서 여러 개의 클로저를 사용하고 싶다면 어떻게 해야 할까요? 첫 번째 후행 클로저를 사용할 클로저의 인수는 생략하고 그 후의 후행 클로저들에는 인수 레이블을 지정하면 됩니다.
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.")
}
클로저가 주변 코드에서 상수와 변수를 캡처하면 그 뒤에 상수와 변수가 접근할 수 없게 되더라도 클로저는 해당 상수 및 변수의 값을 참조하고 수정할 수 있습니다.
Swift에서 가장 쉽게 값을 캡처하는 방법은 중첩 함수를 사용하는 것입니다.
중첩 함수는 자신을 둘러싼 함수의 매개변수도 캡처할 수 있고 정의된 상수나 변수도 캡처할 수 있습니다.
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
makeIncrementer
의 매개변수인 amount와 함수 내에서 정의된 runningTotal 변수를 캡처하여 사용하는 것을 볼 수 있습니다. 실제 사용은 다음과 같습니다.
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30
위의 예에서 본 incrementByTen은 상수로 선언된 값이지만 계속해서 그 안의 값들이 변하는 것을 볼 수 있었습니다.
그 이유는 함수와 클로저는 참조 타입이기 때문입니다. 함수나 클로저를 상수나 변수에 할당하게 되면 상수나 변수에 복사되는 것이 아닌 메모리 주소만 참조하게 되고 이를 만약 다른 상수나 변수에 incrementByTen을 할당하면 다음과 같은 일이 일어나게 됩니다.
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 40
incrementByTen()
// returns a value of 50
아까 증가된 값이 그대로 유지되는 것을 볼 수 있습니다.
클로저는 함수에 대한 인수로 클로저가 전달될 때 함수를 Escape 한다고 하지만 사실은 함수가 반환된 후에 호출됩니다.
클로저를 매개 변수 중 하나로 사용하는 함수를 선언하면 매개변수 타입 앞에 @escaping
키워드를 작성해서 클로저가 Escape 될 수 있음을 나타낼 수 있습니다.
클로저가 escape 할 수 있는 한 가지 방법은 함수 외부에 정의된 변수에 클로저를 저장하고 이를 매개변수로 사용하는 것입니다.
예를 들어 비동기 작업을 시작하는 많은 함수는 완료 핸들러로 클로저를 사용합니다.
함수는 작업을 시작한 후 반환되지만 작업이 완료될 때까지 클로저가 호출되지 않습니다. 클로저는 Escape 해야 나중에 호출할 수 있는데 예를 들면 다음과 같습니다.
var completionHandlers = [() -> Void]()
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure(_ :)
함수는 매개변수로 클로저를 받고 완료된 작업을 외부에서 선언된 Array에 추가하는 함수입니다. @escapeing
을 사용하지 않으면 컴파일 오류를 갖게 됩니다. 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"
class SomeOtherClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { [self] in x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
struct SomeStruct {
var x = 10
mutating func doSomething() {
someFunctionWithNonescapingClosure { x = 200 } // Ok
someFunctionWithEscapingClosure { x = 100 } // Error
}
}
Autoclosure는 함수에 인수로 전달되는 표현식을 감싸기 위해 자동으로 생성되는 클로저입니다.
매개변수를 가지지 않으며 호출될 때 그 안에 있는 표현식의 값을 반환합니다.
편의를 위해 Autoclosure를 사용할 때는 명시적인 클로저 대신 정규 표현식을 작성해서 함수의 매개변수를 감싸는 괄호를 생략할 수 있습니다.
Autoclosure를 수행하는 함수를 호출하는 것은 일반적이지만 이러한 함수를 구현하는 것은 일반적이진 않습니다.
예를 들어 assert(condition:message:file:line:)
함수는 condition
, message
매개변수를 위해 오토 클로저를 사용합니다. condition 매개변수는 디버그 빌드에서만 사용되고 message 매개변수는 condition이 false 일 경우에만 사용됩니다.
Autoclosure를 사용하면 클로저를 호출할 때까지 내부 코드가 실행되지 않기 때문에 사용이 지연됩니다. 이렇게 지연되는 것은 계산 비용이 크거나 부작용이 있는 곳에서 유용하게 쓰일 수 있는데요, 이는 코드의 사용 시점을 제어할 수 있기 때문입니다. 예를 한 번 보겠습니다.
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"
위와 같은 코드에서 customersInLine Array에 있는 항목을 customerProvider에서 클로저 내부 코드로 제거하지만 실제로는 제거가 되지 않는 것을 볼 수 있습니다.
즉 클로저가 실제로 호출되지 않는 이상 수행이 되지 않는 것이죠. 실제로 호출을 하게 되면 클로저 내부 코드를 수행해 Array의 항목이 하나 제거되는 것을 볼 수 있습니다.
// 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 함수는 매개변수로 명시적으로 클로저를 받고 Array의 항목을 하나 제거하며 제거된 값을 반환받아 print로 출력하는 함수입니다.
// 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
키워드를 사용해 autoclosure를 사용한 것입니다. 이렇게 하면 클로저를 호출할 때 마치 String 타입을 사용하는 것처럼 함수를 호출할 수 있습니다. @autoclosure를 붙이게 되면 인수가 자동으로 클로저로 변환됩니다.만약 autoclosure와 escape 클로저를 함께 사용하고 싶다면 둘 다 사용할 수도 있습니다.
// 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!"
위와 같이 collectCustomerProviders의 매개변수 customerProvider로 전달된 클로저를 호출하는 대신 collectCustomerProviders 함수는 클로저를 customerProviders Array에 추가합니다.
Array는 collectCustomerProviders 밖에서 선언되었기 때문에 함수가 반환된 후 Array의 클로저가 실행될 수 있습니다.
즉 매개변수 customerProvider의 값이 함수의 범위를 벗어날 수 있어야 하는 것입니다.