ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Swift 정리 #7 클로저
    코딩/Swift 2022. 12. 14. 14:25
    728x90

    클로저

    클로저는 전달되고 코드에서 사용될 수 있는 독립적인 기능 블록입니다. Swift의 클로저는 C 및 Objective-C의 블록과 다른 프로그래밍 언어의 람다와 유사합니다.

    클로저는 정의된 컨텍스트에서 어떤 상수 및 변수에 대한 참조를 캡처하고 저장할 수 있습니다. 이것은 이러한 상수와 변수를 닫는다라는 것으로 알려져 있습니다. Swift는 캡처의 모든 메모리 관리를 처리합니다.

    Functions에서 소개된 전역 및 중첩 함수는 실제로 클로저의 특별한 경우입니다. 클로저는 다음 세 가지 형식 중 하나를 취합니다.

    • 전역 함수는 이름이 있고 값을 캡처하지 않는 클로저
    • 중첩 함수는 이름이 있고 둘러싸는 함수에서 값을 캡처할 수 있는 클로저
    • 클로저 표현식은 주변 컨텍스트에서 값을 캡처할 수 있는 가벼운 구문으로 작성된 명명되지 않은 클로저

    Swift의 클로저 표현식은 일반적인 시나리오에서 간결하고 깔끔한 구문을 장려하는 최적화와 함께 깨끗하고 명확한 스타일을 가지고 있습니다. 이러한 최적화에는 다음이 포함됩니다.

    • 컨텍스트에서 매개변수 및 반환 값 유형 유추
    • 단일 표현식 클로저의 암시적 반환
    • 단축 인수 이름
    • 후행 클로져 구문

    클로저 표현식

    중첩 함수는 자체 포함 코드 블록을 더 큰 함수의 일부로 명명하고 정의하는 편리한 방법입니다. 그러나 전체 선언 및 이름 없이 함수와 유사한 구성의 더 짧은 버전을 작성하는 것이 때때로 유용합니다. 이는 함수를 하나 이상의 인수로 사용하는 함수나 메서드로 작업할 때 특히 그렇습니다.

    클로저 표현식은 간결하고 집중된 구문으로 인라인 클로저를 작성하는 방법입니다. 클로저 표현식은 명확성이나 의도를 잃지 않고 짧은 형식으로 클로저를 작성하기 위한 몇 가지 구문 최적화를 제공합니다. 아래의 클로저 표현식 예제는 sorted(by:) 메소드의 단일 예제를 여러 반복에 걸쳐 개선하여 이러한 최적화를 보여줍니다. 각 반복은 동일한 기능을 보다 간결한 방식으로 표현합니다.

    The Sorted Method

    Swift의 표준 라이브러리는 사용자가 제공하는 정렬 클로저의 출력을 기반으로 알려진 유형의 값 배열을 정렬하는 sorted(by:)라는 메서드를 제공합니다. 정렬 프로세스가 완료되면 sorted(by:) 메서드는 올바른 정렬 순서의 요소와 함께 이전 배열과 유형 및 크기가 동일한 새 배열을 반환합니다. 원래 배열은 sorted(by:) 메서드에 의해 수정되지 않습니다.

    아래의 클로저 표현식 예제는 sorted(by:) 메소드를 사용하여 String 값의 배열을 알파벳 역순으로 정렬합니다. 정렬할 초기 배열은 다음과 같습니다.

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

    sorted(by:) 메서드는 배열의 내용과 같은 유형의 두 인수를 받는 클로저를 허용하고 값이 정렬된 후 첫 번째 값이 두 번째 값 앞이나 뒤에 나타나야 하는지 여부를 나타내는 Bool 값을 반환합니다. 정렬 클로저는 첫 번째 값이 두 번째 값 앞에 나타나야 하는 경우 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를 반환하여 정렬된 배열에서 s1이 s2 앞에 나타나야 함을 나타냅니다. 문자열에 있는 문자의 경우 "보다 큼"은 "알파벳에서 나중에 나타남"을 의미합니다. 이는 문자 "B"가 문자 "A"보다 "더 크고" 문자열 "Tom"이 문자열 "Tim"보다 크다는 것을 의미합니다. 이렇게 하면 "Barry"가 "Alex" 앞에 오는 등 알파벳 역순으로 정렬됩니다.

    그러나 이것은 본질적으로 단일 표현식 함수(a > b)를 작성하는 다소 장황한 방법입니다. 이 예제에서는 클로저 표현식 구문을 사용하여 정렬 클로저를 인라인으로 작성하는 것이 좋습니다.

    클로저 표현식 구문

    { (parameters) -> return_type in
        statements
    }

    클로저 표현식 구문의 매개변수는 인-아웃 매개변수일 수 있지만 기본값을 가질 수는 없습니다. 가변 매개변수는 가변 매개변수의 이름을 지정하면 사용할 수 있습니다. 튜플은 매개변수 유형 및 반환 유형으로도 사용할 수 있습니다.

    아래 예제는 위의 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:) 메서드에 대한 전반적인 호출이 동일하게 유지되었음을 보여줍니다. 한 쌍의 괄호는 여전히 메서드에 대한 전체 인수를 래핑합니다. 그러나 해당 인수는 이제 인라인 클로저입니다.

    컨텍스트에서 유형 유추

    정렬 클로저가 메소드에 대한 인수로 전달되기 때문에 Swift는 매개변수의 유형과 리턴하는 값의 유형을 유추할 수 있습니다. sorted(by:) 메서드는 문자열 배열에서 호출되므로 해당 인수는 (String, String) -> Bool 유형의 함수여야 합니다. 이는 (String, String) 및 Bool 유형을 클로저 표현식 정의의 일부로 작성할 필요가 없음을 의미합니다. 모든 유형을 유추할 수 있으므로 리턴 화살표(->)와 매개변수 이름 주위의 괄호도 생략할 수 있습니다.

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

    클로저를 인라인 클로저 표현식으로 함수나 메서드에 전달할 때 매개변수 유형과 반환 유형을 유추하는 것은 항상 가능합니다. 결과적으로 클로저가 함수 또는 메소드 인수로 사용될 때 완전한 형식으로 인라인 클로저를 작성할 필요가 없습니다.

    그럼에도 불구하고 원하는 경우 여전히 유형을 명시적으로 만들 수 있으며 코드를 읽는 사람에게 모호성을 피하는 경우 그렇게 하는 것이 좋습니다. sorted(by:) 메소드의 경우, 클로저의 목적은 정렬이 일어나고 있다는 사실에서 명확하며 읽는 사람이 클로저가 문자열 값으로 작동할 가능성이 높다고 가정하는 것이 안전합니다. 그것은 문자열 배열의 정렬을 지원하기 때문입니다.

    단일 표현식 클로저의 암시적 반환

    단일 식 클로저는 이전 예제의 이 버전에서와 같이 선언에서 return 키워드를 생략하여 단일 식의 결과를 암시적으로 반환할 수 있습니다.

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

    여기에서 sorted(by:) 메소드의 인자의 함수 유형은 Bool 값이 클로저에 의해 반환되어야 함을 분명히 합니다. 클로저의 본문에는 Bool 값을 반환하는 단일 표현식(s1 > s2)이 포함되어 있으므로 모호성이 없으며 return 키워드를 생략할 수 있습니다.

    속기 인수 이름

    Swift는 자동으로 인라인 클로저에 속기 인수 이름을 제공하며, 이는 $0, $1, $2 등의 이름으로 클로저의 인수 값을 참조하는 데 사용할 수 있습니다.

    클로저 표현식 내에서 이러한 속기 인수 이름을 사용하는 경우 클로저의 인수 목록을 정의에서 생략할 수 있습니다. 속기 인수 이름의 유형은 예상되는 함수 유형에서 유추되며 사용하는 가장 높은 번호의 속기 인수는 클로저가 취하는 인수 수를 결정합니다. in 키워드도 생략할 수 있습니다. 클로저 표현식이 전체 본문으로 구성되기 때문입니다.

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

    여기서 $0 및 $1은 클로저의 첫 번째 및 두 번째 문자열 인수를 참조합니다. $1은 가장 높은 숫자를 가진 속기 인수이기 때문에 클로저는 두 개의 인수를 취하는 것으로 이해됩니다. 여기서 sorted(by:) 함수는 인수가 모두 문자열인 클로저를 예상하기 때문에 속기 인수 $0 및 $1은 모두 문자열 유형입니다.

    연산자 방법

    실제로 위의 클로저 표현식을 작성하는 더 짧은 방법이 있습니다. Swift의 문자열 유형은 보다 큼 연산자(>)의 문자열 특정 구현을 문자열 유형의 두 매개변수를 갖는 메소드로 정의하고 Bool 유형의 값을 반환합니다. 이는 sorted(by:) 메서드에 필요한 메서드 유형과 정확히 일치합니다. 따라서 보다 큼 연산자를 간단히 전달할 수 있으며 Swift는 문자열 특정 구현을 사용하기를 원한다고 추론합니다.

    reversedNames = names.sorted(by: >)

    후행 클로저

    클로저 표현식을 함수의 마지막 인수로 함수에 전달해야 하고 클로저 표현식이 긴 경우 대신 후행 클로저로 작성하는 것이 유용할 수 있습니다. 후행 클로저가 여전히 함수의 인수인 경우에도 함수 호출의 괄호 뒤에 후행 클로저를 작성합니다. 후행 클로저 구문을 사용하는 경우 함수 호출의 일부로 첫 번째 클로저에 대한 인수 레이블을 작성하지 않습니다. 함수 호출에는 여러 후행 클로저가 포함될 수 있습니다. 그러나 아래의 처음 몇 가지 예는 단일 후행 클로저를 사용합니다.

    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
    }

    위의 클로저 표현식 구문 섹션의 문자열 정렬 클로저는 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]

    위의 코드는 정수 숫자와 해당 이름의 영어 버전 간의 매핑 dictionary를 만듭니다. 또한 문자열로 변환할 준비가 된 정수 배열을 정의합니다.

    이제 숫자 배열을 사용하여 배열의 map(_:) 메서드에 후행 클로저로 클로저 표현식을 전달하여 문자열 값의 배열을 만들 수 있습니다.

    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 매개변수 값으로 초기화되어 클로저 본문 내에서 값을 수정할 수 있습니다. (함수와 클로저에 대한 매개변수는 항상 상수입니다.) 클로저 표현식은 매핑된 출력 배열에 저장될 유형을 나타내기 위해 문자열 반환 유형도 지정합니다.

    클로저 표현식은 호출될 때마다 output이라는 문자열을 만듭니다. 나머지 연산자(숫자 % 10)를 사용하여 숫자의 마지막 숫자를 계산하고 이 숫자를 사용하여 digitNames dictionary에서 적절한 문자열을 찾습니다. 클로저는 0보다 큰 정수의 문자열 표현을 만드는 데 사용할 수 있습니다.

    노트
    digitNames dictionary의 아래 첨자에 대한 호출 뒤에 느낌표(!)가 붙습니다. 사전 아래 첨자는 키가 존재하지 않으면 사전 조회가 실패할 수 있음을 나타내는 선택적 값을 반환하기 때문입니다. 위의 예에서 숫자 % 10은 항상 digitNames 사전에 대한 유효한 아래 첨자 키이므로 느낌표는 아래 첨자의 선택적 반환 값에 저장된 문자열 값을 강제로 풀기 위해 사용됩니다.


    digitNames dictionary에서 검색된 문자열은 출력의 앞에 추가되어 숫자의 문자열 버전을 효과적으로 역순으로 작성합니다. (숫자 % 10은 16의 경우 6, 58의 경우 8, 510의 경우 0의 값을 제공합니다.)

    그런 다음 숫자 변수를 10으로 나눕니다. 정수이기 때문에 나누기에서 내림하므로 16은 1, 58은 5, 510은 51이 됩니다.

    이 프로세스는 number가 0이 될 때까지 반복되며, 이 시점에서 출력 문자열은 클로저에 의해 반환되고 map(_:) 메서드에 의해 출력 배열에 추가됩니다.

    위의 예제에서 후행 클로저 구문을 사용하면 클로저가 지원하는 함수 바로 뒤에 클로저의 기능을 깔끔하게 캡슐화할 수 있습니다. 전체 클로저를 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: someServer) { picture in
        someView.currentPicture = picture
    } onFailure: {
        print("Couldn't download the next picture.")
    }

    이 예제에서 loadPicture(from:completion:onFailure:) 함수는 네트워크 작업을 백그라운드로 디스패치하고 네트워크 작업이 완료되면 두 완료 핸들러 중 하나를 호출합니다. 이 방법으로 함수를 작성하면 두 가지 상황을 모두 처리하는 하나의 클로저를 사용하는 대신 성공적인 다운로드 후 사용자 인터페이스를 업데이트하는 코드에서 네트워크 오류 처리를 담당하는 코드를 명확하게 분리할 수 있습니다.

    노트
    완료 핸들러는 특히 여러 핸들러를 중첩해야 하는 경우 읽기 어려워질 수 있습니다. 다른 접근 방식은 동시성에 설명된 대로 비동기 코드를 사용하는 것입니다.


    값 캡처

    클로저는 정의된 주변 컨텍스트에서 상수와 변수를 캡처할 수 있습니다. 그런 다음 클로저는 상수와 변수를 정의한 원래 범위가 더 이상 존재하지 않더라도 본문 내에서 해당 상수와 변수의 값을 참조하고 수정할 수 있습니다.

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

    다음은 incrementer라는 중첩 함수를 포함하는 makeIncrementer라는 함수의 예입니다. 중첩된 incrementer() 함수는 주변 컨텍스트에서 runningTotal 및 amount라는 두 값을 캡처합니다. 이러한 값을 캡처한 후, 호출될 때마다 runningTotal을 양만큼 증가시키는 클로저로 makeIncrementer에 의해 incrementer가 반환됩니다.

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

    makeIncrementer의 반환 유형은 () -> Int입니다. 이것은 단순한 값이 아닌 함수를 반환한다는 것을 의미합니다. 반환하는 함수에는 매개 변수가 없으며 호출될 때마다 Int 값을 반환합니다.

    makeIncrementer(forIncrement:) 함수는 반환될 증분기의 현재 누계를 저장하기 위해 runningTotal이라는 정수 변수를 정의합니다. 이 변수는 값 0으로 초기화됩니다.

    makeIncrementer(forIncrement:) 함수에는 인수 레이블이 forIncrement이고 매개변수 이름이 amount인 단일 Int 매개변수가 있습니다. 이 매개변수에 전달된 인수 값은 반환된 증분 함수가 호출될 때마다 runningTotal이 증가해야 하는 양을 지정합니다. makeIncrementer 함수는 실제 증가를 수행하는 incrementer라는 중첩 함수를 정의합니다. 이 함수는 단순히 runningTotal에 금액을 더하고 결과를 반환합니다.

    개별적으로 고려하면 중첩된 incrementer() 함수가 비정상적으로 보일 수 있습니다.

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

    incrementer() 함수에는 매개변수가 없지만 함수 본문 내에서 runningTotal 및 amount를 참조합니다. 주변 함수에서 runningTotal 및 amount에 대한 참조를 캡처하고 자체 함수 본문 내에서 사용하여 이를 수행합니다. 참조로 캡처하면 makeIncrementer에 대한 호출이 종료될 때 runningTotal 및 amount가 사라지지 않고 다음에 incrementer 함수가 호출될 때 runningTotal을 사용할 수 있습니다.

    노트
    최적화로서 Swift는 값이 클로저에 의해 변경되지 않고 클로저가 생성된 후 값이 변경되지 않은 경우 대신 값의 복사본을 캡처하고 저장할 수 있습니다.
    또한 Swift는 더 이상 필요하지 않은 변수 폐기와 관련된 모든 메모리 관리를 처리합니다.


    다음은 작동 중인 makeIncrementer의 예입니다.

    let incrementByTen = makeIncrementer(forIncrement: 10)

    이 예제는 호출될 때마다 runningTotal 변수에 10을 추가하는 증분 함수를 참조하도록 incrementByTen이라는 상수를 설정합니다. 함수를 여러 번 호출하면 이 동작이 작동하는 것을 볼 수 있습니다.

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

    두 번째 증분기를 만들면 새로운 별도의 runningTotal 변수에 대한 참조가 저장됩니다.

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

    원래 증분기(incrementByTen)를 다시 호출하면 자체 runningTotal 변수가 계속 증가하며 incrementBySeven에 의해 캡처된 변수에는 영향을 주지 않습니다.

    incrementByTen()
    // returns a value of 40

    노트
    클로저를 클래스 인스턴스의 속성에 할당하고 클로저가 인스턴스 또는 해당 멤버를 참조하여 해당 인스턴스를 캡처하는 경우 클로저와 인스턴스 사이에 강력한 참조 순환이 생성됩니다. Swift는 캡처 목록을 사용하여 이러한 강력한 참조 순환을 끊습니다.


    클로저는 참조 유형

    위의 예에서 incrementBySeven 및 incrementByTen은 상수이지만 이러한 상수가 참조하는 클로저는 여전히 캡처한 runningTotal 변수를 증가시킬 수 있습니다. 이는 함수와 클로저가 참조 유형이기 때문입니다.

    함수나 클로저를 상수나 변수에 할당할 때마다 실제로 해당 상수나 변수를 함수나 클로저에 대한 참조로 설정하는 것입니다. 위의 예에서, incrementByTen이 참조하는 것은 클로저 자체의 내용이 아니라 상수를 참조하는 클로저의 선택입니다.

    이는 클로저를 두 개의 다른 상수나 변수에 할당하면 두 상수나 변수가 모두 동일한 클로저를 참조한다는 의미이기도 합니다.

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

    위의 예는 alsoIncrementByTen을 호출하는 것이 incrementByTen을 호출하는 것과 동일함을 보여줍니다. 둘 다 동일한 클로저를 참조하기 때문에 둘 다 증가하고 동일한 누계를 반환합니다.

    이스케이프 클로저

    클로저는 함수에 대한 인수로 전달되지만 함수가 반환된 후에 호출될 때 함수를 이스케이프한다고 합니다. 클로저를 매개 변수 중 하나로 사용하는 함수를 선언할 때 매개 변수의 유형 앞에 @escaping을 작성하여 클로저가 이스케이프할 수 있음을 나타낼 수 있습니다.

    클로저가 이스케이프할 수 있는 한 가지 방법은 함수 외부에 정의된 변수에 저장되는 것입니다. 예를 들어 비동기 작업을 시작하는 많은 함수는 완료 처리기로 클로저 인수를 사용합니다. 함수는 작업을 시작한 후 반환되지만 클로저는 작업이 완료될 때까지 호출되지 않습니다. 클로저는 나중에 호출하기 위해 이스케이프해야 합니다. 예를 들어:

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

    someFunctionWithEscapingClosure(_:) 함수는 클로저를 인자로 받아 함수 외부에 선언된 배열에 추가합니다. 이 함수의 매개변수를 @escaping으로 표시하지 않으면 컴파일 타임 오류가 발생합니다.

    self가 클래스의 인스턴스를 참조하는 경우 self를 참조하는 이스케이프 클로저는 특별한 고려가 필요합니다. 이스케이프 클로저에서 self를 캡처하면 자연스럽게 강력한 참조 순환을 쉽게 만들 수 있습니다.

    일반적으로 클로저는 변수를 클로저 본문에서 사용하여 암시적으로 변수를 캡처하지만 이 경우에는 명시적이어야 합니다. self를 캡처하려면 self를 사용할 때 명시적으로 작성하거나 클로저의 캡처 목록에 self를 포함하십시오. 명시적으로 self를 작성하면 의도를 표현할 수 있고 참조 순환이 없음을 확인하도록 상기시켜줍니다. 예를 들어, 아래 코드에서 someFunctionWithEscapingClosure(_:) 에 전달된 클로저는 자기 자신을 명시적으로 참조합니다. 대조적으로, 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를 참조할 수 있습니다. 그러나 이스케이프 클로저는 self가 구조체나 열거형의 인스턴스일 때 self에 대한 변경 가능한 참조를 캡처할 수 없습니다. 구조체와 열거형은 공유 가변성을 허용하지 않습니다.

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

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

    자동 클로저

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

    자동 클로저를 사용하는 함수를 호출하는 것은 일반적이지만 그런 종류의 함수를 구현하는 것은 일반적이지 않습니다. 예를 들어, assert(condition:message:file:line:) 함수는 조건 및 메시지 매개변수에 대해 자동 종료를 취합니다. 해당 조건 매개변수는 디버그 빌드에서만 평가되고 해당 메시지 매개변수는 조건이 거짓인 경우에만 평가됩니다.

    자동 클로저는 클로저를 호출할 때까지 내부 코드가 실행되지 않기 때문에 평가를 지연시킬 수 있습니다. 지연 평가는 코드 평가 시기를 제어할 수 있기 때문에 부작용이 있거나 계산 비용이 많이 드는 코드에 유용합니다. 아래 코드는 클로저가 평가를 지연시키는 방법을 보여줍니다.

    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의 유형은 문자열이 아니라 () -> 문자열(문자열을 반환하는 매개 변수가 없는 함수)입니다.

    클로저를 함수에 대한 인수로 전달할 때 지연된 평가와 동일한 동작을 얻습니다.

    // 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 속성으로 매개변수의 유형을 표시하여 자동 클로저를 취합니다. 이제 클로저 대신 문자열 인수를 받는 것처럼 함수를 호출할 수 있습니다. 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!"

    노트
    자동 클로저를 과도하게 사용하면 코드를 이해하기 어려울 수 있습니다. 컨텍스트 및 함수 이름은 평가가 연기되고 있음을 분명히 나타내야 합니다.


    이스케이프가 허용된 자동 클로저를 원하면 @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 인수로 전달된 클로저를 호출하는 대신 collectCustomerProviders(_:) 함수는 클로저를 customerProviders 배열에 추가합니다. 배열은 함수 범위 밖에서 선언됩니다. 즉, 배열의 클로저는 함수가 반환된 후에 실행될 수 있습니다. 결과적으로 customerProvider 인수의 값은 함수 범위를 벗어날 수 있어야 합니다.

    728x90

    '코딩 > Swift' 카테고리의 다른 글

    Swift 정리 #9 구조체와 클래스  (0) 2022.12.28
    Swift 정리 #8 Enumerations  (0) 2022.12.28
    Swift 정리 #6 함수  (0) 2022.12.14
    Swift 정리 #5 Control flow  (1) 2022.12.14
    Swift 정리 #4 컬렉션 타입  (0) 2022.12.14

    댓글

Designed by Tistory.