[Swift 4.0] Closures

IOS/Swift 2017. 4. 1. 23:15

Introduction
: Swift의 Closure는 C와 Obj-C의 Block, 다른 language의 lambda와 비슷하다.


Closure Expressions
: Closure는 의미의 명확성을 해치지 않는 범위 내에서 축약되어 표현될 수 있다.
지금부터 예제 메서드인 sorted(by:)가 변화하는 과정을 살펴 보면서 어떤식으로 축약될 수 있는지 살펴 보겠다.

The Sorted Method
: Swift의 standard library는 sorted(by:)라는 메서드를 제공한다. 이 메서드는 메서드로 넘어오는Closure를 기반으로 array의 값들을 정렬한다.

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}

var reverseNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

알파벳 순서대로 내림차순 정렬된다.

Closure Expression Syntax
: Closure는 다음과 같은 형식을 지닌다.

{ ("parameters") -> "return type" in 
    statements
}

parameters는 in-out parameter가 될수 있고, variadic parameter로 지정해서 사용할 수도 있다. 또한 Tuple을 parameter와 return type으로 지정할 수 있다.

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
 })

Inferring Type From Context
: 위의 parameter로 넘겨진 closure의 경우 type을 명시하지 않더라도 추론할 수 있다. 왜냐하면 names array가 String type의 value를 다루기 때문에 closure의 type은 (String, String) -> Bool이 되어야 한다.
그래서 다음과 같이 생략해서 표현할 수 있다.

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })

이처럼 Closure는 다른 function이나 method로 넘겨질 때 항상 type 추론될 수 있기 때문에 위의 예처럼 축약형 표현이 가능하다.

Implicit Returns from Single-Expression Closures
: 위 closure는 'return' 키워드 또한 생략할 수 있다.

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

위 예제의 경우 Closure가 반드시 Bool 값을 return 해야 한다. 그리고 Closure의 body는 single expression( s1> s2)만 포함하고 있기 때문에 'return' 키워드를 생략할 수 있는것이다.

Shorthand Argument Names
: Swift의 Closure는 각 argument에 대해서 shorthand 버젼을 제공한다. argument 순서대로 $0, $1, $2 ... 형식으로 제공된다.
shorthand argument를 사용하면 argument의 정의를 생략할 수 있다.

reversedNames = names.sorted(by: { $0 > $1 } )

Operator Methods
: Swift의 String type은 '>' operator의 구현을 operator method로 제공하고 있다. 해당 operator method는 두개의 String type을 argument로 받고, Bool을 return 한다. 이는 sorted(by:)의 argument type과 동일하므로 다음과 같이 축약해서 사용가능하다.

reversedNames = name.sorted(by: >)

operator method는 'Advanced Operators' 챕터에서 배우도록 하겠다.


Trailing Closures
: 만약 어떤 function의 마지막 argument가 closure이고 그 closure의 내용이 상당히 길다면 Trailing Closure형태로 표현할 수 있다. 
예제를 보자.

func someFunctionThatTakesAClosure(closure: () -> void) 
{
    
}

someFunctionThatTakesAClosure(closure: {
    
})
// 일반적인 function 호출 방법이다.

someFunctionThatTakesAClosure() {

}
// trailing closure 표현법을 사용한 function 호출 방법이다.

위에서 예로 든 sorted(by:)를 trailing closure 표현법으로 호출해 보겠다.

reversedNames = names.sorted() { $0 > $1 }

() 도 생략할 수 있다.

reversedNames = names.sorted { $0 > $1 }


Capturing Values
: closure는 closure안에 있는 상수와 변수를 capture 한다. 그래야만 capture한 상수, 변수의 scope이 끝나서 없어지더라도 미리 capture 했기 때문에 closure 안에서는 계속 사용할 수 있는 것이다. 예제를 보자.

func makeIncrementer(forIncrement amount: Int) -> () -> Int 
{
    var runningTotal = 0

    func incrementer() -> Int
    {
        runningTotal += amount
    
        return runningTotal
    }

    return incrementer
}

위에 함수에서 incrementer closure(또는 Nested Function)는 makeIncrementer 에서 선언 및 argument로 넘어온 runningTotal, amount를 capture 한다. 그래서 makeIncrementer call이 끝나고 runningTotal, amount의 scope를 벗어나더라도 incrementer closure 안에서는 해당 변수 및 상수들을 계속 사용할 수 있는것이다.

let incrementByTen = makeIncrementer(forIncrement: 10)

incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

let incrementBySeven = makeIncrementer(forIncrement: 7)

incrementBySeven()
// returns a value of 7

incrementByTen()
// returns a value of 40

* 만약 closure를 class instance의 property로 할당하고, closure가 자신이 속한 class instance 또는 instance의 멤버 변수를 capture함으로써 자신이 속한 class instance를 캡쳐하고 있다면, strong reference cycle이 만들어 지게 된다.
이런 문제를 회피하는 방법은 'Automatic Reference Counting' 챕터에서 자세히 배울 것이다.


Closure Are Reference Types
: 위 예에서 incrementBySeven, incrementByTen은 let 으로 선언되어 constant 이지만, 이 constant들이 참조하고 있는 closure는 reference type 이기 때문에 캡쳐 하고 있는 변수인 runningTotal의 값을 증가시킬 수 있다. 이것은 function과 closure는 referenceType 이기 때문이다.


Escaping Closures
: 우리가 정의하는 많은 함수들이 비동기 operation을 수행한다. 그리고 block을 매개변수로 받아 비동기 operation이 완료되면 해당 block을 호출함으로써 비동기 operation이 완료되었다는 것을 호출자에게 알려준다. 이때 함수는 operation이 끝나기전에 반환 되지만, block은 operation이 끝날때까지 반환되면 안되기 때문에 Swift에서는 @escaping 라는 syntax를 정의했다. 즉, 함수의 범위를 벗어나서 함수가 반환 되더라도 나중에 호출될 수 있는 block 이라는 것을 가리키는 것이다.

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping() -> Void) {
    completionHandlers.append(completionHandler)
}

위 코드에서 @escaping 을 빼게되면 compile error가 발생한다. 

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"

위 코드에서 @escaping 선언되어 있는 block 에서는 self를 명시적으로 capturing 해야 한다.(개인적인 생각은 @escaping 되면서 self를 자동으로 인식할 수 있는 범위를 벗어나서 명시해줘야 하는것 같다.) self를 명시하지 않으면 compile error가 발생한다.


AutoClosures
: AutoClosure는 함수에 인자로 전달되는 표현식을 wrapping 하기 위해 자동 생성되는 Closure이다. 아무런 인자도 가지지 않고, 호출될때 내부에서 wrapping된 표현식의 값을 반환한다. 이 구문은 명시적인 Closure 대신에 일반 표현식으로 작성해서 함수의 매개변수 주변의 중괄호를 생략할 때 편리하다. 
이것을 이용해서 closure가 만들어질때 바로 실행하는 것이 아니라 closure가 호출될 때 실행되기 때문에 delaying evaluation이 가능하다.

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"

위 코드와 같이 autoClosure가 생성될 때 로직이 실행되는 것이 아니라, autoClosure가 호출될 때 실행된다. 

// 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가 호출될 때 로직이 실행된다.

@autoclosure syntax를 사용해서 parameter로 String Argument 인것 처럼 전달 하지만 자동으로 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!"

* @autuclosure syntax를 자주 남용하면 읽기 어려운 코드를 유발할 수 있다. 사용하더라도 네이밍을 적절히 하여 해당 argument가 autoclosure 인지 명확히 표시해야 한다. 

만약, autoClosure가 escape되야 한다면, @autoclosure와 @escaping을 같이 명시해야 한다. 

// 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!"


'IOS > Swift' 카테고리의 다른 글

[Swift 4.0] Classes and Structures  (0) 2017.04.01
[Swift 4.0] Enumerations  (0) 2017.04.01
[Swift 4.0] Functions  (0) 2017.04.01
[Swift 4.0] Control Flow  (0) 2017.04.01
[Swift 4.0] Collection Types  (0) 2017.02.03
Posted by 홍성곤
,