Swift/Swift 가이드

[Swift] 공식 문서 - 클로저 (Closures)

유정주 2021. 12. 30. 00:10
반응형

안녕하세요. 개발하는 정주입니다.

 

오늘은 클로저 (Closures)를 정리해보겠습니다.

 

* 공식 문서 내용을 한 줄 한 줄 읽는 것에 의의를 두었습니다.

* 파파고의 힘을 빌려 번역했으며 잘못된 번역이 있다면 댓글로 알려주세요.

 


새로 배운 점

  • 전역 함수와 중첩 함수가 클로저라는 사실
  • sorted 메서드가 클로저를 기반으로 동작된다는 사실
  • 클로저의 인자, 반환값 생략할 수 있는 이유가 Swift의 타입 유추라는 점
    => 생략 가능하다는 것은 알고 있었지만 이번 포스팅을 통해 이유를 알게 되었다.
  • ">" 연산자 메서드에 문자열별 구현이 되어 있어 sorted()에 클로저 대신 쓸 수 있다는 점
  • 클로저는 참조 타입이라는 점
  • 값이 캡처가 되면 클로저와 인스턴스 사이에 강한 참조 사이클이 생성된다는 점
  • 캡처로 생성된 강한 참조 사이클을 캡처 목록을 사용하여 관리한다는 점
  • 이스케이프 클로저에 대한 내용
  • 자동 클로저에 대한 내용

목차


    서론

    클로저는 코드에서 함수적인 것을 독립적으로 사용할 수 있는 코드(self-contained blocks of functionality)입니다.

    Swift의 클로저는 C와 Objective-C의 블럭과 다른 프로그래밍 언어의 람다와 유사합니다.

     

    클로저는 Context에 정의된 모든 상수와 변수를 캡처(capture)와 참조 저장(store references) 할수 있습니다. 

    이것은 이러한 상수와 변수를 클로징(closing over)합니다. 

    Swift는 캡처(capturing)하는 모든 메모리를 관리합니다. 캡처에 대해서는 아래 나옵니다.

     

    Functions(2021.12.28 - [Swift/공식 문서 정리] - [Swift] 공식 문서 - 함수 (Functions))에서 소개된, 전역 함수와 중첩된 함수들은 실제로는 클로져의 특별한 경우(case)입니다.

    클로져는 다음 3가지 중의 하나입니다.

    • 전역 함수는 이름을 가지고 어떠한 값도 캡처하지 않는 클로저입니다.
    • 중첩 함수는 이름을 가지고 둘러싼 함수로 부터 값을 캡처할 수 있는 클로저입니다.
    • 클로저 표현식은 이름이 없고 주변 컨텍스트에서 값을 캡처할 수 있는 경량 구문으로 작성된 클로저입니다.

     

    Swift의 클로저 표현식은 일반적인 상황에서 간결(brief)하며, 혼란스럽지 않도록(clutter-free) 최적화해주는 깔끔하고 명확한 스타일입니다.

    이러한 최적화에는 다음과 같습니다.

    • 컨텍스트에서 파라미터와 반환값 타입 유추
    • 단일 표현식 클로저의 암시적 반환
    • 약식 인자 이름
    • 후행 클로저 구문

     


    클로저 표현식 (Closure Expressions)

    중첩 함수(Nested Functions)는 큰 함수의 일부를 독립적인 코드 블럭으로 이름을 지정하고 정의하기 편한 방법입니다.

    그러나 때로는 전체 선언과 이름이 없는 구조체처럼 더 짧은 버전의 함수를 작성하는 것이 유용할 수 있습니다.

    이것은 함수를 하나 이상의 인수로 사용하는 함수 또는 메서드로 작업할 때 특히 유용합니다.

     

    클로저 표현식(Closure expressions)은 문법에 집중해서 간단하게 인라인 클로저를 작성하는 방법입니다.

    클로저 표현식은 명확성이나 의도를 잃지 않고 짧은 형태로 클로저를 쓰기 위한 몇 가지 최적화 문법을 제공합니다.

     

    아래의 클로저 표현식 예제는 여러 반복에 걸쳐 sorted(by:) 메서드의 단일 예제를 구체화하는 최적화를 나타냅니다. 각 예제는 동일한 기능을 보다 간결한 방식으로 표현합니다.

     

    정렬 메서드 (The Sorted Method)

    Swift의 표준 라이브러리는 사용자가 제공하는 정렬 클로저의 출력을 기반으로 값 배열을 정렬하는 sorted(by:) 라는 메서드를 제공합니다. sorted(by:) 메서드는 원본 배열과 타입과 크기가 같고 올바르게 정렬된 새로운 배열로 반환합니다. 기존 배열은 sorted(by:) 메서드로 수정되지 않습니다.

     

    아래 예제의 클로저 표현식은 알파벳 역순으로 String 값의 배열을 정렬하기 위해 sorted(by:) 메서드를 사용합니다. 다음은 원본 배열입니다.

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

     

    sorted(by:) 메서드는 배열과 동일한 타입의 두 인자를 사용하는 클로저를 받습니다.

    정렬 클로저는 첫 번째 인자 값이 두 번째 값 이전이면 true를 반환하고 이후면 false를 반환합니다.

     

    이 예시는 문자열 값의 배열을 정렬하는 것이므로 정렬 클로저는 (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로 반환되어 정렬된 배열에서 s1이 s2보다 먼저 나타나야 함을 나타냅니다.

     

    문자열에서 "더 크다"는 "알파벳순으로 더 뒤에 나타난다"를 의미합니다.

    문자 B 는 문자 A 보다 "더 크다"이고 문자열 "Tom" 은 문자열 "Tim" 보다 더 큽니다.

    알파벳 역순으로 정렬될 때 "Barry" 는 "Alex" 보다 앞에 위치합니다.

     

    그러나 이것은 단일 표현식 함수 (a > b)를 작성하는 다소 긴 방식입니다.

    이 예제에서는 클로저 표현식 구문을 사용하여 정렬 클로저를 인라인으로 작성하는 것이 좋습니다.

     

    클로저 표현구 (Closure Expression Syntax)

    클로저 표현구는 아래와 같은 형태를 가지고 있습니다.

    { (parameters) -> return type in
        statements
    }

    클로저 표현구의 파라미터 는 in-out 파라미터 일 수 있지만 기본값을 가질 수 없습니다

    가변 파라미터의 이름을 지정하면 가변 파라미터를 사용할 수 있습니다

    튜플은 파라미터 타입과 반환 타입으로 사용될 수도 있습니다.

     

    아래 예제는 위의 backward(_:_:) 함수의 클로저 표현 버전입니다.

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

    이 인라인 클로저의 파라미터와 반환 타입의 선언은 backward(_:_:) 함수에서 선언한 것과 동일하며 모두 (s1: String, s2: String) -> Bool 로 작성합니다. 

    그러나 인라인 클로저 표현식을 위한 파라미터와 반환 타입은 중괄호 바깥이 아닌 안에 작성합니다.

     

    클로저의 바디의 시작은 in 키워드로 시작합니다. 이 키워드는 클로저의 파라미터와 리턴 타입 정의가 끝남을 나타내며 클로저의 바디가 시작함을 나타냅니다.

     

    클로저의 바디가 짧을 떄는 한줄로 작성할 수 있습니다.

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

    이것은 sorted(by:) 메서드에 대한 전체 호출이 동일하게 유지되었음을 보여줍니다.

    소괄호는 여전히 메서드의 전체 인자를 둘러싸고 있지만 인자는 인라인 클로저가 됐습니다.

     

    컨텍스트로 타입 유추 (Inferring Type From Context)

    정렬 클로저는 메서드에 인자로 전달되므로 Swift는 파라미터 타입과 반환되는 값의 타입을 유추할 수 있습니다.

    sorted(by:) 메서드는 문자열 배열에서 호출되므로 인자는 (String, String) -> Bool 타입의 함수이어야 합니다.

     

    이는 (String, String) 과 Bool 타입을 클로저 표현식 정의에 굳이 작성할 필요가 없음을 의미합니다.

    모든 타입은 유추할 수 있기 때문에 반환 화살표 (->)와 파라미터의 이름을 둘러싼 소괄호를 생략할 수 있습니다.

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

     

    함수나 메서드에 인라인 클로저 표현식으로 클로저를 전달할 때 항상 파라미터 타입과 반환 타입을 유추할 수 있습니다. 결과적으로 클로저가 함수 또는 메서드 인자로 사용될 때 완전한 형태로 인라인 클로저를 작성할 필요가 없습니다.

     

    원하는 경우 코드의 모호성을 줄이고 싶다면 타입을 명시적으로 적어도 됩니다.

     

    단일 표현 클로저의 암시적 반환 (Implicit Returns from Single-Expression Closures)

    단일 표현 클로저(Single-expression closures)는 return 키워드를 생략하여 암시적으로 값을 반환할 수 있습니다.

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

    여기서 sorted(by:) 메서드의 인자의 함수 타입은 명확합니다. 클로저에서 Bool 값이 반환되기 때문입니다.

    클로저의 바디에 Bool 값을 반환하는 단일 표현식 (s1 > s2)가 포함되어 모호하지 않으므로 return 키워드를 생략할 수 있습니다.

     

    짧은 인자 이름 (Shorthand Argument Names)

    Swift는 인라인 클로저에 자동적으로 클로저의 인자값을 참조하는 $0, $1, $2 등 짧은 인자 이름 (shorthand argument names)을 제공합니다.

     

    클로저 표현식에 이런 짧은 인자 이름을 사용한다면 선언에 클로저의 인자 목록을 생략할 수 있고 짧은 인자 이름의 수와 타입은 함수 타입에서 유추됩니다. 클로저 표현식이 전체 바디로 구성되기 때문에 in 키워드를 생략할 수도 있습니다.

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

    여기서 $0 와 $1 은 클로저의 첫번째와 두번째 String 인자를 참조합니다. $1 이 짧은 인자에서 가장 높은 숫자이므로 클로저는 2개의 인자가 있다고 이해합니다. 여기서 sorted(by:) 함수는 인자가 모두 문자열인 클로저만 허용하므로 짧은 인자 $0 과 $1 은 모두 타입 String 입니다.

     

    연산자 메서드 (Operator Methods)

    실제로 위의 클로저 표현식을 더 짧게 작성하는 방법이 있습니다. 

     

    Swift의 String 타입은 보다 큰 연산자 (>)의 문자열별 구현이 되어 있습니다. String 타입의 파라미터 2개가 있는 메서드로 정의하고 Bool 타입의 값을 반환합니다. 이것은 sorted(by:) 메서드에 필요한 메서드 타입과 정확하게 일치합니다. 따라서 간단하게 ">" 연산자를 전달할 수 있고 Swift는 문자열별 구현을 사용하기를 원한다고 유추합니다.

    reversedNames = names.sorted(by: >)

     


    후행 클로저 (Trailing Closures)

    함수의 마지막 인자로 클로저 표현식을 전달해야하고 클로저 표현식이 긴 경우 후행 클로저(trailing closure)로 작성하는 것이 유용할 수 있습니다. 후행 클로저는 함수의 인자이지만 함수 호출의 소괄호 다음에 작성합니다.

    후행 클로저 구문을 사용할 때 함수 호출의 일부로 첫번째 클로저에 대한 인자 라벨을 작성하지 않아도 됩니다.

     

    함수 호출은 여러개의 후행 클로저를 포함할 수 있지만 아래 몇가지 예제에서는 단일 후행 클로저를 사용합니다.

    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
    }

     

    위의 클로저 표현구(Closure Expression Syntax) 섹션에 문자열 정렬 클로저는 후행 클로저로 sorted(by:) 메서드의 소괄호 바깥에 작성될 수 있습니다.

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

     

    후행 클로저 표현식이 메서드의 유일한 인자일 경우 함수를 호출할 때 소괄호 () 를 작성하지 않아도 됩니다.

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

     

    후행 클로저는 클로저가 길어서 한줄로 인라인 작성이 불가능할 때 유용합니다.

    예를 들어 Swift의 Array 타입은 클로저 표현식을 단일 인자로 가지는 map(_:) 메서드가 있습니다. 이 클로저는 배열의 각 아이템에 대해 한번씩 호출되고 매핑된 대체값(다른 타입일 수 있음)이 반환됩니다. map(_:) 에 전달한 클로저의 코드에 따라 매핑 특성과 반환된 값의 타입을 지정합니다.

     

    제공된 클로저에 각 배열의 요소를 적용한 후에 map(_:) 메서드는 기존 배열의 값과 같은 순서로 새로 매핑된 값을 가진 새로운 배열을 반환합니다.

     

    다음은 Int 값의 배열을 String 값의 배열로 변환하기 위해 후행 클로저와 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]

    위 코드는 정수와 그 정수에 맞는 영어 표기를 매핑하는 딕셔너리를 생성합니다. 문자열로 변환하기 위한 정수의 배열도 정의합니다. number 배열을 사용하여 후행 클로저로 map(_:) 메서드로 클로저 표현식을 전달하여 String 값의 배열을 생성할 수 있습니다.

    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 는 클로저의 number 파라미터의 값으로 초기화되기 때문에 클로저 바디내에서 값이 수정될 수 있습니다(함수와 클로저의 파라미터는 항상 상수입니다). 

    위의 예에서 후행 클로저 구문을 사용하면 클로저가 지원하는 함수 바로 뒤에 있는 클로저의 기능을 깔끔하게 캡슐화 합니다. 전체 클로저를 map(_:) 메서드의 바깥 소괄호로 감쌀 필요가 없습니다.

     

    함수가 여러개의 클로저를 가지고 있다면 첫번재 후행 클로저의 인자 라벨을 생략하고 남은 후행 클로저의 라벨은 표기합니다. 예를 들어 아래의 함수는 사진 갤러리에서 사진 하나를 불러옵니다.

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

    이 예제에서 loadPicture(from:completion:onFailure:) 함수는 네트워크 작업을 백그라운드로 전달하고 네트워크 작업이 완료되면 두 완료 중 하나를 호출합니다. 이렇게 함수를 작성하면 하나의 클로저에서 두 상황을 모두 처리하면서 성공 코드에서 네트워크 오류를 처리하는 코드를 명확하게 분리할 수 있습니다.

     


    캡처값 (Capturing Values)

    클로저는 컨텍스트 근처에 정의된 상수와 변수를 캡처 (capture) 할 수 있습니다. 그러면 클로저는 상수와 변수를 정의한 범위가 더이상 존재하지 않더라도 바디 내에서 해당 상수와 변수의 값을 참조하고 수정할 수 있습니다.

     

    Swift에서 값을 캡처할 수 있는 가장 간단한 클로저 형태는 다른 함수의 바디 내에 작성하는 중첩 함수입니다. 중첩 함수는 바깥 함수의 어떠한 인자도 캡처할 수 있고 바깥 함수 내에 정의된 상수와 변수를 캡처할 수도 있습니다.

     

    아래는 incrementer 라는 중첩 함수가 포함된 makeIncrementer 라는 함수의 예입니다.

    중첩된 incrementer() 함수는 둘러싸인 컨텍스트에 runningTotal 과 amount 인 2개의 값을 캡처합니다.

    이 값을 캡처한 후에 incrementer 는 호출될 때마다 amount 로 runningTotal을 증가시키는 클로저로 makeIncrementer에 의해 반환됩니다.

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

    makeIncrementer 의 반환 타입은 () -> Int 입니다. 이것은 단순한 값이 아닌 함수를 반환한다는 의미입니다. 

    반환하는 함수에는 파라미터가 없으며 호출될 때마다 Int 값을 반환합니다.

    makeIncrementer 함수는 실제 증가를 수행하는 incrementer 라는 중첩 함수를 정의합니다.

     

    incrementer() 함수만 단독으로 보면 비정상적으로 보일 수 있습니다.

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

    외부 함수에 runningTotal 과 amount 대한 참조(reference)를 캡처하고 함수 내에서 사용합니다.

    참조를 캡처하는 것은 makeIncrementer 호출이 종료될 때 runningTotal과 amount가 사라지지 않고 다음에 incrementer 함수가 호출될 때 runningTotal 을 사용할 수 있습니다.

     

    Swift의 최적화로 인해 값이 클로저에 의해 변경되지 않거나 클로저가 생성된 후 값이 변경되지 않는 경우 값의 복사본을 캡처하고 저장할 수 있습니다.
    Swift는 더이상 필요하지 않을때 변수와 관련된 모든 메모리를 관리합니다.

     

    다음은 makeIncrementer 수행에 대한 예입니다.

    let incrementByTen = makeIncrementer(forIncrement: 10)

     

    두 번째 함수를 생성하면 새롭게 분리된 runningTotal 변수에 참조 저장됩니다.

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

     

    기존 incrementByTen을 다시 호출하면 그것의 runningTotal 변수는 이어서 증가되고 incrementBySeven의 캡처된 변수는 영향을 주지 않습니다.

    incrementByTen()
    // returns a value of 40

     

    클래스 인스턴스의 프로퍼티에 클로저를 할당하고 클로저가 인스턴스 또는 멤버를 참조하여 해당 인스턴스를 캡처하면 클로저와 인스턴스 사이에 강한 참조 사이클이 생성됩니다. Swift는 캡처 목록을 사용하여 이러한 강한 참조 사이클을 깨뜨립니다. 

     


    클로저는 참조 타입 (Closures Are Reference Types)

    위 예제에서 incrementBySeven 과 incrementByTen 은 상수이지만 이러한 상수가 참조하는 클로저는 캡처한 runningTotal 변수를 계속 증가시킬 수 있습니다. 이는 함수와 클로저가 참조 타입(reference types)이기 때문입니다.

     

    함수 또는 클로저를 상수 또는 변수에 할당할 때마다, 실제로 함수나 클로저를 참조(reference)하는 상수와 변수를 설정합니다. 위의 예에서 incrementByTen 은 상수를 참조하는 클로저를 선택하고 클로저 자신의 내용은 아닙니다.

    이는 클로저에 두 개의 다른 상수와 변수를 할당하는 경우에, 이러한 상수나 변수 모두 동일한 클로저를 참조할 것을 의미합니다.

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

    위의 예제는 alsoIncrementByTen 호출은 incrementByTen 호출과 같음을 보여줍니다. 2개 모두 같은 클로저를 참조하기 때문에 둘다 증가하고 같은 러닝 합계를 반환합니다.

     


    이스케이프 클로저 (Escaping Closures)

    클로저가 함수에 인자로 전달될때 클로저는 함수를 탈출한다(escape)고 말하지만, 함수가 반환한 뒤에 호출됩니다. 클로저를 파라미터로 갖는 함수를 선언할 때 이 클로저가 탈출하기를 허락한다는 의미로 파라미터의 타입 전에 @escaping 을 작성할 수 있습니다.

     

    클로저가 탈출할 수 있는 한가지 방법은 함수 바깥에 정의된 변수에 저장되는 것입니다. 예를 들어 비동기적 작업을 시작하는 대부분의 함수는 완료 핸들러로 클로저를 사용합니다. 이 함수는 작업을 시작한 후에 반환되지만 작업이 완료될때까지 클로저는 호출되지 않습니다. 클로저가 나중에 호출되도록 탈출(escape)해야 합니다. 

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

    someFunctionWithEscapingClosure(_:) 함수는 인자로 클로저를 가지고 있고 함수 바깥에 선언된 배열에 클로저를 추가합니다. 함수의 파라미터에 @escaping 을 표시하지 않으면 컴파일 시 에러가 발생합니다.

     

    self 를 참조하는 escape 클로저는 self 가 클래스의 인스턴스를 참조하는 경우 특별한 고려가 필요합니다. escape 클로저에서 self를 캡처하면 강한 참조 사이클이 생기기 쉽습니다.

    일반적으로 클로저는 클로저 내부에서 변수를 사용하여 암시적으로 변수를 캡처하지만, 이 경우에는 명시적이어야 합니다. self 를 캡처하려면 self를 사용할 때 명시적으로 self 를 작성하거나 클로저의 캡처 목록에 self 를 포함합니다. self 를 명시적으로 쓰면 의도를 표현할 수 있으며 참조 사이클이 없음을 확인할 수 있습니다. 

     

    예를 들어 아래 코드에서 someFunctionWithEscapingClosure(_:) 에 전달된 클로저는 명시적으로 self 를 참조합니다. 반대로 someFunctionWithNonescapingClosure(_:) 에 전달된 클로저는 nonescaping 클로저입니다. 즉 암시적으로 self 를 참조할 수 있습니다.

    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"

     

    다음 예시는 클로저의 캡처 목록에 포함시켜 "self "를 캡처한 다음 암시적으로 "self"를 가리키는 doSomething() 입니다.

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

    self 가 구조체 또는 열거형 인스턴스이면 항상 암시적으로 self 를 참조할 수 있습니다. 그러나 escaping 클로저는 구조체 또는 열거형 인스턴스면 self 에 대한 변경 가능한 참조를 캡처할 수 없습니다. 구조체와 열거형은 공유 변경을 허용하지 않습니다.

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

    위의 예제에서 someFunctionWithEscapingClosure 함수 호출은 변경 가능한 메서드 내부에 있기 때문에 에러이고 self 는 변경 가능합니다. 이것은 이스케이프 클로저는 구조체인 self 를 변경가능한 참조로 캡처할 수 없다는 규칙을 위반합니다.

     


    자동 클로저 (Autoclosures)

    자동 클로저(autoclosure)는 함수에 인자로 전달되는 표현식을 래핑하기 위해 자동으로 생성되는 클로저입니다. 인자를 가지지 않으며 호출될 때 내부에 래핑된 표현식의 값을 반환합니다. 이러한 구문상의 편의를 통해 명시적 클로저 대신에 일반 표현식을 작성하여 함수의 파라미터 주위의 중괄호를 생략할 수 있습니다.

     

    일반적으로 함수 호출(call)은 자동클로져(autoclosures) 이지만, 이러한 종류의 함수를 구현(implement)하는 것은 일반적이지 않습니다. 예를 들어 assert(condition:message:file:line:) 함수는 condition 과 message 파라미터에 대한 자동 클로저를 가집니다. condition 파라미터는 오직 디버그 빌드인지 판단하고 message 파라미터는 condition 이 false 인지만 판단됩니다.

     

    자동 클로저는 클로저가 호출될 때까지 코드 내부 실행이 되지 않기 때문에 처리가 지연될 수 있습니다. 코드 처리 시기를 제어할 수 있기 때문에 사이드 이펙트가 있거나 계산이 오래 걸리는 코드에 유용합니다. 아래 코드는 클로저가 어떻게 처리를 지연하는지 보여줍니다.

    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 배열의 첫번째 요소는 삭제되지만 클로저가 실제로 호출되기 전까지 삭제되지 않습니다. 클로저가 호출되지 않으면 클로저 내부의 표현식은 판단되지 않습니다.

     

    함수의 인자로 클로저를 전달하면 위와 같은 지연 판단과 동일한 동작을 가질 수 있습니다.

    // 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:) 함수는 소비자의 이름을 반환하는 명시적 클로저를 가집니다.

     

    아래의 serve(customer:) 의 버전은 같은 동작을 수행하지만 명시적 클로저를 대신 파라미터 타입에 @autoclosure 속성을 표기하여 자동 클로저를 가집니다. 그럼 클로저 대신 String 인수를 받는 것처럼 함수를 호출할 수 있습니다. 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!"

    자동 클로저를 남용하면 코드 이해를 어렵게 만들 수 있습니다. 컨텍스트(context)와 함수 이름은 처리가 지연된다는 것을 명확히 해야합니다.

    자동 클로저가 이스케이프를 허용하길 원한다면 @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!"

    위의 코드에서 collectCustomerProviders(_:) 함수는 customerProvider 인자로 전달된 클로저를 호출하는 대신에 customerProviders 배열에 추가합니다. 이 배열은 함수의 범위 밖에 선언되어 있습니다. 이것은 배열의 클로저는 함수가 반환된 후에 실행될 수 있다는 의미입니다. 그 결과 customerProvider 인자의 값은 함수의 범위를 벗어날 수 있어야 합니다.

     


    참조

    https://docs.swift.org/swift-book/

    https://bbiguduk.gitbook.io/swift/

     

     

    반응형