-
Notifications
You must be signed in to change notification settings - Fork 2
Description
클로저(Closure)는 코드 안에서 사용되고 전달될 수 있는 코드 블럭
C와 Objective-C의 블럭(blocks) 또는 다른 언어의 람다(lambdas)와 유사
클로저는 어떤 상수나 변수의 참조를 캡쳐(capture)해 저장 가능
Swift는 해당 캡쳐와 관련한 모든 메모리를 알아서 처리
전역 함수(global function)와 중첩 함수(nested function)는 사실 클로저의 특별한 형태
클로저는 다음 세 가지 형태 중 하나를 띤다
- 전역 함수(global function) : 이름이 있고, 어떤 값도 캡쳐하지 않는 클로저
- 중첩 함수(nested function) : 이름이 있고, 인접한 함수로부터 값을 캡쳐할 수 있는 클로저
- 클로저 표현(closure expression) : 경량화된 문법으로 쓰여지고, 관련 문맥(context)으로부터 값을 캡쳐할 수 있는 이름 없는 클로저
Swift의 클로저 표현은 최적화되어 간결하고 명확한 스타일을 갖고 있다
다음과 같은 최적화 내용이 포함된다
- 문맥으로부터 매개변수와 반환 값의 타입을 추론
- 단일 표현 클로저의 암시적 반환
- 축약된 매개변수 이름
- 후위 클로저 문법
클로저 표현
중첩 함수는 더 큰 함수의 일부로 자체 포함된 코드 블록의 이름을 지정하고 정의하는 편리한 수단
그러나, 완전한 선언과 이름 없이 상수와 같은 모습의 짧은 함수가 중첩 함수를 사용하는 것보다 유용하다
함수가 인수로 하나 이상의 다른 함수를 받을 때 특히 더 그렇다
클로저 표현은 인라인 클로저를 간단하고, 문법에 집중하여 작성할 수 있는 방법
클로저 표현은 명확성이나 의도를 잃지 않고도 문법을 축약해 사용할 수 있는 몇 가지 최적화 방식을 제공
1) 정렬 메서드(sorted(by:))
Swift의 표준 라이브러리는 알려진 타입(클로저의 결과 값에 기반을 둔)의 배열을 정렬하는 sorted(by:) 메서드를 제공
정렬이 성공하면 sorted(by:) 메서드는 기존과 같은 타입, 크기를 갖는 새로운 배열을 정렬하여 반환
원래 배열은 sorted(by:)에 의해 수정되지 않는다
아래의 클로저 표현 예시는 문자열 배열을 알파벳 역순으로 정렬하기 위해 sorted(by:)를 사용한다.
sorted(by:) 메소드는 배열 요소와 같은 타입의 두 매개변수를 갖는 클로저를 수용
그리고, 정렬되었을 때 첫 번째 값이 두 번째 값보다 알파벳 순으로 먼저 나온다는 것을 보여 주기 위해 Bool 값을 반환
정렬 클로저는 (String, String) -> Bool 타입을 필요
정렬 클로저를 나타내는 한 가지 방법은 일반적인 함수를 작성하고 sorted(by:) 메서드에 인수로 전달
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"]이 방법은 a > b라는 단일 표현을 작성하기엔 지나치게 길 수도 있다
클로저 표현 문법을 이용하여 정렬 클로저를 한 줄에 작성하는 게 더 적합
2) 클로저 표현식 구문
클로저 표현식 구문의 기본적인 형태는 다음과 같다
{ (parameters) -> return type in
statements
}클로저 표현 문법의 매개 변수는 인-아웃 매개변수가 될 수 있지만, 기본 값을 가질 수는 없다
Variadic 매개변수를 사용할 수도 있으며, 매개변수 타입과 반환 타입에 튜플도 가능
아래의 예시는 위의 함수를 클로저 표현을 사용하여 간결화 한 버전
인라인 클로저에서 매개변수 타입과 반환 값 타입을 선언하는 것과 backward(::) 함수에서 선언하는 것은 동일
양쪽 모두 (s1: String, s2: String) -> Bool 타입이 작성
하지만, 인라인 클로저는 매개변수와 반환 값 타입을 바깥이 아닌 중괄호 안에 작성
클로저의 바디는 매개변수와 반환 값의 타입 선언이 끝났음을 알리는 역할인 in 키워드 이후에 시작
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})3) 문맥으로부터 타입 추론하기
정렬 클로저가 메서드의 인수로 들어가기 때문에
Swift는 해당 매개 변수와 반환 값의 타입을 추론할 수 있다
sorted(by:) 메서드는 문자열 배열에서 호출되며,
이것의 매개변수는 (String, String) -> Bool 타입의 함수여야만 한다
(String, String)과 Bool 타입은 클로저를 선언할 때 작성할 필요가 없다는 뜻
모든 타입이 추론 가능하기 때문에 반환 화살표(->)와 소괄호 역시 생략 가능
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )인라인 클로저 표현으로써 함수나 메서드에 클로저를 인수로 넣을 때,
항상 매개 변수와 반환 값의 타입을 추론 가능하다
결과적으로 클로저가 함수나 메서드의 매개변수로 사용될 때 인라인 클로저의 전체 형태를 적을 필요가 없다
하지만, 원한다면 코드의 모호성을 피하기 위해 명시적으로 타입을 표시 가능
4) 단일 표현 클로저에서의 암시적 반환
단일 표현 클로저는 return 키워드를 선언에서 생략
결과 값을 암시적으로 반환 가능
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )sorted(by:) 메소드에 인수로 들어간 함수의 타입은 클로저에 의해 Bool 값을 반환할 것이 명확
클로저의 바디가 포함하고 있는 단일 표현(s1 > s2)가 Bool 값을 반환하기 때문
반환 값을 도출하는 표현이 명확하기에 return 키워드를 생략 가능하다
5) 매개변수 이름의 축약
Swift는 자동적으로 인라인 클로저의 축약된 매개변수 이름을 제공한다. 이러한 이름은 $0, $1, $2와 같이 인덱스 기반으로 표현된다.
만약 클로저 표현에서 이런 축약된 매개변수 이름을 사용한다면, 클로저의 정의에서 매개변수 목록을 생략 가능
in 키워드 역시 생략 가능
reversedNames = names.sorted(by: { $0 > $1 } )축약은 되었지만 논리를 표현하는 데는 지장이 없다
인라인 클로저에 생략된 내용을 포함해 설명하면,
$0과 $1 매개변수를 두 개 받아서 $0이 $1 보다 알파벳 순으로 먼저 나오는지를 비교하고 그 결과(Bool)를 반환하라는 뜻이다
6) 연산자 메서드
Swift의 String 타입 연산자에는 String끼리 비교할 수 있는 비교 연산자(>)가 구현해되어 있다
그냥 연산자를 사용하여 표현도 가능하다
reversedNames = names.sorted(by: >)후위 클로저 (Trailing Closures)
클로저 표현식을 함수의 최종 인수로 함수에 전달해야 하고,
해당 클로저 표현이 길다면 후위 클로저(trailing closure)를 유용하게 사용 가능
후위 클로저는 호출하는 함수의 소괄호() 이후에 작성
후위 클로저 문법을 사용할 때는 함수 호출의 일부분으로써 매개변수 라벨을 클로저에 사용할 필요가 없다
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 }만약 함수의 매개변수가 후위 클로저뿐이라면 함수를 호출할 때 소괄호를 쓸 필요가 없다
reversedNames = names.sorted { $0 > $1 }후위 클로저는 한 줄에 작성할 수 없을 정도로 클로저가 충분히 클 경우 유용
ex. Swift 배열 타입에는 클로저 표현 하나만을 매개 변수로 갖는 map(_:) 메서드가 있다
예제 코드는 각 자리수를 구해서 그 자리수를 문자로 변환하고,
10으로 나눠서 자리수를 바꾸며 문자로 변환하는 것을 반복
해당 과정을 통해 숫자 배열을, 문자 배열로 전환
number값은 상수인데, 상수 값을 클로저 안에서 변수 var로 재정의 했기 때문에 number값의 변환이 가능
기본적으로 함수와 클로저에 넘겨지는 인수 값은 상수이다
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]
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"]함수가 여러 클로저를 사용하는 경우 첫 번째 후위 클로저에 대한 인수 레이블을 생략하고, 나머지 후위 클로저에 레이블을 지정한다
해당 방법으로 함수를 작성하면 두 가지 상황을 모두 처리하는 하나의 클로저가 아닌
성공적인 다운로드 후 사용자 인터페이스를 업데이트하는 코드에서, 네트워크 오류 처리를 담당하는 코드를 명확하게 분리 가능
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 핸들러는 특히 여러 핸들러를 중첩해야 하는 경우 읽기 어려울 수 있다
> 해당 경우 동시성을 활용한 비동기 코드를 사용하여 보완이 가능
## 값 캡쳐 (Capturing Values)
클로저는 그것이 정의된 주변의 문맥으로부터 상수나 변수를 캡쳐 가능
클로저는 바디에서 이 상수 / 변수의 값을 보내고 수정할 수 있다
원본 스코프가 더 이상 존재하지 않더라도 가능
Swift에서 값을 캡쳐할 수 있는 가장 간단한 방법은 중첩 함수(nested function)
중첩 함수는 바깥 함수의 매개변수나 그 안에서 선언된 상수 / 변수를 캡쳐해 사용할 수 있다
```swift
func makeIncrementer(forIncrement amount: Int) -> (() -> Int) {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
// makeIncrementer의 반환 값 타입은 () -> Int
// 해당 함수는 매개 변수가 없으며 Int 값을 반환incrementer() 함수만 따로 놓으면 일반적인 함수처럼 보이지 않는다
incrementer() 함수는 매개 변수가 없고, 블록 안에 runningTotal과 amount도 없다
해당 상위 블록 함수로부터 runningTotal과 amount의 참조를 캡쳐해 와서 incrementer()의 바디 안에서 사용하는 방식으로 작동makeIncrementer()가 끝나도 참조의 캡쳐는 사라지지 않고 유지
최적화를 위해 Swift는 더 이상 클로저에 의해 값이 사용되지 않으면
값을 복사해 저장하거나 캡쳐링 하지 않는다
Swift는 또 특정 변수가 더 이상 필요하지 않을 때 제거하는 것과 관련한 모든 메모리 관리를 알아서 처리한다
makeIncrementer(forIncrement:)의 결과로 함수를 할당하고
해당 함수를 호출하면, 호출마다 runningTotal에 10을 더하게 된다
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30만약 두 번째로 호출한다면 새로운 runningTotal의 참조를 갖게 된다.
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7
incrementByTen()
// returns a value of 40만약 클로저를 어떤 클래스 인스턴스의 프로퍼티로 할당하고
해당 클로저가 그 인스턴스를 캡쳐링하면 강한 순환참조에 빠지게 된다
즉, 인스턴스의 사용이 끝나도 메모리를 해제하지 못하게 됨
따라서, Swift는 이 문제를 다루기 위해 캡쳐 리스트(capture list)를 사용
참조 타입 (Reference Type)인 클로저
앞의 예제에서 incrementBySeven과 incrementByTen은 상수
그럼에도 runningTotal 변수를 계속 증가시키는 게 가능함
함수와 클로저는 참조 타입이기 때문
함수와 클로저를 상수나 변수에 할당할 때
실제로는 상수와 변수에 해당 함수나 클로저의 참조(reference) 즉, 메모리 주소가 할당
그래서 만약 한 클로저를 두 상수나 변수에 할당하면 그 두 상수나 변수는 같은 클로저를 참조하게 된다
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50
incrementByTen()
// returns a value of 60이스케이핑 클로저 (Escaping Closures)
클로저가 함수의 인수로 들어갔지만
함수가 반환된 이 후에 호출될 때, 클로저는 함수를 탈출했다고 말한다
클로저를 매개변수 중 하나로 선언할 때, 이 클로저는 탈출(escape)을 허용한다는 뜻으로
@escaping 어노테이션을 매개 변수 타입 앞에 작성하면 이스케이핑 클로저로 활용 가능
클로저가 탈출할 수 있는 하나의 방법은 함수 바깥에서 정의된 변수에 저장되는 것
ex. 많은 함수가 클로저를 completion handler로 취하기 위해 비동기로 작동
해당 함수는 클로저는 조작이 끝날 때까지 호출되지 않음에도 조작을 시작한 이 후에 반환
클로저는 이 후에 호출되기 위해 탈출할 필요가 있다
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}위 함수에서 인수로 전달된 completionHandler는 someFunctionWithEscapingClosure 함수가 끝나고 나중에 처리
만약 함수가 끝나고 실행되는 클로저에 @escaping 키워드를 붙이지 않으면 컴파일시 오류가 발생
@escaping를 사용하는 클로저에서는 self를 명시적으로 언급해야 한다
혹은, 캡쳐 리스트에 self를 포함시켜 암시적으로 참조해야 한다
someFunctionWithEscapingClosure(_:)는 이스케이핑 클로저이기 때문에 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"
class SomeOtherClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { [self] in x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
// 클로저의 캡처 리스트에 포함하여 doSomething()캡처 한 다음 암시적으로 참조하는 버전
struct SomeStruct {
var x = 10
mutating func doSomething() {
someFunctionWithNonescapingClosure { x = 200 } // Ok
someFunctionWithEscapingClosure { x = 100 } // Error
}
}
// self가 struct 또는 enum의 인스턴스인 경우 항상 self를 암시적으로 참조
// 이스케이핑 클로저는 self가 struct 또는 enum의 인스턴스일 때 self에 대한 mutable reference를 포착할 수 없다
// struct 또는 enum은 공유된 변동성을 허용하지 않는다 - 값 타입과 참조 타입의 차이자동 클로저 (Autoclosures)
자동 클로저는 매개변수 값이 없으며 특정 표현을 감싸서 다른 함수에 전달 인자로 사용할 수 있는 클로저
자동 클로저는 클로저를 실행하기 전까지 실제 실행이 되지 않는다
그래서, 복잡한 연산을 하는데 유용하다. 왜냐면 실제 계산이 필요할 때 호출되기 때문
1) 명시적인 클로저 구성
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 배열의 첫 번째 원소가 클로저 안의 코드에서는 지워졌음에도 불구하고,
클로저가 호출될 때까지 배열의 원소는 실제로 삭제되지 않는다
만약 클로저가 호출되지 않는다면, 안의 표현 역시 평가되지 않는다
함수의 인수로 클로저를 넣음으로써 같은 효과를 얻을 수 있다.
// 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함수는 매개변수로 (() -> String) 형, 즉 인자가 없고, String을 반환하는 클로저를 받는 함수
해당 함수를 호출할 때는 serve(customer: { customersInLine.remove(at: 0) } )와 같이
클로저 { customersInLine.remove(at: 0) }를 명시적으로 직접 넣을 수 있다
2) @autoclosure키워드 사용
함수의 인자로 클로저를 넣을 때 명시적으로 넣는 대신, @autoclosure키워드를 이용 가능
이제 클로저 대신 String 인자를 받는 함수를 호출할 수 있게 됨
customerProvider 매개변수 타입이 @autoclosure 속성으로 마크됐기 때문에, 매개변수는 자동적으로 클로저로 변환
따라서, 함수의 인자 값을 넣을 때 클로저가 아니라 클로저가 반환하는 반환 값과 일치하는 타입의 함수를 인자로 넣을 수 있다
클로저 인자에 @autoclosure를 선언하면,
함수가 이미 클로저인 것을 알기 때문에 반환 값 타입과 같은 값을 넣는 게 가능
var customersInLine = ["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와 @escaping 어노테이션을 모두 사용
var customersInLine = ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
// collectCustomerProviders 함수의 매개변수 customerProvider는 @autoclosure이면서 @escaping로 선언
// @autoclosure로 선언됐기 때문에 함수의 인자로 리턴 값이 String인 customersInLine.remove(at: 0) 형태의 구문 인수로 전달 가능
// @escaping로 선언됐기 때문에 해당 클로저는 collectCustomerProviders 함수가 종료된 후에 실행되는 클로저
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProviders.append(customerProvider)
// 클로저를 customerProviders에 저장하는 로직
}
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!"
// 최종적으로 customersInLine은 []