티스토리 뷰

Swift 공식 가이드/Swift 2

Closures

찜토끼 2016. 3. 12. 19:14

Swift 3.0.1 가이드에 대응하는 정리글을 작성하였습니다!!!

Closures 정리 최신버전 > http://wlaxhrl.tistory.com/40




Apple 제공 Swift 프로그래밍 가이드(2.1)의 Closures 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.


들어가며

클로저는 코드 내에서 전달되거나 사용될 수 있는 기능을 독립적으로 포함하는(self-contained) 블록이다. C와 Objective-C에서의 블록과 유사하다.

클로저는 자신이 정의된 콘텍스트에 존재하는 상수/변수의 참조를 캡쳐해서 저장할 수 있다. 즉 클로저는 자신이 정의되어 있는 곳의 scope에 속하는 상수/변수의 참조에 접근할 수 있다. Swift는 이 과정에서 메모리 관리를 알아서 관리해준다.

<Note> 캡쳐라는 개념을 모르더라도 걱정하지 마시오. 나중에 자세히 다룹니다. 참고로 이 글에서는 가이드에 나온 그대로 캡쳐라고 표기를 했습니다.


사실 global function, nested function은 클로저의 특수한 케이스에 속한다. 클로저는 다음 세 개의 유형 중 하나다.

  • Global function : 이름이 있고, 아무 값도 캡쳐하지 않는 클로저

  • Nested function : 이름이 있고, 자신을 감싸는 함수(enclosing function)에서 값을 캡쳐할 수 있는 클로저

  • Closure expression : 이름이 없고, 자신을 둘러싼 컨텍스트에서 값을 캡쳐할 수 있는, 가벼운 문법으로 작성된 클로저


Swift의 closure expression 은 일반적인 경우에 대해 최적화 되어있는 문법을 제공한다.

  • 파라미터와 리턴 타입을 컨텍스트에서 유추

  • single-expression closure 에서의 명확한 리턴

  • Shorthand argument names

  • 클로저 문법 추적



Closure Expressions

Nested function 은 함수 내부에 다른 함수(코드 블록)를 포함시키고 이름붙일 수 있는 유용한 수단이다. 그러나 경우에 따라 Nested Function 을 정의하기보다는 이와 비슷한 역할을 할 수 있는 함수 비슷한 것을 짧고 간단하게 정의하는 것이 더 유용할 수도 있다. (예를 들면 함수 타입의 파라미터를 여러 개 받는 함수의 경우) 이럴 경우 쓰이는 것이 Closure expression 이다. 이것이 인라인 클로저를 빠르게 작성할 수 있는 방법이다.

그럼 지금부터 sort 메서드의 예제를 통해 Closure expression 의 몇 가지 최적화 문법을 살펴보자.


<The Sort Method>

Swift의 standard library 가 제공하는 sort 메서드는 특정 타입의 값들이 들어있는 배열을 정렬해준다. 이때의 정렬기준은 메서드가 argument로 받을 sorting closure에 따른다. 이 클로저의 역할은 -> 정렬할 배열에 들어있는 값과 같은 타입을 가지는 argument 두 개를 받아서, 첫 번째 값이 먼저 나와야 하면 true 아니면 false를 리턴하는 클로저이다.

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

func backwards(s1: String, _ s2: String) -> Bool {
    return s1 < s2
}
var reversed = names.sort(backwards)
// reversed is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

위의 예제에서는 sorting closure 를 위해 함수를 하나 정의하고 있다. 그러나 단순히 a < b 를 알려주기 위해 이같이 함수를 만드는 것보다 더 좋은 방법이 있다. Closure expression syntax 를 사용하는 것이다.


<Closure Expression Syntax>

Closure Expression Syntax 의 일반적인 형태다.

상수/변수/tuple/inout 파라미터를 사용할 수 있으며 variadic 파라미터(가변적인 파라미터)도 이름을 붙인다면 사용할 수 있다. 디폴트 값은 허용되지 않는다.

reversed = names.sort({ (s1: String, s2: String) -> Bool in
    return s1 > s2
})

위 예제코드의 인라인 클로저와 아까 보았던 backwards 함수는 파라미터 타입과 리턴 타입이 (String, String) -> Bool 로 동일하다. 그러나 인라인 클로저는 파라미터와 리턴 타입의 정의가 대괄호 안에 들어있으며, in 이라는 키워드로 클로저의 내용이 시작된다.

reversed = names.sort( { (s1: String, s2: String) -> Bool in return s1 > s2 } )

클로저 내용이 짧으니까 이렇게 한 줄에 쓸 수도 있다.


<Inferring Type From Context>

sort 메서드의 경우, 정렬할 배열에 어떤 타입의 값이 저장되어 있는지 알 수 있으므로, 굳이 sorting closure에서 (String, String) -> Bool 이라는 타입을 일일히 명시하지 않아도 컨텍스트에서 유추할 수 있다. 따라서 이런 타입 명시는 다음과 같이 생략할 수 있다.

reversed = names.sort( { s1, s2 in return s1 < s2 } )

이렇듯 앞으로 인라인 클로저를 함수에 전달할 경우에는 파라미터 타입과 리턴 타입을 명시할 일이 거의 없을 것이다. (당연히 하고 싶으면 해도 된다)


<Implicit Returns from Single-Expression Closures>

예제의 경우 sorting closure가 Bool 타입을 리턴해야 하는 것은 명확하다. 또한 s1 < s2 가 Bool 을 리턴하는 것도 명확하다. 고로 return 키워드도 생략할 수 있다.

reversed = names.sort( { s1, s2 in s1 > s2 } )


<Shorthand Argument Names>

클로저의 argument 들을 순서대로 $0, $1, ... 로 표현할 수 있다. 이 경우 파라미터에 이름을 붙일 필요가 없으므로 in 키워드도 생략 가능하다.

reversed = names.sort( { $0 > $1 } )


<Operator Functions>

사실 여기서 Operator 함수를 쓰면 더 줄일 수가 있다. Swift에서 String 타입은 operator > 를 함수로 정의하고 있다.이것은 Operator 함수에 속하는데, String 타입을 두 개 받아서 Bool 타입의 값을 리턴하는 함수이다. 따라서 다음과 같이 축약할 수 있다.

reversed = names.sort(>)

Operator 함수에 대해서는 나중에 자세히 다룹니다.



Trailing Closures

closure expression 을 함수의 마지막 argument 로 넘기는 경우 trailing closure 를 쓰는 것이 유용할 수 있다. trailing 클로저는 함수 호출 시 괄호의 바깥에 쓰는 closure expression이다.

func someFunctionThatTakesAClosure(closure: () -> Void) {
    // function body goes here
}
 
// trailing closure 를 안쓰고 최대한 줄이면:
someFunctionThatTakesAClosure({
    // closure's body goes here
})
 
// trailing closure 사용:
someFunctionThatTakesAClosure() {
    // trailing closure's body goes here
}

아까 봤던 sorting closure의 예를 들어보면 이렇게 할 수 있다.

reversed = names.sort() { $0 > $1 }
// 오직 trailing closure 만 argument로 전달되는 경우에는 괄호도 생략가능
reversed = names.sort { $0 > $1 }

trailing closure 은 closure expression이 한 줄에 끝나지 않고 길어지는 경우에 특히 효과적이다. 예를 들어 배열의 map 메서드는 argument로 클로저를 하나 받는데 이 클로저는 내용이 길어지는 경우가 많다. (* 배열의 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 {
    (var number) -> String in
    var output = ""
    while number > 0 {
        output = digitNames[number % 10]! + output
        number /= 10
    }
    return output
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]



Capturing Values

 클로저는 자신을 둘러싼 컨텍스트에서 상수/변수를 캡쳐할 수 있다. 이렇게 캡쳐한 상수/변수들이 원래 정의되어 있던 scope가 더이상 존재하지 않을 때에도, 클로저는 그것들의 값을 참조하거나 변경할 수 있다.

값을 캡쳐하는 클로저를 설명하기 위해 nested function 을 예로 들어 설명하겠다. nested function 은 자신이 정의되어 있는 function(outer function)의 상수/변수/파라미터를 캡쳐할 수 있다.

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

incrementer 는 runningTotal과 amount의 참조를 캡쳐해놓는다. incrmenter의 실행이 끝나도 계속 값들이 유지가 되어야 하며, 또 다시 불렸을 때에도 runningTotal의 값을 변경할 수 있어야 하기 때문이다.

<note> Swift는 최적화를 위해 클로저의 안밖에서 수정되지 않을 값을 -> 참조가 아니라 값복사를 해서 캡쳐하고 저장한다. 따라서 위의 예제에서 amount는 참조캡쳐가 아니라 값캡쳐가 되었을 것이다. 또한 Swift는 클로저에서 캡쳐한 값이 더이상 필요하지 않을 때에는 알아서 메모리 관리를 해준다.

let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30


// 새로운 incrementer 를 만들경우
// 이전의 incrementer가 캡쳐했던 runningTotal 과 독립적인 runningTotal 의 참조를 캡쳐하게 된다.
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7
incrementByTen()
// returns a value of 40

<Note> 만약 클래스 인스턴스의 프로퍼티로 클로저를 지정한다면, 그리고 클로저가 인스턴스 혹은 인스턴스의 멤버변수를 캡쳐한다면, 강한 참조 싸이클(strong reference cycle)을 만들 수 있다. 이것을 깨기 위해 Swift는 capture list 를 사용한다. 자세한 것은 나중에 다룹니다.


Closures Are Reference Types

위 예제코드에서 incrementBySeven 와 incrementByTen 은 상수이다. 그러나 각 클로저가 캡쳐해둔 runningTotal 변수는 호출 시마다 값이 변하고 있다. 이것은 함수와 클로저가 reference type 이기 때문이다. (incrementBySeven 과 incrementByTen는 상수이므로 가리키는 대상을 바꿀 수는 없다. 그러나 그 대상(=클로저) 자체는 상태가 바뀔 수 있다는 뜻이다)

함수/클로저를 상수/변수에 할당할 때, 그 상수/변수에는 함수/클로저를 가리키는 참조가 할당된다. 예제로 생각을 해보자. 참조이기 때문에, incrementByTen 을 다른 상수/변수에 할당하게 되면 두 개는 같은 클로저를 가리키게 된다.

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50



Nonescaping Closures

클로저를 파라미터로 받는 함수를 정의할 때 @noescape 키워드를 파라미터 이름에 붙이면 클로저는 그 함수를 빠져나갈 수 없게된다. 즉 해당 함수 안에서만 실행되고 함수가 종료되는 시점에 같이 소멸되는 것이다.

@noescape 키워드를 붙이면 컴파일러는 그 클로저의 생성/소멸주기를 더 잘 알게 되기 때문에 최적화된 관리를 해준다.

<참고> @noescape 를 붙이면 허락되지 않는 것들

func someFunctionWithNoescapeClosure(@noescape closure: () -> Void) {
    closure()
   // 파라미터로 받은 클로저가 이 함수에서만 쓰이는게 확실할 때 쓰면 좋다
}

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


class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNoescapeClosure { x = 200 } // self 안써도 됨
    }
}
 
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// prints "200"
 
completionHandlers.first?()
print(instance.x)
// prints "100"

위 예제에서 보이는 것처럼 @noescape 클로저는 어차피 해당 함수 안에서만 실행되기 때문에 self 를 명시할 필요가 없다.



Autoclosures

오토클로저는 함수 argument로 넘어온 expression 을 자동으로 감싸서 만들어주는 클로저다. @autoclosure 키워드를 파라미터에 붙이게되면 함수 argument로 클로저 형태가 아닌 일반 expression을 넘겨도 자동으로 클로저로 만들어준다.

오토클로저가 쓰이는 예를 하나 들어보면 Assert가 있다.

// 정의를 보면 condition 은 사실 클로저이다
public func assert(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = default, file: StaticString = default, line: UInt = default)

// 그러나 밖에서 쓸때는 treu/false만 셋팅하고 있다
assert(false, "")

오토클로저는 delay evaluation 이 가능하게 해준다. 클로저를 호출할 때까지 expression 수행이 시작되지 않기 때문이다. 예를 들어 첫번째 파라미터로 오토클로저를 받는 어떤 함수가 있다고 해보자. someFunction(expensiveComputationForRemoveArrayItem, "") 라고 한다면 expensiveComputationForRemoveArrayItem 은 자동으로 클로저로 바뀌게 되고, 클로저가 실행되기 전까지는 expensiveComputationForRemoveArrayItem도 수행이 되지 않는다. someFunction 안에서 클로저가 호출되기 전까지는 배열의 아이템이 삭제되지 않는 것이다.

@autoclosure 지정은 @noescape 지정을 포함하고 있다고 보면 된다. 오토클로저는 자연히 noescape가 적용된다. 오토클로저이면서 escape 를 시키고 싶다면 @autoclosure(escaping) 라고 명시해주면 된다.


'Swift 공식 가이드 > Swift 2' 카테고리의 다른 글

Classes and Structures  (0) 2016.03.27
Enumerations  (0) 2016.03.15
Functions  (0) 2016.03.12
Control Flow  (0) 2016.03.03
Collection Types - Dictionary  (0) 2016.03.01
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함