클로저는 코드블럭으로 C와 Objective-C의 블럭과 다른 언어의 람다와 비슷하다. 전역 함수와 중첩 함수은 실제 클로저의 특별한 경우입니다. 클로저는 다음 세 가지 형태 중 하나를 갖습니다.
Swift에서 클로저 표현은 최적화 되어서 간결하고 명확하다. 최적화에는 다음과 같은 내용을 포함한다.
클로저 표현은 인라인 클로저를 명확하게 표현하는 방법으로 문법에 초점이 맞춰져 있다. 클로저 표현은 코드의 명확성과 의도를 잃지 않으면서 문법을 축약해 사용할 수 있는 다양한 문법의 최적화 방법을 제공한다.
Swift의 표준 라이브러리에 sorted(by:)
라는 알려진 타입의 Array 값을 정열하는 메소드를 제공한다. 여기 by
에 어떤 방법으로 정렬을 수행할 것인지에 대해 기술한 클로저를 넣으면 그 방법대로 정렬된 Array를 얻을 수 있다. sorted(by:)
메소드는 원본 Array는 변경하지 않는다.
let names = ["짱구","철수","맹구","이슬"]
sorted(by:)
메소드는 Array의 콘텐츠와 같은 타입을 갖고 두 개의 인자를 갖는 클로저를 인자로 사용한다. names
의 콘텐츠는 String 타입이므로(String, String) -> (Bool)
의 타입의 클로저를 사용해야한다.
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["철수", "짱구", "이슬", "맹구"]
backward
클로저를 만들고 names.sorted(by: backward)
에 넣으면 원본 Array에서 순서가 바뀐 Array를 얻을 수 있다.
클로저 표현 문법은 일반적으로 아래의 형태를 띈다.
{ (parameters) -> return type in
statements
}
인자로 넣은 parameters
, 인자값으로 처리할 내용을 기술하는 statements
그리고 return type
이다.
앞의 backward
클로저를 이용해 Array를 정렬하는 코드는 클로저 표현을 이용해 다음과 같이 바꿀 수 있다.
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 })
위와 같이 클로저의 인자값과 반환 타입을 생략할 수 있지만, 가독성과 코드의 모호성을 피하기 위해 타입을 명시할 수도 있다.
단일 표현 클로저에서는 반환 키워드를 생략할 수 있다.
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 })
이렇게 타입을 생략해도 Swift의 타입추론 덕분에 어떠한 모호성도 없이 s1과 s2를 인자로 받아 그 두 값을 비교한 결과를 반환한다.
Swift는 인라인 클로저에 자동으로 축약 인자 이름을 제공한다.
이 인자를 사용하면 인자값을 순서대로 $0, $1, $2 등으로 사용할 수 있다. 축약 인자 이름을 사용하면 인자값과 그 인자로 처리할 때 사용하는 인자가 같다는 것을 알기 때문에 인자를 입력 받는 부분과 in
키워드 부분을 생략할 수 있다.
reversedNames = names.sorted(by: { $0 > $1 })
축약은 되었지만 논리를 표현하는데 지장이 없다.
인라인 클로저에 생략된 내용을 포함해 설명하자면
1. $0과 $1 인자를 두 개 받아서
2. $0이 $1 보다 큰지를 비교하고
3. 그 결과(Bool)를 반환해라
이다.
심지어 여기서도 축약을 더 할 수 있다. Swift의 String
타입 연산자에는 String
끼리 비교할 수 있는 비교 연산자(>)를 구현해 두었다.
reversedNames = names.sorted(by: >)
만약 함수의 마지막 인자로 클로저를 넣고 그 클로저가 길다면 후위 클로저를 사용할 수 있다.
func someFunctionThatTakesAClosure(closure: () -> Void) {
// function body goes here
}
위 클로저의 인자값 입력 부분과 반환형 부분을 생략할 수 있다.
someFunctionThatTakesAClosure(closure: {
// closure's body goes here
})
이것을 후위 클로저로 표현하면 이렇게 표현할 수 있다.
someFunctionThatTakesAClosure() {
// trailing closure's body goes here
}
앞의 정렬 예제를 후위 클로저를 이용해 표현할 수 있다.
reversedNames = names.sorted() { $0 > $1 }
만약 함수의 마지막 인자가 클로저이고 후위 클로저를 사용하면 괄호()
를 생략할 수 있다.
reversedNames = names.sorted { $0 > $1 }
이번에는 후위 클로저를 이용해 Int를 String으로 Mapping하는 예제를 살펴보자.
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]
이 값을 Array의 map(_:)
메소드를 이용해 특정값을 다른 특정값으로 매핑할 수 있는 클로저를 구현한다.
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
}
print(strings)
// strings는 타입 추론에 의해 String타입의 Array를 갖는다.
// Prints : ["OneSix", "FiveEight", "FiveOneZero"]
위 코드는 각 자리수를 구해서 그 자리수를 문자로 변환하고, 10으로 나눠서 자리수를 바꾸며 문자로 변환하는 것을 반복한다. 이 과정을 통해 Int 타입의 Array를 String 타입의 Array로 바꿀 수 있다.
digitNames[number % 10]!
에서 뒤에!
가 붙어있는 것은 Dictionary의subscipt
는 옵셔널이기 때문이다.
즉, Dictionary에서 특정key
에 대한 값은 있을 수도 있고 없을 수도 있기 때문이다.
클로저는 특정 문맥의 상수나 변수의 값을 캡쳐할 수 있다. 다시말해 원본 값이 사라져도 클로저의 body
안에서 그 값을 활용할 수 있다. Swift에서 값을 캡쳐하는 가장 단순한 형태는 중첩 함수이다. 중첩 함수는 함수의 body
에서 다른 함수를 다시 호출하는 형태로 된 함수이다.
func makeINcrementer(forIncrement amount: Int) -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
이 함수는 makeIncermenter
함수 안에서 incermenter
함수를 호출하는 형태로 중첩 함수이다.
인자와 반환값 (forIncrement amount: Int) -> () -> Int
중에 처음 ->
을 기준으로 앞의 (forIncrement amount: Int)
부분이 인자값이고 () -> Int
부분이 반환값이다.
반환값을 인자가 없고 Int
형의 클로저를 반환한다는 의미이다.
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
incrementer
함수는 파라미터가 없고runningTotal
과 amount
도 없지만 runningTotal
과 amount
가 캡쳐링 되어서 함수는 돌아간다.
let incrementByTen = makeInrcementer(forIncerment: 10)
makeIncrementer
함수는 클로저를 반환한다.
여기서 makeIncrementer
내부의 incrementer
함수를 실행하는 메소드를 반환한다.
incrementByTen()
// 값으로 10을 반환
incrementByTen()
// 값으로 20을 반환
incrementByTen()
// 값으로 30을 반환
함수가 각기 실행되지만 실제로는 변수 runningTotal
과 amount
가 캡쳐링 되서 그 변수를 공유하기 때문에 계산이 누적된다.
이때 새로운 클로저 생성하게 되면 고유의 저장소에 runningTotal
과 amount
를 캡쳐링해서 사용하기 때문에 다른 값이 나온다.
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7
또 다시 이전의 클로저를 실행한다면 이전 저장소의 변수를 사용해 계산한다.
incrementByTen()
// 값으로 40을 반환
앞의 예제에서 incrementBySeven
과 incrementByTen
은 상수이다. 그런데 함수와 클로저는 참조 타입이기 때문에 runningTotal
변수를 계속 증가시킬 수 있다. 함수와 클로저를 상수나 변수에 할당할 때 실제로는 상수와 변수에 해당 함수나 클로저의 참조가 할당된다.
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 값으로 50을 반환
앞에서 사용했던 클로저를 상수에 할당하고 실행시키면 사용한 클로저의 마지막 상태에서 한번 더 계산한 결과값을 반환한다.
클로저를 함수의 파라미터로 넣을 수 있는데, 함수가 끝나고 실행되는 클로저 예를 들어, 비동기로 실행되거나 completionHandler
로 사용되는 클로저는 파라미터 타입 앞에 @escaping
이라는 키워드를 명시해야 한다.
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
위 함수에서 인자로 전달된 completionHandler
는 someFunctionWithEscapingClosure
함수가 끝나고 나중에 처리된다. 만약 함수가 끝나고 실행되는 클로저에 @escaping
키워드를 붙이지 않으면 컴파일시 오류가 발생한다.
@escaping
을 사용하는 클로저에서는 self
를 명시적으로 언급해야 한다.
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure() // 함수 안에서 끝나는 클로저
}
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
// 명시적으로 self를 언급
someFunctionWithNonescapingClosure { x = 200 }
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"
completionHandlers.first?()
print(instance.x)
// Prints "100"
자동클로저는 인자값이 없으며 특정 표현을 감싸서 다른 함수에 전달 인자로 사용할 수 있는 클로저이다. 자동클로저는 클로저를 실행하기 전까지 실제 실행이 되지 않고 실제 계산이 필요할 때 호출이 되기 때문에 복잡한 연산을 하는데 유용하다.
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 : ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"
serve
함수는 인자로 () -> String
형, 즉 인자가 없고, String
을 반환하는 클로저를 받는 함수이다.
그리고 이 함수를 실행할 때는
serve(customer: { customersInLine.remove(at: 0) } )
이와 같이 클로저 { customersInLine.remove(at: 0) }
를 명시적을 직접 넣을 수 있다.
위 예제에서는 함수의 인자로 클로저를 넣을 때 명시적으로 넣는 경우에 대해 알아보았다. 위 예제를 @autoclosure
키워드를 이용해서 더 간결하게 사용할 수 있다.
// 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!"
serve
함수의 인자를 받는 부분 customerProvider: @autoclosure ()
에서 클로저의 인자 ()
앞에 @autoclosure
라는 키워드를 붙였다. 이 키워드를 붙임으로써 인자값은 자동으로 클로저로 변환된다. 그래서 함수의 인자값을 넣을 때 클로저가 아니라 클로저가 반환하는 반환값과 일치하는 형의 함수를 인자로 넣을 수 있다.
serce(customer: { customersInLine.remove(at: 0) })
이 코드를 @autoclosure
키워드를 사용했기 때문에
serve(customer: customersInLine.remove(at: 0))
이렇게 {}
없이 사용할 수 있다.
정리하면 클로저 인자에 @autoclosure
를 선언하면 함수가 이미 클로저인것을 알기 때문에 리턴값 타입과 같은 값을 넣어줄 수 있다.
자동클로저 @autoclosure
는 이스케이프 @escaping
와 같이 사용할 수 있다.
// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
// 클로저를 저장하는 배열을 선언
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProviders.append(customerProvider)
}
// 클로저를 인자로 받아 그 클로저를 customerProviders 배열에 추가하는 함수를 선언
collectCustomerProviders(customersInLine.remove(at: 0))
// 클로저를 customerProviders 배열에 추가
collectCustomerProviders(customersInLine.remove(at: 0))
print("Collected \(customerProviders.count) closures.")
// Prints "Collected 2 closures."
// 2개의 클로저가 추가 됨
for customerProvider in customerProviders {
print("Now serving \(customerProvider())!")
// 클로저를 실행하면 배열의 0번째 원소를 제거하며 그 값을 출력
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"
collectCustomerProviders
함수의 인자 customerProvider
는 @autoclosure
이면서 @escaping
로 선언되었다. @autoclosure
로 선언됐기 때문에 함수의 인자로 리턴값 String
만 만족하는 customersInLine.remove(at: 0)
형태로 함수 인자에 넣을 수 있고, 이 클로저는 collectCustomerProviders
함수가 종료된 후에 실행되는 클로저이기 때문에 인자 앞에 @escaping
키워드를 붙였다.
Javascript 에서 클로저를 이해하기가 그렇게 어려웠는데 Swift에도 람다 대신해서 클로저가 있군요. 람다를 대신한다는 말에서 Swift는 잘 모르지만 힌트 얻고 갑니다 :D