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 |