본문 바로가기
프로그래밍/Swift

[Swift] Closures (클로저)

by 별준 2021. 12. 22.

References

Contents

  • Closures
  • Closure Expressions
  • Trailing Closures
  • Capturing Values
  • Closures Are Reference Types
  • Escaping Closures
  • Autoclosures

클로저(Closures)가 꽤 익숙하지는 않아서, 다른 부분을 먼저 보고 공부를 해보려고 했습니다만.. 일단 그냥 진행해보기로 했습니다. 조금 내용이 부족할 수도 있습니다.. !

 

클로저는 Swift에서만 사용되는 개념은 아니고, 함수형 프로그래밍 언어에서 나타나는 중요한 특성이라고 합니다. Swift의 클로저는 C나 Objective-C의 블록과 유사하고, 다른 프로그래밍 언어의 람다(lambda)와 유사하다고 할 수 있습니다.

 

클로저는 정의된 context에서 모든 상수 및 변수에 대한 참조를 캡처(capture)해 저장할 수 있습니다. 이를 변수와 상수에 대한 closing over이라고 하며, Swift는 캡처와 관련된 모든 메모리를 알아서 처리합니다.

(캡처에 대한 개념은 아래에서 다시 설명하겠습니다.)

 

사실 이전 포스팅([Swift] Functions (함수))에서 이미 클로저에 대해서 알아봤습니다. 전역 함수(Global Function)이나 중첩 함수(Nested Function)이 바로 클로저의 특별한 케이스입니다.

클로저는 다음의 세 가지 형태 중에 하나를 갖습니다.

  • Global functions(전역 함수) : 이름(name)이 있고, 어떠한 값도 캡처하지 않는 클로저
  • Nested functions(중첩 함수) : 이름(name)이 있고, 이를 감싸는 함수의 값을 캡처할 수 있는 클로저
  • Closure expressions(클로저 표현) : 경량화된 문법으로 쓰여지고 주변 문맥(context)로부터 값을 캡처할 수 있는 이름이 없는 클로저

Swift의 클로저 표현은 일반적으로 간결하고 혼란이 없는 문법을 사용하도록 최적화되어 깔끔하고 명확한 스타일을 가지고 있습니다. 클로저의 최적화는 아래를 포함하고 있습니다.

  • 문맥(context)에서 파라미터와 리턴값의 타입 추론
  • single-expression 클로저에서 암시적 리턴
  • 축약된 인자(argument) 이름
  • Trailing(후위) 클로저 문법

 

그럼 지금부터 클로저에 대해 조금 더 자세히 알아보도록 하겠습니다.


Closure Expressions

이전 포스팅의 중첩 함수(Nested Functions)는 더 큰 함수의 일부로 코드의 블록을 명명하고 정의할 수 있는 편리한 수단입니다. 그러나 때때로는 선언이나 이름이 없는 짧은 버전의 함수를 작성하는 것이 유용할 수 있습니다. 예를 들면, 함수를 인수로 사용하는 함수 또는 메소드에서 유용할 수 있습니다.

 

Closure Expressions은 inline 클로저는 간략하고 명확하게 표현하는 문법에 초점이 맞춰져 있습니다. Closure Expression은 명확성이나 의도를 잃지 않고 축약된 형태로 클로저를 사용하기 위한 몇 가지 문법 최적화를 제공합니다. 아래에서 sorted(by:) 메소드를 사용하는 몇 가지 예제를 살펴보겠습니다.

 

The Sorted Method

Swift의 표준 라이브러리(standard library)는 배열 값을 정렬하는 sorted(by:)라는 메소드를 제공합니다. sorted 메소드에서 정렬이 완료되면 메소드는 정렬된 배열을 반환합니다. 원본 배열이 변경되지는 않습니다.

아래 예제에서 sorted(by:) 메소드를 사용하여 문자열 값의 배열을 어떻게 알파벳 역순으로 정렬하는지 알아보겠습니다.

정렬할 초기 배열은 다음과 같습니다.

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

sorted(by:) 메소드는 같은 타입(배열의 타입)의 두 개의 인수를 받는 클로저를 전달받는데, 이 클로저는 전달받은 첫 번째 인수 값이 두 번째 인수 값 앞에 위치하는지 뒤에 위치하는지 여부를 나타내는 Bool 값을 반환합니다. sorted의 by 인자에 전달되는 클로저는 첫 번째 값이 두 번째 값 앞에 위치하면 true를 반환하고 그렇지 않으면 false를 반환해야 합니다.

 

우리는 String 값들의 배열을 정렬해야하기 때문에 정렬 클로저의 타입은 (String, String) -> Bool이 되어야 합니다. 정렬 클로저를 전달하는 한 가지 방법은 올바른 타입의 일반 함수를 정의하여, 이 함수를 sorted(by:) 메소드의 인자로 전달하는 것입니다.

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"]

만약 첫 번째 문자열(s1)이 두 번째 문자열(s2)보다 크다면 backward(_:_:) 함수는 true를 반환하고, 이는 s2 앞에 s1이 위치해야 한다는 것을 의미합니다. 문자에서 '보다 크다'라는 것은 알파벳 순이 더 늦다라는 것을 의미합니다. 따라서, 'B'는 'A'보다 크고, 'Tom'은 'Tim'보다 크다고 할 수 있습니다. 따라서 위 예제에서는 'Barry'가 'Alex'보다 크기 때문에 알파벳 역순으로 정렬이 됩니다.

 

그러나 backward 함수는 single-expression 함수(a > b)를 길게 늘여쓴 방법에 불과합니다. 따라서, 이 예제에서는 closure expression 문법으로 정렬 클로저를 작성하는 것이 더 좋습니다.

 

Closure Expression Syntax

클로저 문법은 일반적으로 다음과 같은 형태로 사용됩니다.

위 문법에서 parameters는 in-out 파라미터가 될 수 있지만, 기본값을 가질 수는 없습니다. 가변 파라미터도 사용할 수 있고, 튜플 또한 파라미터 타입이나 리턴 타입이 될 수 있습니다.

 

아래 코드는 위에서 살펴본 backward(_:_:) 함수를 클로저 문법으로 작성한 버전입니다.

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

inline 클로저에 대한 파라미터와 리턴 타입의 선언은 backward(_:_:) 함수와 동일합니다. 두 경우 모두 (s1: String, s2: String) -> Bool의 타입을 갖습니다. 다만, inline 클로저의 경우 파라미터와 리턴 타입은 중괄호({}) 외부가 아닌 내부에 작성됩니다.

 

클로저 본문(body)의 in 키워드 다음부터 시작됩니다. 이 키워드는 클로저의 파라미터와 리턴 타입에 대한 정의가 완료된 후에 클로저의 본문이 시작된다는 것을 나타냅니다. 위의 inline 클로저는 본문이 매우 짧기 때문에 다음과 같이 한 줄로 작성할 수도 있습니다.

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

 

Inferring Type From Context

정렬 클로저는 (배열의)메소드에 인수가 전달되기 때문에 Swift는 파라미터의 타입과 리턴되는 값의 타입을 추론할 수 있습니다. sorted(by:) 메소드는 문자열 배열에서 호출되므로 해당 인수는 반드시 (String, String) -> Bool의 함수 타입이어야 합니다. 따라서, (String, String)과 Bool 타입이 클로저 정의의 일부로 작성할 필요가 없습니다. 모든 타입이 추론될 수 있으므로, return arrow(->)와 파라미터의 괄호를 생략할 수 있습니다.

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

함수나 메소드의 인수로 클로저를 전달할 때, 파라미터 타입과 리턴 타입을 추론할 수 있기 때문에 inline 클로저를 완전한 형태로 작성할 필요가 없습니다. 하지만, 원한다면 타입을 명시적으로 나타낼 수 있는데, 이는 코드를 읽는 사람의 혼란을 피할 수 있도록 해줍니다.

 

Implicit Returns from Single-Expression Closures

함수와 마찬가지로 Single-expression 클로저는 본문 내에서 return 키워드를 생략하고 단일 표현구문을 사용하여 그 구문의 결과를 암시적으로 반환할 수 있습니다. 따라서 sorted(by:)에 전달되는 클로저는 다음과 같이 작성될 수 있습니다.

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

명시적으로 return 키워드, 리턴 타입을 나타내지 않았지만, 클로저의 본문이 (s1 > s2)라는 단일 표현식만을 포함하고 있기 때문에 이 클로저는 Bool 값을 리턴합니다.

 

Shorthand Argument Names

Swift는 inline 클로저에 자동으로 부여되는 인수 이름을 제공하며, $0, $1, $2 등의 이름으로 클로저의 인수 값을 참조하는데 사용할 수 있습니다. 클로저 문법에서 이러한 인수 이름을 사용하는 경우, 클로저를 정의할 때 파라미터 리스트를 생략할 수 있습니다. 이러한 인수의 타입은 기대되는 함수 타입에서 추론되며, 가장 큰 숫자의 인수가 클로저가 전달받는 인수의 수를 결정합니다.

이런 경우에는 클로저가 본문으로만 구성되므로 in 키워드를 생략할 수도 있습니다.

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

이 클로저에서 $0과 $1은 클로저의 첫 번째와 두 번째 String 인자를 참조합니다. $1이 가장 높은 번호의 shorthand argument이므로, 이 클로저는 두 개의 인자를 전달받는 것으로 이해할 수 있습니다. 그리고 sorted(by:) 함수는 두 개의 문자열을 인수로 가지는 클로저라고 예상할 수 있으므로, $0과 $1은 모두 String 타입으로 추론됩니다.

 

Operator Methods

지금까지 살펴본 클로저 문법들보다 훨씬 더 짧게 사용할 수 있는 방법이 있습니다. Swift의 String 타입은 두 개의 String 타입의 파라미터를 전달받아 Bool 타입을 반환하는 greater-than operator (>) 메소드를 제공합니다. 이 메소드는 sorted(by:) 메소드에 필요한 타입과 일치합니다. 따라서 greater-than 연산자(>)를 sorted 메소드의 인자로 전달할 수 있습니다.

reversedNames = names.sorted(by: >)

 


Trailing Closures

함수의 마지막 인수로 클로저를 전달하는데, 이 클로저의 본문이 너무 길 경우 후행 클로저(Trailing Closure)로 작성하는 것이 유용합니다. 후행 클로저는 함수 호출 괄호()에 이어서 작성하면 됩니다. 

 

단일 클로저를 인자로 전달받는 someFunctionThatTakesAClosure라는 함수가 있다면, 

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
}

line 7처럼 파라미터 타입과 리턴 타입을 생략하여 작성할 수도 있고, line 13처럼 후위 클로저를 사용할 수도 있습니다.

 

문자열 정렬 메소드에 후행 클로저를 사용하려면 다음과 같이 작성할 수 있습니다.

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

만약 함수나 메소드에 클로저가 유일한 인수로 전달된다면, 후행 클로저를 사용하여 이 함수나 메소드를 호출할 때 괄호()를 작성할 필요가 없습니다.

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

 

후행 클로저는 클로저가 한 줄로 작성할 수 없을 정도로 길 때 유용합니다.

예를 들어, 배열 타입은 클로저를 단일 인자로 전달받는 map(_:) 메소드를 가지고 있는데, 이 메소드에 전달되는 클로저는 배열의 각 항목에 대해 한 번씩 호출되며, 해당 항목에 대해 매핑된 값(배열과 다른 타입도 가능)을 반환합니다. map(_:) 메소드는 원래 배열의 값과 같은 순서로 매핑된 새로운 값들을 포함하는 새로운 배열을 반환합니다.

 

아래 코드는 후행 클로저와 map(_:) 메소드를 사용하여 정수 값의 배열을 문자열 값으로 변환하는 예제입니다. 예를 들어, [16, 58, 510] 배열을 사용하여 ["OneSix", "FiveEight", "FiveOneZero"]의 새로운 배열을 생성합니다.

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]

먼저 정수와 문자열을 매핑하는 딕셔너리를 생성하고, 변환할 정수 타입의 배열을 생성합니다. 이제 map(_:) 메소드와 후행 클로저를 사용하여 정수 값의 배열인 numbers로 새로운 문자열 값들의 배열을 생성할 수 있습니다.

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"]

map(_:) 메소드는 배열의 각 항목 당 한 번씩 클로저를 호출합니다. 클로저의 입력 파라미터의 타입은 지정할 필요가 없는데, 이는 매핑되는 배열의 값에서 추론할 수 있기 때문입니다.

이 예제 코드에서 number 변수(클로저 본문)는 클로저의 number 파라미터의 값으로 초기화되고, 이 변수는 클로저 본문에서 수정됩니다 (함수와 클로저의 파라미터는 항상 상수입니다). 클로저는 String 타입을 리턴 타입으로 명시했는데, 이는 매핑되는 output 배열에서 저장되는 값을 나타냅니다.

클로저 본문 코드의 자세한 설명은 스킵하도록 하겠습니다 !

 

위의 예시 코드처럼 후행 클로저 문법은 map(_:) 메소드 내에서 완전한 클로저를 래핑할 필요없이 깔끔하게 작성할 수 있습니다.

 

만약 함수가 여러 개의 클로저를 사용하는 경우, 첫 번째 후행 클로저의 argument label은 생략하고, 남은 후행 클로저는 label을 지정합니다.

아래 예제 코드를 살펴보겠습니다. loadPicture 함수는 갤러리에 사용할 사진을 로드합니다. 

func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}

이 함수를 호출할 때 두 개의 클로저를 전달해야하는데, 첫 번째 클로저는 성공적으로 다운로드한 후 사진을 표시하는 completion handler이고, 두 번째 클로저는 사용자에게 에러를 표시하는 error handler입니다.

loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}

이 예제에서, loadPicture(from:completion:onFailure:)는 다운로드 작업을 수행하고 완료되면 두 개의 handler 중에 하나를 호출합니다. 위처럼 코드를 작성하면 다운로드 완료 후 사용자 인터페이스를 업데이트하는 코드와 에러를 처리하는 코드를 깔끔하게 분리할 수 있습니다.

 


Capturing Values

클로저는 주변 context로부터 상수와 변수를 캡처할 수 있습니다. 캡처하게 되면 캡처한 상수와 변수를 정의한 원래 scope가 더 이상 존재하지 않더라도 해당 상수 및 변수의 값을 본문 내부에서 참조하고 수정할 수 있습니다.

(scope는 global, local 변수의 범위를 생각하시면 쉽게 이해가 될 것 입니다.)

 

Swift에서 값을 캡처할 수 있는 가장 간단한 형태의 클로저는 다른 함수의 본문 내에서 작성된 중첩 함수(nested function)입니다. 중첩 함수는 중첩 함수 외부의 인수를 캡처할 수 있으며 외부에 정의된 모든 상수와 변수를 캡처할 수도 있습니다.

 

아래에 makeIncrementer라고 불리는 예제 함수가 있습니다. 이 함수는 내부에 incrementer라는 중첩 함수를 포함하고 있습니다. 중첩된 incrementer() 함수는 runningTotal과 amount라는 두 개의 값을 주변 context로 부터 캡처합니다. 이 값을 캡처한 이후에 incrementer는 makeIncrementer에 의해 리턴되고, 리턴된 클로저는 호출될 때마다 amount만큼 runningTotal을 증가시킵니다.

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

makeIncrementer의 리턴 타입은 () -> Int 입니다. 이는 함수를 리턴한다는 것을 의미합니다. makeIncrementer가 반환하는 함수는 파라미터가 없고, 호출될 때마다 Int 값을 반환합니다.

아래는 amount의 값을 3으로 설정하고 수행한 결과입니다.

 

makeIncrementer(forIncrement:) 함수는 runningTotal이라는 정수 타입의 변수를 정의하고, 이 변수는 반환되는 incrementer 함수의 현재 합계를 저장합니다. 이 변수는 0으로 초기화됩니다. makeIncrementer는 하나의 Int 파라미터는 forIncrement라는 이름의 인자로 가집니다. 이 인자는 runningTotal이 반환된 incrementer 함수가 호출될 때마다 runningTotal을 얼마나 증가시킬지를 결정합니다. 

makeIncrementer 함수는 내부에서 incrementer라는 중첩된 함수를 정의하는데, 이 함수는 간단히 amount를 runningTotal에 더하고, 이 값을 리턴합니다.

 

만약 incrementer()만 따로 놓고 본다면, 아래 코드는 정상적이지는 않습니다.

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

incrementer() 함수는 아무런 파라미터도 없는데, 여전히 runningTotal과 amount를 함수 내부에서 참조하고 있습니다. 이는 함수 주변으로부터 runningTotal과 amount의 참조를 캡처함으로써 이들을 incrementer 내부에서 사용할 수 있습니다. 참조로 캡처하면, makeIncrementer의 호출이 종료되어도 runningTotal과 amount가 사라지지 않고, 다음번에 incrementer 함수를 호출할 때 runningTotal과 amount를 사용할 수 있도록 해줍니다.

 

이번에는 amount 3이 아닌 10으로 설정하여 반환되는 함수를 incrementByTen 상수에 저장하여 사용합니다.

let incrementByTen = makeIncrementer(forIncrement: 10)

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

두 번째 incrementer를 생성해보겠습니다.

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

결과 값을 살펴보면 새로운 runningTotal 변수가 생성되어 각 incrementer 함수는 자신만의 runningTotal을 가지고 있다는 것을 볼 수 있습니다. 따라서 incrementByTen을 호출하여 runningTotal 변수의 값을 증가시켜도, incrementBySeven에 의해 캡처된 변수에는 영향을 미치지 않습니다.

 


Closures Are Reference Types

방금 전의 예제에서 incrementBySeven과 incrementByTen는 상수이지만, 캡처된 runningTotal 변수는 증가시킬 수 있었습니다. 이는 함수와 클로저가 reference types이기 때문입니다.

함수나 클로저를 상수나 변수에 할당하는 것은 실제로는 해당 상수 또는 변수를 함수나 클로저의 대한 참조(reference)로 설정하는 것을 의미합니다.

그래서 만약 한 클로저를 두 상수나 변수에 할당한다면, 두 상수나 변수는 같은 클로저를 참조하고 있습니다. C나 C++에 익숙하다면 함수 포인터를 저장한다고 생각하면 이해하기 쉽습니다.

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

incrementByTen()
// returns a value of 60

앞에서 사용한 incrementByTen 클로저를 상수에 할당하고 실행하면 사용한 클로저의 마지막 상태에서 10을 증가시키므로 50을 반환하게 됩니다.

 


Escaping Closures

함수의 파라미터로 전달되어 사용되지만 그 함수가 리턴된 이후에 호출되는 클로저를 Escaping Closure라고 합니다.

이러한 클로저는 함수를 선언할 때 파라미터 타입 앞에 @escaping을 작성하여 해당 클로저가 escape를 할 수 있음을 나타내야 합니다.

 

클로저가 escape할 수 있는 한 가지 방법은 함수 외부에 정의된 변수에 저장되는 것입니다. 

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

위 코드에서 @escaping은 함수의 파라미터로 전달받은 클로저를 함수 외부 변수에 저장하여 외부에 사용하겠다는 것을 의미합니다. 만약 @escaping이 없다면 함수가 리턴된 이후에는 사용할 수 없다는 것을 의미하므로, 배열에 할당할 수 없습니다. @escaping 키워드를 붙이지 않는다면 컴파일 에러가 발생합니다.

 

다음 예제 코드를 살펴보겠습니다.

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }
        someFunctionWithEscapingClosure { self.x = 100 }
    }
}

SomeClass라는 클래스를 하나 정의하고, doSomething이라는 메소드를 정의했습니다. doSomething 메소드는 someFunctionWithNonescapingClosure와 someFunctionWithEscapingClosure를 순차적으로 호출하고 종료됩니다.

 

이제 다음의 코드를 실행해보겠습니다. 먼저 SomeClass의 인스턴스를 생성하여 instance라는 상수에 할당하고, 인스턴스의 doSomething 메소드를 실행합니다. 그리고 인스턴스의 x의 값을 확인해보면, 그 값은 200입니다. 

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"

completionHandlers.first?()
print(instance.x)
// Prints "100"

someFunctionWithEscapingClosure는 doSomething 함수의 외부에서 사용되도록 Escaping 클로저를 인자로 전달받고, 외부 변수에 저장합니다. 따라서 doSomething 함수 내부에서 x = 200만이 수행되어 인스턴스의 x값이 200으로 변경된 것을 확인할 수 있습니다.

그리고 외부 변수 completionHandlers에 저장된 클로저를 실행하면 인스턴스의 x값이 100으로 변경되는 것을 확인할 수 있습니다.

 

여기서 someFunctionWithEscapingClosure에 전달되는 클로저 본문에 self.x = 100 이라고 작성되었습니다.

일반적으로 클로저는 암시적으로 변수들을 캡처하면 클로저 본문에서 사용하지만, 위와 같이 escaping 클로저에는 명시적으로 해주어야합니다. 위 코드에서는 someFunctionWithEscapingClosure(_:) 함수에 전달된 클로저가 self를 명시적으로 참조하고 있습니다. 반면, someFunctionWithNonescapingClosure(_:)에 전달되는 클로저는 non-escaping 클로저이므로 암시적으로 self를 참조할 수 있습니다.

(C++의 람다 함수에서 캡처하는 것을 생각하시면 조금 이해가 빠를 것 같습니다.)

 

다음은 클로저의 캡처 리스트에 포함하여 self를 캡처하는 doSomething() 메소드 예제 코드입니다.

class SomeOtherClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { [self] in x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

 

만약 self가 구조체(structure) 또는 열거형(enumeration)의 인스턴스인 경우, 항상 self를 암시적으로 참조할 수 있습니다. 그러나 escaping 클로저가 변경 가능한 참조로 self를 캡처할 수는 없습니다(self가 구조체 또는 열거형의 인스턴스인 경우).

struct SomeStruct {
    var x = 10
    mutating func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }  // Ok
        someFunctionWithEscapingClosure { x = 100 }     // Error
    }
}

위 예제 코드에서 someFunctionWithEscapingClosure 함수의 호출은 에러입니다. 이는 클로저 내부에서 self의 값을 변경하려고 하기 때문입니다.

 


Autoclosures

오토클로저는 함수의 인자로 전달된 표현식을 래핑하여 자동으로 생성되는 클로저입니다. 오토클로저는 어떠한 인자도 필요없으며, 오토클로저가 호출되면 오토클로저 내부에 래핑된 표현식의 값을 리턴합니다.

 

오토클로저를 사용하면 내부 코드가 클로저를 호출할 때까지 실행되지 않으므로 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"

 

customersInLine 배열의 첫 번째 요소가 클로저 내부에서 삭제되지만, 배열의 요소는 클로저가 실제로 호출되기 전까지 제거되지 않습니다. 만약 클로저가 절대로 호출되지 않는다면, 클로저의 내부 표현식은 수행되지 않으며, 이는 배열 요소가 제거되지 않는다는 것을 의미합니다. 여기서 customerProvider는 () -> String 타입인 함수이며 파라미터가 없고 문자열값을 리턴하는 클로저입니다.

 

함수 인자로 클로저를 전달해도 동일한 동작을 수행할 수 있습니다.

// 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(customer:) 함수는 customer의 이름을 리턴하는 클로저를 파라미터로 전달받습니다.

아래의 server(customer:) 함수도 위와 동일한 동작을 수행하지만, 명시적인 클로저를 전달받는 것이 아니라 @autoclosure 속성이 붙은 오토클로저를 파라미터로 전달받습니다. 이제 클로저 대신 문자열 인자를 받는 함수처럼 호출할 수 있습니다. 전달된 인자는 자동으로 클로저로 변환되는데, 이는 customerProvider 파라미터 타입에 @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!"

 

만약 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!"

위 코드에서 customerProvider 인자로 전달된 클로저를 호출하는 대신, collectCustomerProverders(_:) 함수는 클로저를 customerProverders 배열에 추가합니다. 이 배열은 함수 밖에서 선언되어 있으므로, 배열의 클로저가 함수의 반환 이후에 실행될 수 있다는 것을 의미합니다. 따라서 customerProvider 인자의 값은 함수 범위를 escape할 수 있어야 합니다.

 

 

댓글