Generics

Groot·2022년 10월 10일
0

Swift Language Guide

목록 보기
22/24
post-thumbnail

Generics

  • Generic 코드를 사용하면 정의한 요구 사항에 따라 모든 type에서 작동할 수 있는 유연하고 재사용 가능한 함수 및 type을 작성할 수 있습니다.
  • 중복을 피하고 명확하고 추상적인 방식으로 의도를 표현하는 코드를 작성할 수 있습니다.

  • Generics는 Swift의 가장 강력한 기능 중 하나이며 Swift 표준 라이브러리의 대부분은 Generic 코드로 빌드됩니다.
  • 사실, 인지하지 못하더라도 언어 가이드 전체에서 Generic을 사용하고 있습니다.
  • 예를 들어, Swift의 Array 및 Dictionary type은 모두 Generic 컬렉션입니다.
  • Int 값을 보유하는 배열, String 값을 보유하는 배열, 또는 실제로 Swift에서 생성할 수 있는 다른 type에 대한 배열을 생성할 수 있습니다.
  • 마찬가지로 지정된 type의 값을 저장하는 사전을 만들 수 있으며 해당 type에 대한 제한은 없습니다.

📌 The Problem That Generics Solve

  • 다음은 두 개의 Int 값을 교환하는 swapTwoInts(::)라는 nongeneric인 표준 함수입니다.

    func swapTwoInts(_ a: inout Int, _ b: inout Int) {
        let temporaryA = a
        a = b
        b = temporaryA
    }
  • 이 함수는 In-Out 매개변수에 설명된 대로 in-out 매개변수를 사용하여 및 b의 값을 교환합니다.

  • swapTwoInts(::) 함수는 b의 원래 값을 b로 바꾸고 a의 원래 값을 b로 바꿉니다. 이 함수를 호출하여 두 Int 변수의 값을 바꿀 수 있습니다.

    var someInt = 3
    var anotherInt = 107
    swapTwoInts(&someInt, &anotherInt)
    print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
    // Prints "someInt is now 107, and anotherInt is now 3"
  • swapTwoInts(::) 함수는 유용하지만 Int 값에만 사용할 수 있습니다.

  • 두 개의 String 값 또는 두 개의 Double 값을 교환하려면 아래 표시된 swapTwoStrings(::) 및 swapTwoDoubles(::) 함수와 같은 더 많은 함수를 작성해야 합니다.

    func swapTwoStrings(_ a: inout String, _ b: inout String) {
        let temporaryA = a
        a = b
        b = temporaryA
    }
    
    func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
        let temporaryA = a
        a = b
        b = temporaryA
    }
  • swapTwoInts(::), swapTwoStrings(::) 및 swapTwoDoubles(::) 함수의 본문이 동일하다는 것을 눈치채셨을 것입니다. 유일한 차이점은 허용하는 값의 type(Int, String 및 Double)입니다.

  • 모든 type의 두 값을 교환하는 단일 함수를 작성하는 것이 더 유용하고 훨씬 더 유연합니다. Generic 코드를 사용하면 이러한 기능을 작성할 수 있습니다. (이러한 함수의 Generic 버전은 아래에 정의되어 있습니다.)

    세 함수 모두에서 a 및 b type은 동일해야 합니다.
    a 와 b가 같은 type이 아니면 값을 바꿀 수 없습니다.
    Swift는 type이 안전한 언어이며 (예를 들어) String type의 변수와 Double type의 변수가 서로 값을 교환하는 것을 허용하지 않습니다. 그렇게 하려고 하면 컴파일 시간 오류가 발생합니다.

📌 Generic Functions

  • Generic 함수는 모든 type에서 작동할 수 있습니다. 다음은 위의 swapTwoInts(::) 함수의 Generic 버전으로, swapTwoValues(::)라고 합니다.

    func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
        let temporaryA = a
        a = b
        b = temporaryA
    }
  • swapTwoValues(::) 함수의 본문은 swapTwoInts(::) 함수의 본문과 동일합니다, 그러나 swapTwoValues(::)의 첫 번째 줄은 swapTwoInts(::)와 약간 다릅니다.

  • 첫 번째 줄을 비교하는 방법은 다음과 같습니다.

    func swapTwoInts(_ a: inout Int, _ b: inout Int)
    func swapTwoValues<T>(_ a: inout T, _ b: inout T)
  • 함수의 Generic 버전은 실제 type 이름(예: Int, String 또는 Double) 대신 자리 표시자 type 이름(이 경우 T라고 함)을 사용합니다.

  • 자리 표시자 type 이름은 T가 무엇인지에 대해 아무 것도 말하지 않지만, T가 나타내는 것이 무엇이든 간에 와 b는 모두 동일한 type T여야 한다고 말합니다.

  • T 대신 사용할 실제 type은 swapTwoValues(_:_:) 함수가 호출될 때마다 결정됩니다.

  • Generic 함수와 Generic이 아닌 함수의 다른 차이점은 Generic 함수의 이름(swapTwoValues(::)) 다음에 꺾쇠 괄호(\) 안에 자리 표시자 type 이름(T)이 온다는 것입니다.

  • 괄호는 T가 swapTwoValues(::) 함수 정의 내의 자리 표시자 type 이름임을 Swift에 알려줍니다.

  • T는 자리 표시자이기 때문에 Swift는 T라는 실제 type을 찾지 않습니다.

  • swapTwoValues(::) 함수는 이제 swapTwoInts와 같은 방식으로 호출될 수 있습니다. 단, 두 값이 서로 같은 type인 한 모든 type의 두 값을 전달할 수 있다는 점만 다릅니다.

  • swapTwoValues(::)가 호출될 때마다 T에 사용할 type은 함수에 전달된 값 type에서 유추됩니다.

  • 아래 두 가지 예에서 T는 각각 Int 및 String으로 유추됩니다.

    var someInt = 3
    var anotherInt = 107
    swapTwoValues(&someInt, &anotherInt)
    // someInt is now 107, and anotherInt is now 3
    
    var someString = "hello"
    var anotherString = "world"
    swapTwoValues(&someString, &anotherString)
    // someString is now "world", and anotherString is now "hello"

    위에 정의된 swapTwoValues(::) 함수는 Swift 표준 라이브러리의 일부인 스왑이라는 Generic 함수에서 영감을 얻었으며 앱에서 자동으로 사용할 수 있습니다.
    자체 코드에서 swapTwoValues(::) 함수의 동작이 필요한 경우 자체 구현을 제공하는 대신 Swift의 기존 swap(::) 함수를 사용할 수 있습니다.

📌 Type Parameters

  • 위의 swapTwoValues(::) 예에서 자리 표시자 type T는 type 매개변수의 예입니다.
  • type 매개변수는 자리 표시자 type을 지정하고 이름을 지정하며 함수 이름 바로 뒤에 일치하는 꺾쇠 괄호 쌍(예: \) 사이에 작성됩니다.

  • type 매개변수를 지정하면 이를 사용하여 함수 매개변수의 type(예: swapTwoValues(::) 함수의 a 및 b 매개변수)을 정의하거나 함수의 반환 type 또는 함수 본문 내 주석 type으로 사용할 수 있습니다.
  • 각각의 경우에 type 매개변수는 함수가 호출될 때마다 실제 type으로 대체됩니다.
  • (위의 swapTwoValues(::) 예제에서 T는 함수가 처음 호출될 때 Int로 대체되었고 두 번째 호출될 때 String으로 대체되었습니다.)

  • 꺾쇠 괄호 안에 여러 type 매개변수 이름을 쉼표로 구분하여 작성하여 둘 이상의 type 매개변수를 제공할 수 있습니다.

📌 Naming Type Parameters

  • 대부분의 경우 형식 매개 변수에는 Dictionary<Key, Value>의 키 및 값 및 Array\의 요소와 같은 설명적 이름이 있습니다.
  • 그러나 그들 사이에 의미 있는 관계가 없을 때 위의 swapTwoValues(::) 함수의 T와 같이 T, U, V와 같은 단일 문자를 사용하여 이름을 지정하는 것이 일반적입니다.

    항상 타입 매개변수에 대문자 대소문자 이름(예: T 및 MyTypeParameter)을 지정하여 값이 아닌 type의 자리 표시자임을 나타냅니다.

📌 Generic Types

  • Generic 함수 외에도 Swift를 사용하면 고유한 Generic type을 정의할 수 있습니다.
  • 이는 배열 및 사전과 유사한 방식으로 모든 type에서 작동할 수 있는 사용자 정의 class, Struct 및 enum입니다.

  • 이 섹션에서는 Stack이라는 Generic 컬렉션 type을 작성하는 방법을 보여줍니다.
  • Stack은 배열과 유사하지만 Swift의 Array type보다 더 제한된 연산 집합을 가진 정렬된 값 집합입니다.
  • 배열을 사용하면 배열의 어느 위치에서나 새 items을 삽입 및 제거할 수 있습니다.
  • 그러나 Stack을 사용하면 새 items을 컬렉션 끝에만 추가할 수 있습니다(새 값을 Stack에 push하는 것으로 알려짐).
  • 마찬가지로 Stack을 사용하면 컬렉션 끝에서만 items을 제거할 수 있습니다(Stack에서 값 pop이라고 함).

    Stack의 개념은 UINavigationController class에서 탐색 계층 Struct의 뷰 컨트롤러를 모델링하는 데 사용됩니다.
    UINavigationController class의 pushViewController(:animated:) 메서드를 호출하여 뷰 컨트롤러를 탐색 Stack에 추가(또는 push)하고 popViewControllerAnimated(:) 메서드를 호출하여 탐색 Stack에서 뷰 컨트롤러를 제거(또는 pop)합니다.
    Stack은 컬렉션 관리에 대한 엄격한 "후입선출" 접근 방식이 필요할 때마다 유용한 컬렉션 모델입니다.

  • 아래 그림은 Stack의 push 및 pop 동작을 보여줍니다.

    - 현재 Stack에는 세 개의 값이 있습니다.
    - 네 번째 값은 Stack의 맨 위에 push됩니다.
    - Stack은 이제 가장 최근 값이 맨 위에 있는 4개의 값을 보유합니다.
    - Stack의 맨 위 items이 pop됩니다.
    - 값을 pop한 후 Stack은 다시 세 개의 값을 보유합니다.
  • 다음은 Int 값 Stack의 경우 Stack의 Generic이 아닌 버전을 작성하는 방법입니다.
    struct IntStack {
        var items: [Int] = []
        mutating func push(_ item: Int) {
            items.append(item)
        }
        mutating func pop() -> Int {
            return items.removeLast()
        }
    }
  • 이 Struct는 items이라는 Array 속성을 사용하여 값을 Stack에 저장합니다.
  • Stack은 Stack에 값을 push하고 pop하는 두 가지 방법(push 및 pop)을 제공합니다.
  • 이러한 메서드는 Struct의 items 배열을 수정(또는 변경)해야 하기 때문에 변형으로 표시됩니다.

  • 그러나 위에 표시된 IntStack type은 Int 값에만 사용할 수 있습니다. 모든 type의 값 Stack을 관리할 수 있는 Generic Stack Struct를 정의하는 것이 훨씬 더 유용합니다.

  • 다음은 동일한 코드의 Generic 버전입니다.
    struct Stack<Element> {
        var items: [Element] = []
        mutating func push(_ item: Element) {
            items.append(item)
        }
        mutating func pop() -> Element {
            return items.removeLast()
        }
    }
  • Stack의 Generic 버전은 기본적으로 Generic이 아닌 버전과 동일하지만 실제 type의 Int 대신 Element라는 type 매개변수가 있는 방법에 유의하십시오.
  • 이 type 매개변수는 Struct 이름 바로 뒤에 한 쌍의 꺾쇠 괄호(\) 안에 작성됩니다.

  • Element는 나중에 제공할 형식의 자리 표시자 이름을 정의합니다. 이 미래형은 Struct의 정의 내 어디에서나 Element라고 할 수 있습니다.
  • 이 경우 Element는 다음 세 위치에서 자리 표시자로 사용됩니다.
    - Element type 값의 빈 배열로 초기화되는 items이라는 속성을 생성하려면
    - push(_:) 메소드에 item이라는 단일 매개변수가 있음을 지정하려면 Element type이어야 합니다.
    - pop() 메서드에서 반환된 값이 Element type의 값이 되도록 지정하려면

  • Generic type이기 때문에 Stack은 Array 및 Dictionary와 유사한 방식으로 Swift에서 모든 유효한 type의 Stack을 생성하는 데 사용할 수 있습니다.

  • 꺾쇠 괄호 안에 Stack에 저장할 type을 작성하여 새 Stack 인스턴스를 만듭니다. 예를 들어 새 문자열 Stack을 만들려면 Stack\()을 작성합니다.
    var StackOfStrings = Stack<String>()
    StackOfStrings.push("uno")
    StackOfStrings.push("dos")
    StackOfStrings.push("tres")
    StackOfStrings.push("cuatro")
    // the Stack now contains 4 strings
  • 다음은 이 네 가지 값을 Stack에 push한 후 StackOfStrings가 어떻게 보이는지 보여줍니다.
  • Stack에서 값을 꺼내면 최상위 값 "cuatro"가 제거되고 반환됩니다.
    let fromTheTop = StackOfStrings.pop()
    // fromTheTop is equal to "cuatro", and the Stack now contains 3 strings
  • 다음은 최상위 값을 pop한 후 Stack의 모습입니다.

📌 Extending a Generic Type

  • Generic type을 extension할 때 extension 정의의 일부로 type 매개변수 목록을 제공하지 않습니다.
  • 대신 원래 type 정의의 type 매개변수 목록을 extension 본문 내에서 사용할 수 있으며 원래 type 매개변수 이름은 원래 정의의 type 매개변수를 참조하는 데 사용됩니다.

  • 다음 예제에서는 Generic Stack type을 extension하여 topItem이라는 읽기 전용 계산 속성을 추가합니다.
  • 이 속성은 Stack에서 items을 꺼내지 않고 Stack의 맨 위 items을 반환합니다.
    extension Stack {
        var topItem: Element? {
            return items.isEmpty ? nil : items[items.count - 1]
        }
    }
  • topItem 속성은 Element type의 optional 값을 반환합니다. Stack이 비어 있으면 topItem은 nil을 반환합니다.
  • Stack이 비어 있지 않으면 topItem은 items 배열의 최종 items을 반환합니다.

  • 이 extension은 type 매개변수 목록을 정의하지 않습니다.
  • 대신, Stack type의 기존 type 매개변수 이름인 Element는 extension 내에서 사용되어 topItem 계산 속성의 optional type을 나타냅니다.

  • 이제 topItem 계산 속성을 모든 Stack 인스턴스와 함께 사용하여 제거하지 않고 상위 items에 액세스하고 쿼리할 수 있습니다.
    if let topItem = StackOfStrings.topItem {
        print("The top item on the Stack is \(topItem).")
    }
    // Prints "The top item on the Stack is tres."
  • Generic type의 extension에는 아래의 Generic Where 절이 있는 extension에 설명된 대로 새 기능을 얻기 위해 extension type의 인스턴스가 충족해야 하는 요구 사항도 포함될 수 있습니다.

📌 Type Constraints

  • swapTwoValues(::) 함수와 Stack type은 모든 type에서 작동할 수 있습니다.
  • 그러나 Generic 함수 및 Generic 형식과 함께 사용할 수 있는 형식에 특정 형식 제약 조건을 적용하는 것이 때때로 유용합니다.
  • 형식 제약 조건은 형식 매개 변수가 특정 class에서 상속되거나 특정 Protocol 또는 Protocol 구성을 준수해야 함을 지정합니다.

  • 예를 들어, Swift의 Dictionary type은 사전의 키로 사용할 수 있는 type에 제한을 둡니다.
  • 사전에 설명된 대로 사전의 키 type은 해시 가능해야 합니다.
  • 즉, 고유하게 표현할 수 있는 방법을 제공해야 합니다.
  • Dictionary는 특정 키에 대한 값이 이미 포함되어 있는지 여부를 확인할 수 있도록 키가 해시 가능해야 합니다.
  • 이 요구 사항이 없으면 Dictionary는 특정 키에 대한 값을 삽입하거나 대체해야 하는지 여부를 알 수 없으며 이미 사전에 있는 주어진 키에 대한 값을 찾을 수도 없습니다.

  • 이 요구 사항은 키 type이 Swift 표준 라이브러리에 정의된 특수 Protocol인 Hashable Protocol을 준수해야 함을 지정하는 Dictionary의 키 type에 대한 type 제약 조건에 의해 적용됩니다.
  • 모든 Swift의 기본 type(예: String, Int, Double 및 Bool)은 기본적으로 해시 가능합니다.
  • 고유한 사용자 지정 type을 Hashable Protocol을 준수하도록 만드는 방법에 대한 자세한 내용은 Hashable Protocol 준수(https://developer.apple.com/documentation/swift/hashable#2849490)를 참조하세요.

  • 사용자 지정 Generic 형식을 만들 때 고유한 형식 제약 조건을 정의할 수 있으며 이러한 제약 조건은 Generic 프로그래밍의 많은 기능을 제공합니다.
  • Hashable과 같은 추상 개념은 구체적인 type이 아니라 개념적 특성 측면에서 type을 특성화합니다.

📍 Type Constraint Syntax

  • type 매개변수 목록의 일부로 콜론으로 구분된 type 매개변수 이름 뒤에 단일 class 또는 Protocol 제한조건을 배치하여 type 제한조건을 작성합니다.
  • Generic 함수의 형식 제약 조건에 대한 기본 구문은 다음과 같습니다(단, 구문은 Generic 형식에 대해 동일함).
    func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
        // function body goes here
    }
  • 위의 가상 함수에는 두 가지 type 매개변수가 있습니다.
  • 첫 번째 type 매개변수 T에는 T가 SomeClass의 하위 class가 되어야 하는 type 제약 조건이 있습니다.
  • 두 번째 type 매개변수 U에는 U가 SomeProtocol Protocol을 준수해야 하는 type 제약 조건이 있습니다.

📍 Type Constraints in Action

  • 다음은 findIndex(ofString:in:)라는 nongeneric 함수로, 찾을 문자열 값과 찾을 문자열 값 배열이 제공됩니다.
  • findIndex(ofString:in:) 함수는 optional Int 값을 반환합니다.
  • 이 값은 발견된 경우 배열에서 첫 번째로 일치하는 문자열의 인덱스가 되고, 문자열을 찾을 수 없는 경우 nil이 됩니다.
    func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
        for (index, value) in array.enumerated() {
            if value == valueToFind {
                return index
            }
        }
        return nil
    }
  • findIndex(ofString:in:) 함수는 문자열 배열에서 문자열 값을 찾는 데 사용할 수 있습니다.
    let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
    if let foundIndex = findIndex(ofString: "llama", in: strings) {
        print("The index of llama is \(foundIndex)")
    }
    // Prints "The index of llama is 2"
  • 그러나 배열에서 값의 인덱스를 찾는 원리는 문자열에만 유용하지 않습니다.
  • 문자열에 대한 언급을 일부 type T의 값으로 대체하여 Generic 함수와 동일한 기능을 작성할 수 있습니다.

  • 다음은 findIndex(of:in:)라고 하는 findIndex(ofString:in:)의 Generic 버전이 작성될 것으로 예상하는 방법입니다.
  • 이 함수의 반환 type은 여전히 Int?입니다. 함수가 배열의 optional 값이 아니라 optional 인덱스 번호를 반환하기 때문입니다.
  • 그러나 경고합니다. 이 함수는 예제 뒤에 설명된 이유로 컴파일되지 않습니다.
    func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
        for (index, value) in array.enumerated() {
            if value == valueToFind {
                return index
            }
        }
        return nil
    }
  • 이 함수는 위에서 작성한 대로 컴파일되지 않습니다. 문제는 "if value == valueToFind"인 등식 검사에 있습니다.
  • Swift의 모든 type을 equal 연산자(==)와 비교할 수 있는 것은 아닙니다.
  • 예를 들어 복잡한 데이터 모델을 나타내기 위해 고유한 class 또는 Struct를 생성하는 경우 해당 class 또는 Struct에 대한 "equal"의 의미는 Swift가 추측할 수 있는 것이 아닙니다.
  • 이 때문에 이 코드가 가능한 모든 type T에 대해 작동한다고 보장할 수 없으며 코드를 컴파일하려고 할 때 적절한 오류가 보고됩니다.

  • 그러나 모든 것이 손실된 것은 아닙니다.
  • Swift 표준 라이브러리는 Equatable이라는 Protocol을 정의합니다.
  • 이 Protocol은 해당 type의 두 값을 비교하기 위해 equal 연산자(==) 및 nonequal 연산자(!=)를 구현하는 type이 필요합니다.
  • Swift의 모든 표준 type은 Equatable Protocol을 자동으로 지원합니다.

  • Equatable인 모든 type은 findIndex(of:in:) 함수와 함께 안전하게 사용할 수 있습니다, 등호 연산자를 지원하기 때문입니다.
  • 이 사실을 표현하기 위해 함수를 정의할 때 type 매개변수 정의의 일부로 Equatable의 type 제약 조건을 작성합니다.
    func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
        for (index, value) in array.enumerated() {
            if value == valueToFind {
                return index
            }
        }
        return nil
    }
  • findIndex(of:in:)에 대한 단일 type 매개변수는 T: Equatable로 작성되며, 이는 "Equatable Protocol을 준수하는 모든 type T"를 의미합니다.
  • findIndex(of:in:) 함수는 이제 성공적으로 컴파일되며 Double 또는 String과 같은 Equatable type과 함께 사용할 수 있습니다.
    let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
    // doubleIndex is an optional Int with no value, because 9.3 isn't in the array
    let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
    // stringIndex is an optional Int containing a value of 2

📌 Associated Types

  • Protocol을 정의할 때 Protocol 정의의 일부로 하나 이상의 Associated type을 선언하는 것이 때때로 유용합니다.
  • 연결된 type은 Protocol의 일부로 사용되는 type에 자리 표시자 이름을 제공합니다.
  • Associated type에 사용할 실제 type은 Protocol이 채택될 때까지 지정되지 않습니다.
  • 연관된 type은 associatedtype 키워드로 지정됩니다.

📍 Associated Types in Action

  • 다음은 Item이라는 Associated type을 선언하는 Container라는 Protocol의 예입니다.
    protocol Container {
        associatedtype Item
        mutating func append(_ item: Item)
        var count: Int { get }
        subscript(i: Int) -> Item { get }
    }
  • Container Protocol은 모든 Container가 제공해야 하는 세 가지 필수 기능을 정의합니다.
    - append(_:) 메서드를 사용하여 Container에 새 items을 추가할 수 있어야 합니다.
    - Int 값을 반환하는 count 속성을 통해 Container의 items 수에 액세스할 수 있어야 합니다.
    - Int 인덱스 값을 사용하는 아래 첨자를 사용하여 Container의 각 items을 검색할 수 있어야 합니다.

  • 이 Protocol은 Container의 items을 저장하는 방법이나 허용되는 type을 지정하지 않습니다.
  • Protocol은 Container로 간주되기 위해 모든 type이 제공해야 하는 3비트 기능만 지정합니다.
  • 준수 type은 이 세 가지 요구 사항을 충족하는 한 추가 기능을 제공할 수 있습니다.

  • Container Protocol을 준수하는 모든 type은 저장하는 값 type을 지정할 수 있어야 합니다.
  • 특히 올바른 type의 items만 Container에 추가되도록 해야 하며 아래 첨자가 반환하는 items type에 대해 명확해야 합니다.

  • 이러한 요구 사항을 정의하기 위해 Container Protocol은 특정 Container에 대한 type이 무엇인지 모른 채 Container가 보유할 요소 type을 참조하는 방법이 필요합니다.
  • Container Protocol은 append(_:) 메서드에 전달되는 모든 값이 Container의 요소 type과 동일한 type을 가져야 하고 Container의 첨자가 반환하는 값이 Container의 요소 type과 동일한 type이 되도록 지정해야 합니다.

  • 이를 달성하기 위해 Container Protocol은 연관 type items으로 작성된 items이라는 연관 type을 선언합니다.
  • Protocol은 items이 무엇인지 정의하지 않습니다.
  • 해당 정보는 준수하는 type이 제공할 수 있도록 남겨둡니다.
  • 그럼에도 불구하고 Item 별칭은 Container의 items type을 참조하고 append(_:) 메서드 및 아래 첨자와 함께 사용할 type을 정의하여 Container의 예상 동작이 적용되도록 하는 방법을 제공합니다.

  • 다음은 Container Protocol을 준수하도록 조정된 위의 Generic type에서 Generic이 아닌 IntStack type의 버전입니다.
    struct IntStack: Container {
        // original IntStack implementation
        var items: [Int] = []
        mutating func push(_ item: Int) {
            items.append(item)
        }
        mutating func pop() -> Int {
            return items.removeLast()
        }
        // conformance to the Container protocol
        typealias Item = Int
        mutating func append(_ item: Int) {
            self.push(item)
        }
        var count: Int {
            return items.count
        }
        subscript(i: Int) -> Int {
            return items[i]
        }
    }
  • IntStack type은 Container Protocol의 세 가지 요구 사항을 모두 구현하며 각 경우에 이러한 요구 사항을 충족하기 위해 IntStack type의 기존 기능의 일부를 래핑합니다.

  • 또한 IntStack은 이 Container 구현을 위해 사용할 적절한 Item이 Int type임을 지정합니다.
  • Typealias Item = Int의 정의는 이 Container Protocol 구현을 위해 Item의 추상 type을 Int의 구체적인 type으로 바꿉니다.

  • Swift의 type 추론 덕분에 실제로 IntStack 정의의 일부로 구체적인 Int items을 선언할 필요가 없습니다.
  • IntStack은 Container Protocol의 모든 요구 사항을 준수하기 때문에 Swift는 단순히 append(_:) 메서드의 items 매개변수 type과 첨자의 반환 type을 보고 사용할 적절한 items을 유추할 수 있습니다.
  • 실제로 위의 코드에서 typealias Item = Int 줄을 삭제해도 Item에 어떤 type을 사용해야 하는지 명확하기 때문에 모든 것이 여전히 작동합니다.

  • Generic Stack type이 Container Protocol을 따르도록 만들 수도 있습니다.
    struct Stack<Element>: Container {
        // original Stack<Element> implementation
        var items: [Element] = []
        mutating func push(_ item: Element) {
            items.append(item)
        }
        mutating func pop() -> Element {
            return items.removeLast()
        }
        // conformance to the Container protocol
        mutating func append(_ item: Element) {
            self.push(item)
        }
        var count: Int {
            return items.count
        }
        subscript(i: Int) -> Element {
            return items[i]
        }
    }
  • 이번에는 type 매개변수 Element를 append(_:) 메서드의 item 매개변수 type과 첨자의 반환 type으로 사용합니다.
  • 따라서 Swift는 Element가 이 특정 Container의 Item으로 사용하기에 적절한 type이라고 추론할 수 있습니다.

📍 Extending an Existing Type to Specify an Associated Type

  • extension으로 Protocol 적합성 추가에 설명된 대로 기존 type을 extension하여 Protocol에 적합성을 추가할 수 있습니다.
  • 여기에는 Associated type이 있는 Protocol이 포함됩니다.

  • Swift의 Array type은 이미 append(_:) 메서드, count 속성 및 요소를 검색하기 위한 Int 인덱스가 있는 첨자를 제공합니다.
  • 이 세 가지 기능은 Container Protocol의 요구 사항과 일치합니다.
  • 즉, Array가 Protocol을 채택한다고 선언하기만 하면 Container Protocol을 준수하도록 Array를 extension할 수 있습니다.
  • extension으로 Protocol 채택 선언에 설명된 대로 빈 extension으로 이 작업을 수행합니다.
    extension Array: Container {}
  • Array의 기존 append(_:) 메서드와 아래 첨자는 위의 Generic Stack type과 마찬가지로 Swift가 Item에 사용할 적절한 type을 유추할 수 있도록 합니다.
  • 이 extension을 정의한 후에는 모든 배열을 Container로 사용할 수 있습니다.

📍 Adding Constraints to an Associated Type

  • 형식 제약 조건을 Protocol의 연결된 형식에 추가하여 준수하는 형식이 이러한 제약 조건을 충족하도록 요구할 수 있습니다.
  • 예를 들어 다음 코드는 Container의 items이 동일해야 하는 Container 버전을 정의합니다.
    protocol Container {
        associatedtype Item: Equatable
        mutating func append(_ item: Item)
        var count: Int { get }
        subscript(i: Int) -> Item { get }
    }
  • 이 버전의 Container를 준수하려면 Container의 item type이 Equatable Protocol을 준수해야 합니다.

📍 Using a Protocol in Its Associated Type’s Constraints

  • Protocol은 자체 요구 사항의 일부로 나타날 수 있습니다.
  • 예를 들어, 여기에 suffix(_:) 메서드의 요구 사항을 추가하여 Container Protocol을 개선하는 Protocol이 있습니다.
  • suffix(_:) 메서드는 Container 끝에서 주어진 수의 요소를 반환하여 Suffix type의 인스턴스에 저장합니다.
    protocol SuffixableContainer: Container {
        associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
        func suffix(_ size: Int) -> Suffix
    }
  • 이 Protocol에서 Suffix는 위의 Container 예제의 item type과 같은 Associated type입니다.
  • Suffix에는 두 가지 제약 조건이 있습니다. SuffixableContainer Protocol(현재 정의 중인 Protocol)을 준수해야 하고 items type은 Container의 items type과 동일해야 합니다.
  • items에 대한 제약 조건은 아래의 Generic Where 절이 있는 Associated type에서 설명하는 Generic where 절입니다.

  • 다음은 SuffixableContainer Protocol에 대한 준수를 추가하는 위의 Generic type에서 Stack type의 extension입니다.
    extension Stack: SuffixableContainer {
        func suffix(_ size: Int) -> Stack {
            var result = Stack()
            for index in (count-size)..<count {
                result.append(self[index])
            }
            return result
        }
        // Inferred that Suffix is Stack.
    }
    var StackOfInts = Stack<Int>()
    StackOfInts.append(10)
    StackOfInts.append(20)
    StackOfInts.append(30)
    let suffix = StackOfInts.suffix(2)
    // suffix contains 20 and 30
  • 위의 예에서 Stack의 접미사 Associated type도 Stack이므로 Stack에 대한 접미사 작업은 다른 Stack을 반환합니다.
  • 또는 SuffixableContainer를 준수하는 type은 자체와 다른 Suffix type을 가질 수 있습니다.
  • 즉, 접미사 작업이 다른 type을 반환할 수 있습니다.
  • 예를 들어, 다음은 IntStack 대신 Stack\를 접미사 type으로 사용하여 SuffixableContainer 준수를 추가하는 비Generic IntStack type에 대한 extension입니다.
    extension IntStack: SuffixableContainer {
        func suffix(_ size: Int) -> Stack<Int> {
            var result = Stack<Int>()
            for index in (count-size)..<count {
                result.append(self[index])
            }
            return result
        }
        // Inferred that Suffix is Stack<Int>.
    }

📌 Generic Where Clauses

  • 형식 제약 조건에 설명된 대로 형식 제약 조건을 사용하면 Generic 함수, 아래 첨자 또는 형식과 연결된 형식 매개 변수에 대한 요구 사항을 정의할 수 있습니다.

  • 연결된 type에 대한 요구 사항을 정의하는 것도 유용할 수 있습니다.

  • Generic where 절을 정의하여 이를 수행합니다.

  • Generic where 절을 사용하면 연결된 형식이 특정 Protocol을 준수해야 하거나 특정 형식 매개변수와 연결된 형식이 같아야 한다고 요구할 수 있습니다.

  • Generic where 절은 where 키워드로 시작하고 그 뒤에 연결된 형식에 대한 제약 조건 또는 형식과 연결된 형식 간의 동등 관계가 옵니다.

  • 형식이나 함수 본문의 여는 중괄호 바로 앞에 Generic where 절을 작성합니다.

  • 아래 예는 두 개의 Container 인스턴스가 같은 순서로 같은 items을 포함하는지 확인하는 allItemsMatch라는 Generic 함수를 정의합니다.

  • 이 함수는 모든 items이 일치하면 부울 값 true를 반환하고 일치하지 않으면 false 값을 반환합니다.

  • 검사할 두 개의 Container가 동일한 type의 Container일 필요는 없지만(일 수도 있지만) 동일한 type의 items을 보유해야 합니다.

  • 이 요구 사항은 형식 제약 조건과 Generic where 절의 조합을 통해 표현됩니다.

    func allItemsMatch<C1: Container, C2: Container>
        (_ someContainer: C1, _ anotherContainer: C2) -> Bool
        where C1.Item == C2.Item, C1.Item: Equatable {
    
            // Check that both containers contain the same number of items.
            if someContainer.count != anotherContainer.count {
                return false
            }
    
            // Check each pair of items to see if they're equivalent.
            for i in 0..<someContainer.count {
                if someContainer[i] != anotherContainer[i] {
                    return false
                }
            }
    
            // All items match, so return true.
            return true
    }
  • 이 함수는 someContainer와 anotherContainer라는 두 개의 인수를 취합니다.

  • someContainer 인수는 C1 type이고 anotherContainer 인수는 C2 type입니다.

  • C1과 C2는 둘 다 함수가 호출될 때 결정되는 두 가지 Container type에 대한 type 매개변수입니다.

  • 함수의 두 type 매개변수에 다음 요구사항이 적용됩니다.

    • C1은 Container Protocol(C1: Container로 작성)을 준수해야 합니다.
    • C2는 또한 Container Protocol(C2: Container로 작성)을 준수해야 합니다.
    • C1의 item은 C2의 item과 동일해야 합니다(C1.Item == C2.Item으로 작성).
    • C1에 대한 item은 Equatable Protocol(C1.Item: Equatable로 작성됨)을 준수해야 합니다.
  • 첫 번째 및 두 번째 요구 사항은 함수의 type 매개 변수 목록에 정의되어 있고 세 번째 및 네 번째 요구 사항은 함수의 Generic where 절에 정의되어 있습니다.

  • 이러한 요구 사항은 다음을 의미합니다.
    - someContainer는 C1 type의 Container입니다.
    - anotherContainer는 C2 type의 Container입니다.
    - someContainer 및 anotherContainer는 동일한 type의 item을 포함합니다.
    - someContainer의 items은 같지 않음 연산자(!=)를 사용하여 서로 다른지 확인할 수 있습니다.

  • 세 번째와 네 번째 요구 사항이 결합되어 otherContainer의 item도 someContainer의 item과 정확히 같은 type이기 때문에 != 연산자로 확인할 수 있음을 의미합니다.

  • 이러한 요구 사항을 통해 allItemsMatch(::) 함수는 Container type이 다른 경우에도 두 Container를 비교할 수 있습니다.

  • allItemsMatch(::) 함수는 두 Container에 동일한 수의 item이 포함되어 있는지 확인하는 것으로 시작합니다.

  • item 수가 다른 경우 일치시킬 방법이 없으며 함수는 false를 반환합니다.

  • 이 검사를 수행한 후 함수는 for-in 루프와 반개방 범위 연산자(..<)를 사용하여 someContainer의 모든 items을 반복합니다.

  • 각 item에 대해 함수는 someContainer의 items이 anotherContainer의 해당 items과 같지 않은지 확인합니다.

  • 두 item이 같지 않으면 두 Container가 일치하지 않고 함수는 false를 반환합니다.

  • 불일치를 찾지 않고 루프가 완료되면 두 Container가 일치하고 함수는 true를 반환합니다.

  • allItemsMatch(::) 함수가 작동하는 모습은 다음과 같습니다.

    var StackOfStrings = Stack<String>()
    StackOfStrings.push("uno")
    StackOfStrings.push("dos")
    StackOfStrings.push("tres")
    
    var arrayOfStrings = ["uno", "dos", "tres"]
    
    if allItemsMatch(StackOfStrings, arrayOfStrings) {
        print("All items match.")
    } else {
        print("Not all items match.")
    }
    // Prints "All items match."
  • 위의 예에서는 String 값을 저장할 Stack 인스턴스를 만들고 3개의 문자열을 Stack에 push합니다.

  • 이 예제에서는 또한 Stack과 동일한 세 개의 문자열을 포함하는 배열 리터럴로 초기화된 Array 인스턴스를 만듭니다.

  • Stack과 배열은 다른 type이지만 둘 다 Container Protocol을 준수하며 둘 다 동일한 type의 값을 포함합니다.

  • 따라서 이 두 Container를 인수로 사용하여 allItemsMatch(::) 함수를 호출할 수 있습니다.

  • 위의 예에서 allItemsMatch(::) 함수는 두 Container의 모든 item이 일치한다고 올바르게 보고합니다.

📌 Extensions with a Generic Where Clause

  • extension의 일부로 Generic where 절을 사용할 수도 있습니다.

  • 아래 예제는 이전 예제의 Generic Stack Struct를 extension하여 isTop(_:) 메서드를 추가합니다.

    extension Stack where Element: Equatable {
        func isTop(_ item: Element) -> Bool {
            guard let topItem = items.last else {
                return false
            }
            return topItem == item
        }
    }
  • 이 새로운 isTop(_:) 메서드는 먼저 Stack이 비어 있지 않은지 확인한 다음 지정된 items을 Stack의 최상위 item과 비교합니다.

  • Generic where 절 없이 이 작업을 수행하려고 하면 문제가 발생합니다.

  • isTop(_:)의 구현은 == 연산자를 사용하지만 Stack의 정의는 item이 동일할 필요가 없으므로 == 연산자를 사용하면 컴파일 타임 오류가 발생합니다.

  • Generic where 절을 사용하면 Stack의 item이 동일할 때만 extension이 isTop(_:) 메서드를 추가하도록 extension에 새 요구 사항을 추가할 수 있습니다.

  • isTop(_:) 메서드가 작동하는 모습은 다음과 같습니다.

    if StackOfStrings.isTop("tres") {
        print("Top element is tres.")
    } else {
        print("Top element is something else.")
    }
    // Prints "Top element is tres."
  • 요소가 동일하지 않은 Stack에서 isTop(_:) 메서드를 호출하려고 하면 컴파일 시간 오류가 발생합니다.

    ```swift
    struct NotEquatable { }
    var notEquatableStack = Stack<NotEquatable>()
    let notEquatableValue = NotEquatable()
    notEquatableStack.push(notEquatableValue)
    notEquatableStack.isTop(notEquatableValue)  // Error
    ```

  • Protocol extension과 함께 Generic where 절을 사용할 수 있습니다. 아래 예제는 이전 예제의 Container Protocol을 extension하여 startsWith(_:) 메서드를 추가합니다.

    extension Container where Item: Equatable {
        func startsWith(_ item: Item) -> Bool {
            return count >= 1 && self[0] == item
        }
    }
  • startsWith(_:) 메서드는 먼저 Container에 하나 이상의 item이 있는지 확인한 다음 Container의 첫 번째 item이 지정된 item과 일치하는지 확인합니다.

  • 이 새로운 startsWith(_:) 메서드는 Container의 item이 동일하다면 위에서 사용된 Stack 및 배열을 포함하여 Container Protocol을 준수하는 모든 type과 함께 사용할 수 있습니다.

    if [9, 9, 9].startsWith(42) {
        print("Starts with 42.")
    } else {
        print("Starts with something else.")
    }
    // Prints "Starts with something else."
  • 위의 예에서 Generic where 절은 Item이 Protocol을 준수하도록 요구하지만 Item이 특정 type이 되도록 요구하는 Generic where 절을 작성할 수도 있습니다.

  • 예를 들어

    extension Container where Item == Double {
        func average() -> Double {
            var sum = 0.0
            for index in 0..<count {
                sum += self[index]
            }
            return sum / Double(count)
        }
    }
    print([1260.0, 1200.0, 98.6, 37.0].average())
    // Prints "648.9"
  • 이 예에서는 Item type이 Double인 Container에 average() 메서드를 추가합니다.

  • Container의 item을 반복하여 합산하고 Container 수로 나누어 평균을 계산합니다.

  • 부동 소수점 나누기를 수행할 수 있도록 카운트를 Int에서 Double로 명시적으로 변환합니다.

  • extension의 일부인 Generic where 절에 여러 요구 사항을 포함할 수 있습니다, 다른 곳에서 작성하는 Generic where 절에 대해 수행할 수 있는 것과 같습니다.

  • 목록의 각 요구 사항을 쉼표로 구분합니다.

📌 Contextual Where Clauses

  • 이미 Generic type의 컨텍스트에서 작업 중일 때 고유한 Generic type 제약 조건이 없는 선언의 일부로 Generic where 절을 작성할 수 있습니다.
  • 예를 들어 Generic 형식의 첨자 또는 Generic 형식에 대한 extension의 메서드에 Generic where 절을 작성할 수 있습니다.
  • Container Struct는 Generic적이며 아래 예제의 where 절은 Container에서 이러한 새 메서드를 사용할 수 있도록 하기 위해 충족해야 하는 형식 제약 조건을 지정합니다.
    extension Container {
        func average() -> Double where Item == Int {
            var sum = 0.0
            for index in 0..<count {
                sum += Double(self[index])
            }
            return sum / Double(count)
        }
        func endsWith(_ item: Item) -> Bool where Item: Equatable {
            return count >= 1 && self[count-1] == item
        }
    }
    let numbers = [1260, 1200, 98, 37]
    print(numbers.average())
    // Prints "648.75"
    print(numbers.endsWith(37))
    // Prints "true"
  • 이 예제는 item이 정수일 때 평균() 메서드를 Container에 추가하고 item이 동일할 때 endWith(_:) 메서드를 추가합니다.
  • 두 함수 모두 Container의 원래 선언에서 Generic item type 매개변수에 type 제약 조건을 추가하는 Generic where 절을 포함합니다.
  • 상황에 맞는 where 절을 사용하지 않고 이 코드를 작성하려면 Generic where 절마다 하나씩 두 개의 extension을 작성합니다.
  • 위의 예와 아래의 예는 동일한 동작을 합니다.
    extension Container where Item == Int {
        func average() -> Double {
            var sum = 0.0
            for index in 0..<count {
                sum += Double(self[index])
            }
            return sum / Double(count)
        }
    }
    extension Container where Item: Equatable {
        func endsWith(_ item: Item) -> Bool {
            return count >= 1 && self[count-1] == item
        }
    }
  • 상황에 맞는 where 절을 사용하는 이 예제 버전에서는 각 메서드의 Generic where 절이 해당 메서드를 사용 가능하게 하기 위해 충족해야 하는 요구 사항을 명시하기 때문에 average() 및 endsWith(_:) 구현이 모두 동일한 extension에 있습니다.
  • 이러한 요구 사항을 extension의 Generic where 절로 이동하면 동일한 상황에서 메서드를 사용할 수 있지만 요구 사항당 하나의 extension이 필요합니다.

📌 Associated Types with a Generic Where Clause

  • 연결된 type에 Generic where 절을 포함할 수 있습니다.

  • 예를 들어, Sequence Protocol이 표준 라이브러리에서 사용하는 것과 같은 iterator를 포함하는 Container 버전을 만들고 싶다고 가정합니다.

  • 작성 방법은 다음과 같습니다.

    ```swift
    protocol Container {
        associatedtype Item
        mutating func append(_ item: Item)
        var count: Int { get }
        subscript(i: Int) -> Item { get }
    
        associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
        func makeIterator() -> Iterator
    }
    ```

  • Iterator의 Generic where 절은 iterator의 type에 관계없이 iterator가 Container의 item과 동일한 item type의 요소를 순회해야 한다고 요구합니다. makeIterator() 함수는 Container의 iterator에 대한 액세스를 제공합니다.

  • 다른 Protocol에서 상속하는 Protocol의 경우 Protocol 선언에 Generic where 절을 포함하여 상속된 Associated type에 제약 조건을 추가합니다.

  • 예를 들어 다음 코드는 Item이 Comparable을 준수해야 하는 ComparableContainer Protocol을 선언합니다.

    protocol ComparableContainer: Container where Item: Comparable { }

📌 Generic Subscripts

  • 첨자는 Generic일 수 있으며 Generic where 절을 포함할 수 있습니다.
  • 아래 첨자 뒤 꺾쇠 괄호 안에 자리 표시자 type 이름을 쓰고 아래 첨자 본문의 여는 중괄호 바로 앞에 Generic where 절을 작성합니다.
    extension Container {
        subscript<Indices: Sequence>(indices: Indices) -> [Item]
            where Indices.Iterator.Element == Int {
                var result: [Item] = []
                for index in indices {
                    result.append(self[index])
                }
                return result
        }
    }
  • Container Protocol에 대한 이 extension은 인덱스 시퀀스를 사용하고 주어진 각 인덱스의 item을 포함하는 배열을 반환하는 첨자를 추가합니다.
  • 이 Generic 첨자는 다음과 같이 제한됩니다.
    • 꺾쇠 괄호 안의 Generic 매개변수 인덱스는 표준 라이브러리의 시퀀스 Protocol을 준수하는 type이어야 합니다.
    • 아래 첨자는 해당 Indices type의 인스턴스인 단일 매개변수 indices를 사용합니다.
    • Generic where 절에서는 시퀀스의 반복자가 Int type의 요소를 순회해야 합니다.
    • 이렇게 하면 시퀀스의 인덱스가 Container에 사용되는 인덱스와 동일한 type이 됩니다.
  • 종합하면 이러한 제약 조건은 indexs 매개변수에 전달된 값이 정수 시퀀스임을 의미합니다.
profile
I Am Groot

0개의 댓글