-
ClosuresiOS/Swift 공식문서 2022. 2. 16. 17:20
Closure란?
코드에서 여러 군데에 넘겨져서 사용할 수 있는 독립적인 기능 블록을 의미한다. 클로저는 자신이 정의된 context로부터 상수/변수에 대한 참조를 캡쳐하고 저장할 수 있다. Swift가 캡쳐 관련 메모리 관리를 알아서 처리한다.
클로져는 세가지 형태를 가진다.
a. 전역 함수: 이름은 있고, 캡쳐하는 값이 없는 클로져
b. 중첩 함수: 이름이 있고, 자신을 감싸고 있는 함수로부터 값을 캡쳐하는 클로져
c. 클로져 표현(Closure Expressions): 이름이 없고, 주변 context로부터 값을 캡쳐할 수 있는 lightweight syntax클로져 표현은 최적화되어 있어 간결하고 명확하다. 최적화로는 다음과 같은 요소들이 있다.
a. context로부터 매개변수 타입과 리턴 타입 추론
b. single-expression closure에서의 암시적 리턴
c. 축약된 인자 이름 (shorthand argument names)
d. 후위 클로져 문법 (trailing closure syntax)Closure Expressions
클로저 표현은 인라인 클로저를 명확하게 표현하는 방법으로 문법에 초첨이 맞춰져 있다. 클로저 표현은 코드의 명확성과 의도를 잃지 않으면서도 문법을 축약해 사용할 수 있는 다양한 문법의 최적화 방법을 제공한다.
The Sorted Method
sorted(by:)
는 Swift의 표준 라이브러리에서 제공하는 메서드이다. 매개변수로 들어가는 클로져를 이용하여 정렬을 수행한다. 기존의 배열은 수정되지 않고, 정렬한 결과에 해당하는 새로운 배열을 반환한다. 다음 배열을 예시로 들자.let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
매개변수로 들어가는 클로져는 같은 타입의 두 매개변수를 받고
Bool
값을 반환해야 한다. 따라서 위 배열을 정렬하기 위해서는(String, String) -> Bool
의 타입을 가진 함수를 사용해야 한다.첫번째 방법은 일반적인 함수를 만들어서 매개변수로 넣어주는 것이다.
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"]
Closure Expression Syntax
클로져 표현은 다음과 같은 형태를 따른다.
{ (parameters) -> return type in statements }
예시에서는 다음과 같이 사용할 수 있다. 다음과 같이 함수로 정의된 형태가 아닌 인자로 들어가 있는 형태의 클로져를 인라인 클로져(inline closure)라고 한다.
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 })
Inferring Type From Context
위 예시에서
sorted(by:)
메서드의 경우 매개변수의 타입이(String, String) -> Bool
이어야 하는 것을 알고 있다. 따라서 클로져 표현을 적을 때 타입을 생략할 수 있다.reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
Implicit Returns from Single-Expression Closures
statement가 단 하나인 경우
return
을 생략할 수 있다.reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
Shorthand Argument Names
Swift는 인라인 클로져에게 자동적으로 축약 인자 이름을 제공한다. 클로져의 매개변수의 값들을
$0
,$1
,$2
등으로 사용할 수 있다. 축약 인자 이름을 사용하는 경우 클로져의 정의에서 매개변수 목록과in
키워드를 생략할 수 있다. 가장 큰 넘버를 가진 매개변수가 매개변수의 개수를 결정한다.reversedNames = names.sorted(by: { $0 > $1 } )
Operator Methods
위 예시에서 클로져의 구현이 연산자를 이용한 비교 결과와 같기 때문에 그냥 연산자 하나로 대체할 수 있다.
reversedNames = names.sorted(by: >)
Trailing Closures
클로져를 함수의 마지막 인자로 넣어야 하고, 해당 클로져가 길다면, 후위 클로져를 대신 사용하는 게 나을 수 있다. 다음과 같이 클로져를 매개변수로 사용하는 함수를 예시로 들자.
func someFunctionThatTakesAClosure(closure: () -> Void) { // function body goes here }
클로져의 인자 부분과 반환 타입을 생략하여 다음과 같이 작성하여 호출할 수 있다.
someFunctionThatTakesAClosure(closure: { // closure's body goes here })
후위 클로져를 사용하여 다음과 같이 함수의 괄호 뒤에 중괄호 안에서 클로져를 작성하여 호출할 수 있다.
someFunctionThatTakesAClosure() { // trailing closure's body goes here }
이전에 문자열 배열을 정렬하는 예시에서는
sorted(by:)
메서드를 다음과 같이 호출할 수 있다.reversedNames = names.sorted() { $0 > $1 }
후위 클로져가 유일한 매개변수인 경우 괄호(
()
)를 생략할 수 있다.reversedNames = names.sorted { $0 > $1 }
배열의
map(_:)
메서드도 아래처럼 후위 클로져를 이용하여 호출할 수 있다.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"]
Multiple Trailing Closures
함수가 클로져를 여러 개 사용하는 경우, 함수 호출 시 첫번째 후위 클로져는 argument label을 생략하고, 나머지 후위 클로져들은 적어야 한다. 다음 함수를 예시로 들자.
func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) { if let picture = download("photo.jpg", from: server) { completion(picture) } else { onFailure() } }
호출 시 두번째 클로져만 argument label을 적는 것을 볼 수 있다.
loadPicture(from: someServer) { picture in someView.currentPicture = picture } onFailure: { print("Couldn't download the next picture.") }
Capturing Values
클로져는 자신이 정의된 context에서 다른 상수/변수를 캡쳐할 수 있다. 즉, 상수/변수를 정의한 스코프가 더 이상 존재하지 않아도 클로져의 body 안에서 해당 값을 참조하여 수정할 수 있다는 뜻이다.
이러한 캡쳐의 가장 간단한 형태는 중첩 함수이다. 중첩 함수는 자신을 감싸는 함수의 매개변수나 해당 함수 안에서 정의된 상수/변수를 캡쳐할 수 있다.
다음 예시를 보자.
makeIncrementer(forIncrement:)
함수 안에서 정의된incrementer()
함수는 외부 함수에서 정의된runningTotal
의 값을 10씩 증가시키면서 값을 반환한다.makeIncrementer(forIncrement:)
의 호출이 끝난 후runningTotal
을 선언한 스코프는 사라졌지만,incrementer()
함수는runningTotal
의 값에 접근할 수 있는 것이다.func makeIncrementer(forIncrement amount: Int) -> () -> Int { var runningTotal = 0 func incrementer() -> Int { runningTotal += amount return runningTotal } return incrementer } let incrementByTen = makeIncrementer(forIncrement: 10) incrementByTen() // returns a value of 10 incrementByTen() // returns a value of 20 incrementByTen() // returns a value of 30
또 다른 incrementer를 만들면, 첫번째 함수와는 별개인
runningTotal
변수에 대한 참조를 저장할 것이다. 따라서 0에서 시작하여 7씩 증가한다. 두 함수 각각의runningTotal
변수는 별개의 변수이다.let incrementBySeven = makeIncrementer(forIncrement: 7) incrementBySeven() // returns a value of 7
Closures Are Reference Types
클로져를 상수/변수에 할당하면, 해당 상수/변수는 해당 클로져에 대한 "참조"를 저장하는 것이다. 다음 예시에서는
alsoIncrementByTen
의 경우incrementByTen()
에서 증가시키는runningTotal
을 그대로 증가시키는 것을 볼 수 있다.let alsoIncrementByTen = incrementByTen alsoIncrementByTen() // returns a value of 40 incrementByTen() // returns a value of 50
Escaping Closures
클로져가 함수의 매개변수로 들어갔는데, 해당 함수가 반환된 이후 호출된다면 클로져가 함수를 escape한다고 한다. escape이 가능하게 하기 위해서는 함수 선언 시 클로져의 타입 앞에
@escaping
키워드를 적어야 한다.다음 예시를 살펴보자. escaping을 허용한 함수와 허용하지 않은 함수가 있다.
var completionHandlers: [() -> Void] = [] func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) { completionHandlers.append(completionHandler) } func someFunctionWithNonescapingClosure(closure: () -> Void) { closure() }
그 다음 클래스의 한 메서드에서 두 함수를 호출한다. 아래 코드는 정상적으로 실행될 것이다. 그러나 위 코드에서
@escaping
키워드를 지워서 실행하면completionHandlers.first?()
코드를 실행 시 에러가 출력됨을 확인할 수 있다. escaping 클로져에서 클래스 멤버에 접근하려면self
를 명시해야 한다.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
를 캡쳐하는 방법도 있다.class SomeOtherClass { var x = 10 func doSomething() { someFunctionWithEscapingClosure { [self] in x = 100 } someFunctionWithNonescapingClosure { x = 200 } } }
Autoclosures
autoclosure는 함수에 인자로 넘겨지는 expression을 감싸기 위해 자동적으로 만들어지는 클로져를 말한다. autoclosure는 매개변수가 없으며, 호출하면 자신이 감싸고 있는 expression을 리턴한다. 다음 코드에서
customProvider
를 선언할 때에는 안의 메서드가 호출되지 않는다.customProvider
를 호출한 뒤 배열의 크기가 감소한 것을 확인할 수 있다.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 is ["Alex", "Ewa", "Barry", "Daniella"] func serve(customer customerProvider: () -> String) { print("Now serving \(customerProvider())!") } serve(customer: { customersInLine.remove(at: 0) } ) // Prints "Now serving Alex!"
escaping이 가능한 autoclosure를 사용하고자 한다면,
@autoclosure
키워드도 작성해야 한다. 이 키워드가 없으면{}
로 expression을 감싸서 함수를 호출해야 하므로 번거롭다.@autoclosure
키워드를 적어야 expression만 적어도 클로져로 인자 값이 설정된다.// 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!"
Reference
https://docs.swift.org/swift-book/LanguageGuide/Closures.html
'iOS > Swift 공식문서' 카테고리의 다른 글
Structures and Classes (0) 2022.02.17 Enumerations (0) 2022.02.16 Functions (0) 2022.02.15 Control Flow (0) 2022.02.15 Collection Types (0) 2022.02.14