Swift/Swift 공식문서

[Swift] 공식문서 23 - Gernerics (제네릭)

말차프라푸치노 2021. 11. 5. 22:39

 

 

Generics — The Swift Programming Language (Swift 5.5)

Generics Generic code enables you to write flexible, reusable functions and types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted manner. Gener

docs.swift.org

1차 수정 (2022 / 03 / 14) : 의역, 오타 및 스타일 수정

 

 

 

Generics (제네릭)

제네릭 코드는 사용자가 "사용자가 정의한 요구사항에 따라 모든 타입에서 사용할 수 있는 유연하고 재사용 가능한 함수와 코드"를 작성할 수 있게 해줍니다. 중복을 피하고 의도를 명확히 표현하는 추상화된 방식으로 코드를 작성할 수 있습니다.

 

제네릭은 스위프트의 가장 강력한 기능 중 하나입니다. 많은 Swift 표준 라이브러리가 제네릭 코드로 작성되었습니다. 사실 알지 모르겠지만 앞선 공식 문서에서 제네릭을 사용했습니다.

예를 들어 Swift 의 배열과 사전 타입은 모두 제네릭 컬렉션(generic collections) 입니다. Int 값을 포함하는 배열을 만들거나 String 값을 포함하는 배열을 만들 수 있습니다. 혹은 Swift에서 만들 수 있는 다른 타입의 배열을 만들 수 있습니다.

마찬가지로 특정 타입의 값을 저장하기위해 사전을 생성할 수 있습니다. 해당 타입의 값에는 제한이 없습니다.

 

 

 

 

 

The Problem That Generics Solve (제네릭이 해결하는 문제)

여기 제네릭이 아닌 기본적인 함수 swapTwoInts(_:_:) 가 있습니다.

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

이 함수는 inout 파라미터를 사용해 a, b 의 값을 바꿉니다.

 

swapTwoInts(_:_:) 함수는 b의 원래 값을 a로, 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 값을 바꾸고 싶다면, 다른 함수를 작성해야 합니다.

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
}

swapTwoStrings(_:_:) 와 swapTwoDoubles(_:_:) 함수가 swapTwoInts(_:_:)함수랑 본문이 똑같은 걸 확인할 수 있습니다. 유일한 차이점은 함수가 받는 값의 타입입니다.

 

모든 타입에 대해 두 값을 교환하는 단일 함수를 작성하는 것이 더 유용하고 유연합니다. 제네릭 코드를 작성하면 이러한 함수를 작성할 수 있습니다.

 

세 개의 모든 함수에서 a와 b의 타입은 반드시 같아야 합니다. a와 b 가 같은 타입이 아니면 그 값을 바꾸는 것은 불가능합니다. Swift는 type-safe 언어입니다. String 타입의 변수와 Double 타입의 변수가 바뀌는 것을 허용하지 않습니다. 이를 시도할 시 컴파일-타임 에러가 발생합니다.

 

 

 

 

 

 

Generic Functions (제네릭 함수)

제네릭 함수(Generic Functions) 는 모든 타입으로 작성할 수 있습니다. 여기 swapTwoInts(_:_:) 의 제네릭 버전 함수 swapTwoValues(_:_:) 가 있습니다.

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

swapTwoValues(_:_:) 함수 본문은 swapTwoInts(_:_:) 함수와 동일합니다. 그러나 첫번째 줄이 살짝 다릅니다.

func swapTwoInts(_ a: inout Int, _ b: inout Int)
func swapTwoValues<T>(_ a: inout T, _ b: inout T)

제네릭 버전의 함수는 실제 타입의 이름대신 placeholder 타입 이름을 사용합니다. (이 경우에는 T) placeholder 타입 이름은 T가 무엇이 되야하는지 나타내지 않습니다. 하지만 a와 b가 모두 같은 타입 T를 가져야 합니다.  T 대신 실제로 사용할 타입은 swapTwoValues(_:_:)  함수 호출시마다 결정됩니다.

 

제네릭 함수와 제네릭이 아닌 함수와의 다른 차이점은 제네릭 함수는 swapTwoValues(_:_:) 이름 뒤에 타입 이름 T 가 옵니다. <> 에 넣어 <T> 형식으로 말입니다. 괄호는 Swift에게 T가 함수의 placeholder 타입 이름이라는 것을 말해줍니다. T 가 placeholder 이기 때문에, Swift는 T라는 타입을 찾지 않습니다.

 

swapTwoValues(_:_:) 함수는 swapTwoInts 와 같은 방식으로 두 호출할 수 있습니다. 단 두 값이 서로 동일한 타입이여야 모든 타입의 값을 전달 할 수 있습니다. swapTwoValues(_:_:) 함수가 호출될 때 마다, T에 사용할 타입은 함수에 전달된 값의 타입에서 유추됩니다.

 

아래 예제에서 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 표준 라이브러리의 일부인 swap이라는 제네릭 함수를 받아와 앱에서 자동으로 사용할 수 있게 해줍니다. 사용자 코드에서 swapTwoValues(_:_:)  함수의 동작이 필요한 경우, 굳이 구현할 필요 없이 Swift의 swap(_:_:) 함수를 사용할 수 있습니다.

 

 

 

 

Type Parameters (타입 파라미터)

swapTwoValues(_:_:) 예제에서, placeholder 타입 T는 타입 파라미터의 예시 입니다. 타입파라미터는 placeholder 타입을 명시하고 이름을 짓습니다. 함수 바로 뒤에 <> 을 사용해 작성합니다. (<T> 처럼)

 

타입 파리미터를 한 번 명시하면, 함수의 파라미터 타입을 정의할 때 사용할 수 있습니다. 혹은 함수의 리턴 타입을 정의하거나 함수 본문 내에서 타입 annotation 으로 사용됩니다. 각 케이스에서 함수가 호출될 때, 타입파라미터는 실제 타입으로 대체됩니다.

 

<> 괄호 안에 , 를 사용해서 하나 이상의 타입 파라미터를 제공할 수 있습니다.

 

 

 

 

 

Naming Type Parameters (타입 파라미터 이름 짓기)

대부분의 경우 타입 파라미터는 설명적인 이름을 가집니다. Dictionary<Key,Value> 의 Key, Value 와 Array<Element> 의 Element 처럼 사용자에게 타입 파라미터와 제네릭 타입(혹은 함수) 사이의 관계를 말해줍니다.

그러나 둘 사이에 의미있는 관계가 없다면, 일반적으로 하나의 문자를 써서 이름을 짓습니다. T, U, V 처럼 말입니다.

<T>, <U>, <V>

 

타입 파라미터에게 항상 카멜 케이스(Camel case) 이름을 사용해 값이 아니라 type을 위한 placeholder 라는 것을 나타냅니다.

 

 

 

 

 

Generic Types (제네릭 타입)

제네릭 함수에 더해, Swift는 사용자에게 제네릭 타입 정의를 가능하게 해줍니다. 이는 모든 타입에서 사용할 수 있는 사용자 정의 클래스, 구조체, 열거형을 말합니다. Array와 Dictionary와 유사한 방식입니다.

 

이 챕터에서는 Stack 이라는 컬렉션 타입을 어떻게 작성하는지 보여줍니다. 스택은 배열과 비슷한 정렬된 값 집합입니다. 하지만 Swift의 배열 타입보다 더 제한된 연산 집합을 가집니다. 배열은 모든 위치에서 새 항목을 삽입하고 제거할 수 있습니다. 그러나 스택에서는 새 항목을 컬렉션 끝에만 추가할 수 있습니다 (push 라고도 합니다). 마찬가지로 스택에서는 항목을 컬렉션의 끝에서만 제거할 수 있습니다 (popping 이라고도 합니다).

 

Stack 의 개념은 UINavigationController 클래스에서 "네비게이션 계층구도에서 뷰 컨트롤러를 모델링하기 위해" 사용됩니다. UINavigationController 클래스의 pushViewController(_:animated:) 메소드를 호출해 뷰 컨트롤러를 네비이게이션 스택에 추가합니다. popViewControllerAnimated(_:) 메소드를 사용해서 네비게이션 스택에서 뷰 컨트롤러를 제거합니다. 스택은 컬렉션은 관리하기 위해 엄격한 "last in, first out" 접근 방식이 필요할 때마다 유용합니다.

 

아래 그림은 어떻게 스택이 작동하는지 보여줍니다.

출처 : Swift 공식 홈체이지

  1. 3 개의 값이 스택에 있습니다
  2. 4 번째 값이 push 되어 스택의 맨 위에 옵니다.
  3. 이제 스택은 4개의 값을 가집니다. 가장 최근의 값이 맨 위에 있습니다.
  4. 스택의 맨 위 항목이 pop 됩니다.
  5. 값이 pop 된 후로 스택은 다시 3 개의 값을 가집니다.

  

아래 코드는 제네릭 아닌 버전의 스택입니다. Int 값을 사용하는 경우입니다.

struct IntStack {
    var items: [Int] = []
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
}

구조체는 값을 스택에 저장하기 위해 배열 프로퍼티 items 를 가집니다. push 와 pop 이라는 두 개의 메소드를 가집니다. 이를 통해 스택에서 항목을 push 하고 pop 합니다. 메소드들이 mutating으로 선언되었는데 구조체의 items 배열을 수정해야하기 때문입니다.

 

IntStack 타입은 Int 값만을 사용해야 합니다. 모든 타입의 값을 관리하기 위해 제네릭 Stack 클래스를 정의하는 것이 더 유용할 것입니다.

 

여기 제네릭 버전의 Stack 이 있습니다.

struct Stack<Element> {
    var items: [Element] = []
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

Stack 의 제네릭 버전이 기본적으로 제네릭 아닌 버전과 동일합니다. 하지만 Int 라는 실제 타입 대신 Element 라는 타입 파라미터가 사용됐습니다. 

 

Element 는 나중에 제공할 타입에 대한 placeholder 이름을 정의합니다. 미래에 올 타입은 구조체의 정의내부에서 Element를 참조합니다. 이 경우 Element는 3 곳에서 placeholder 로 사용됩니다.

  • Element 타입의 값을 가지는 빈 배열로 초기화 되는, item 프로퍼티를 생성하기 위해
  • 반드시 Element 타입을 가져야하는 item 을 단일 파라미터로 가지는 push 메소드를 명시하기 위해
  • pop 메소드에 의해 리턴되는 값이 Element 타입의 값이라는 것을 명시하기 위해

제네릭 타입이기 때문에, Stack은 Swift의 모든 유효한 타입에 대해 Stack을 만들 수 있습니다.

 

<> 괄호 내에 스택에 저장할 타입을 작성함으로써 새로운 Stack 인스턴스를 생성할 수 있습니다.

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// the stack now contains 4 strings

 

아래 그림은 stackOfStrings 가 어떻게 동작하는지 보여줍니다.

Swift 공식 홈페이지

 

스택에서 값을 Popping 하면, 맨 위에 있는 값 "cuatro" 가 제거되고 리턴됩니다.

let fromTheTop = stackOfStrings.pop()
// fromTheTop is equal to "cuatro", and the stack now contains 3 strings

Swift 공식 홈페이지

 

 

 

Extending a Generic Type (제네릭 타입 확장)

제네릭 타입을 확장할 때, 확장 정의의 일부로 타입 파라미터를 제공하지 않습니다. 대신에 원본 타입 정의로부터의 타입 파라미터 목록은 확장의 본문에서 사용할 수 있습니다. 원본 타입 파라미터 이름은 원래 정의에서 타입 파라미터를 참조하는데 사용됩니다.

 

아래 예제는 제네릭 Stack 타입을 읽기-전용 연산 프로퍼티(read-only computed property) topItem으로 확장합니다. 스택에서 popping 없이 맨 위 항목을 리턴합니다.

extension Stack {
    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
}

topItem 프로퍼티는 Element 타입의 옵셔널 값을 리턴합니다. 만약 스택이 비어있으면 nil을 리턴하고, 비어있지 않으면 items 배열의 마지막 항목을 리턴합니다.

 

이 확장은 타입 파리미터 목록을 정의하지 않습니다. 대신에 Stack 타입의 기존 타입 파라미터 이름 Element 를 사용합니다.

 

topItem 연산 프로퍼티는 모든 Stack 인스턴스와 함께 사용하여 맨 위 항목을 제거하지 않고 접근하고 요청할 수 있습니다.

if let topItem = stackOfStrings.topItem {
    print("The top item on the stack is \(topItem).")
}
// Prints "The top item on the stack is tres."

제네릭 타입의 확장은 "확장된 타입의 인스턴스가 새기능을 얻기 위해 충족해야하는 요구사항"도 포함될 수 있습니다.

 

 

 

 

 

Type Constraints (타입 제약조건)

swapTwoValues(_:_:) 함수와 Stack 타입은 모든 타입에서 사용할 수 있습니다. 그러나 제네릭 함수와 제네릭 타입에 사용되는 타입에 정확한 타입 제약조건(type constraints) 를 강제하는 것이 가끔은 유용합니다. 타입 제약조건은 타입 파라미터가 반드시 특정 클래스로부터 상속받거나 특정 프로토콜 (혹은 합성 프로토콜) 을 준수하도록 명시합니다.

 

예를 들어, Swift의 사전(Dictionary) 타입은 사전의 키를 사용할 수 있는 타입에 제한을 둡니다. 사전의 키타입은 반드시 hashable 이여야 합니다. 즉, 키는 자신이 하나만 존재한다는 것을 표현해야 합니다.

사전에서 특정 키에 대한 값이 이미 포함되어 있는지 확인할 수 있도록 해당 키를 hasahble 할 수 있어야 합니다. 이 요구사항이 없으면 사전은 특정 키에 대한 값을 삽입하거나 대체하는지 알 수 없습니다. 사전에 이미 있는 지정된 키의 값을 찾을 수 없습니다.

 

이 요구사항은 사전의 키 타입에 대한 타입 제약조건에 의해 적용됩니다. Hashable 프로토콜을 반드시 준수하는 키 타입을 명시합니다. Swift의 모든 기본 타입(string, Int, Double, Bool) 은 기본적으로 hashable 입니다.

 

사용자 지정 제네릭 타입을 만들 때 사용자 지정 타입 제약조건을 정의할 수 있습니다. 이 제약조건은 제네릭 프로그래밍의 많은 기능을 구현합니다. hashable 같은 추상화 개념은 구체적인 타입보다는 개념적 특성(conceptual characteristics) 에 따라 타입을 특성화합니다.

 

 

 

Type Constraint Syntax (타입 제약조건 구문)

타입 제약조건은 단일 클래스 혹은 프로토콜 제약조건 뒤에 타입 파라미터 이름으로 작성합니다. 타입 파라미터 목록의 일부로써 말입니다. 제네릭 함수의 타입 제약조건에 대한 기본 구문이 아래에 나와 있습니다.

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // function body goes here
}

위의 가상함수는 두 개의 타입 파라미터를 가집니다. 첫 번째 파라미터 T는 SomeClass의 서브클래스여야 한다는 조건이 있습니다. 두 번째 파라미터 U는 SomeProtocol을 준수해야 하는 타입 제약조건이 있습니다.

 

 

 

Type Constraints in Action (타입 제약조건 사용)

여기 제네릭이 아닌 함수 findIndex(ofString:in:) 가 있습니다. 찾을 String 값과 찾을 Stirng 값을 가지는 배열로 구성됩니다. findIndex(ofString:in:) 함수는 옵셔널 Int 값을 리턴합니다. 배열에서 첫 번째로 일치하는 문자열이 있을 경우 index 를 리턴하고, 문자열을 찾을 수 없으면 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"

배열에서 해당하는 값의 index 를 찾는 원리는 문자열에만 쓰기 아깝습니다. 문자열 대신 T 타입 값을 사용하여 동일한 기능을 가진 제네릭 함수를 작성할 수 있습니다.

 

아래에 findIndex(ofString:in:) 함수의 제네릭 함수 findIndex(of:in:)함수가 있습니다.함수는 배열에서 옵셔널 값이 아닌 Int? 를 리턴합니다. 함수가 옵셔널 index 숫자를 리턴하기 때문입니다. 

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 의 모든 타입이 동등연산자 == 로 비교될 수 있는 것이 아닙니다. 복잡한 모델을 표현하기 위해 자신만의 클래스나 구조체를 만든다면, 해당 클래스나 구조체에 대한 "같다" 의 의미는 Swift가 추측할 수 있는 것이 아닙니다. 이 때문에 위 코드가 모든 가능한 타입 T에 대해 작동한다고 보장할 수 없습니다. 코드를 컴파일 할 때 오류가 보고됩니다.

 

그러나 모든 것을 잃은 것이 아닙니다. Swift 표준 라이브러리는 Equatable 이라는 프로토콜을 정의합니다. Equatable 은 해당 타입의 두 값을 비교하기 위해 같다는 의미의 연산자 == 와 같지 않다는 의미의 연산자 !=의 구현을 요구합니다. Swift의 모든 기본 타입은 자동적으로 Equatable 프로토콜을 지원합니다.

 

Equatable 의 모든 타입은 findIndex(of:in:) 함수와 함께 안전하게 사용할 수 있습니다. 동등 연산자를 지원하는 것이 보장되기 때문입니다. 이 사실을 표현하기 위해, 함수를 정의할 때 타입 파라미터 정의의 일부로 Equatable 타입 제약조건을 작성합니다.

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:)에 대한 단일 타입 파라미터는 T: Equatable로 작성됩니다. 이는 "Equatable 프로토콜을 준수하는 모든 타입" 을 의미합니다.

 

findIndex(of:in:) 함수는 이제 정상적을 컴파일 됩니다. Equatable 한 모든 타입과 함께 사용할 수 있습니다.

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 (관련 유형)

프로토콜을 정의할 때, 프로토콜 정의의 일부로 하나 이상의 Associated Types을 선언하는 것이 가끔 유용합니다. Associated Types 은 "프로토콜의 일부로 사용되는 타입"에 placeholder 이름을 줍니다. 해당 Associated Types을 위해 사용되는 실제 타입은 프로토콜이 채택될 때까지 명시되지 않습니다. Associated Types 은 associatedtype 키워드로 명시됩니다.

 

 

Associated Types in Action (Associated Types 사용)

아래 예제는 Container 프로토콜을 정의합니다. Associated Types Item을 정의하고 있습니다.

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

Container 프로토콜은 컨테이너가 제공해야 할 세 가지 필수 기능을 정의합니다.

  • append(_:) 메소드를 사용해 새로운 항목을 컨테이너에 추가할 수 있어야 한다
  • Int 값을 반환하는 count 프로퍼티를 통해, 컨테이너 항목 갯수에 접근이 가능해야한다
  • Int index 값을 사용하는 첨자구문을 사용해 컨테이너에 있는 각 항목을 검색할 수 있어야 한다

이 프로토콜은 어떻게 컨테이너의 항목이 저장되고 무슨 타입이 허용되는지 명시하지 않습니다. 프로토콜은 컨테이너로 간주되기 위해 모든 타입이 제공해야하는 3 가지 기능만을 명시합니다. 준수 타입은 이 3 가지 요구사항을 충족하는 추가적인 기능을 제공할 수 있습니다.

 

Container 프로토콜을 준수하는 모든 타입은 반드시 저장하는 값의 타입을 명시할 수 있어야 합니다. 구체적으로 컨테이너에 올바른 타입의 항목만 추가되고 첨자구문에 의해 리턴되는 항목의 타입이 명확해야 합니다.

 

이 요구사항들을 정의하기 위해서, Container 프로토콜은 특적 컨테이너에 대한 타입을 몰라도 컨테이너가 보유할 항목의 타입을 참조하는 방법이 필요합니다. Container 프로토콜은 add(_:) 메소드에 전달된 모든 값이 컨테이너의 항목 타입과 동일해야 하다는 것을 명시해야 합니다. 그리고 컨테이너 첨자 구문에 의해 리턴되는 값이 컨테이너의 항목 타입과 동일해야 함을 명시해야 합니다.

 

이를 달성하기 위해 Container 프로토콜은 Item 이라는 associated type을 선언합니다. 프로토콜은 Item 이 무엇인지 정의하지 않습니다 (Itme이 무엇인지에 대한 정보는 제공되는 모든 준수 타입을 위해 남겨집니다). 그럼에도 Item 별칭은 1. Container의 항목 타입을 참조하고, 2. append(_:) 메소드와 첨자구문을 사용할 타입을 정의하고, 3. 모든 Cotainer의 예상 동작을 강제할 수 있는 방법을 제공합니다.

 

아래 코드는 IntStack 타입의 제네릭 아닌 버젼입니다. Container 프로토콜을 준수합니다.

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 타입은 Container 프로토콜의 3 가지 요구사항을 모두 구현합니다. 각 케이스에서 해당 요구사항을 충족하기 위해 IntStack 타입의 기존 기능 일부를 제공합니다.

 

IntStack 컨테이너의 이 구현에 사용할 적절한 Item이 Int 타입이라는 것을 명시합니다.  typealias Item = Int 정의는 이러한 Container 프로토콜 구현을 위해 Item의 추상화 타입을 Int의 구체적인 타입으로 바꿉니다.

 

Swift 의 타입 추론 덕분에, IntStack 정의 일부로 Int의 구체적인 Item을 선언할 필요가 없습니다. IntStack이 Container 프로토콜의 모든 요구사항을 충족시키기 때문에, Swift는 적절한 Item 을 추론합니다. 단지 append(_:) 메소드의 item 파라미터 타입과 첨자 구문의 리턴 타입을 보면 됩니다. 

위의 코드에서 "typalias Item = Int" 줄을 삭제해도 모든 것이 계속 작동합니다. 항목에 사용할 유형이 분명하기 때문입니다.

 

Container 프로토콜을 준수하는 제네릭 Stack 타입을 만들 수도 있습니다.

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

이번에는 타입 파라미터 Element 가 append(_:) 메소드의 item 파라미터 타입과 첨자구문의 리턴 타입으로 사용됩니다. Swift 는 Element가 이 특정 컨테이너를 위한 Item으로 사용하기 적합한 유형이라고 추론할 수 있습니다.

 

 

 

 

Extending an Existing Type to Specify an Associated Type (기존 타입을 확장하여 Associated Type 명시)

프로토콜 준수를 추가하기 위해 기존 타입을 확장할 수 있습니다. 여기에는 associated type 의 프로토콜이 포함됩니다.

 

Swift의 밸열 타입은 이미 append(_:) 메소드를 제공합니다. 항목을 검색하기 위해 Int index를 사용한 첨자구문도 제공합니다. 이 세가지 기능은 Container 프로토콜의 요구사항과 일치합니다. 이는 Array가 프로토콜을 채택한다고 선언함으로써 Conatiner 프로토콜을 준수하는 Array를 확장할 수 있다는 말입니다.

extension Array: Container {}

Array의 기존 append(_:) 메소드와 첨자 구문은 Swift가 Item을 위해 사용할 적절한 타입을 추론 가능하게 합니다. 확장 정의가 끝나면 모든 Array 를 Container 처럼 사용할 수 있습니다.

 

 

 

 

Adding Constraints to an Associated Type (associated type에 제약조건 추가)

준수 타입이 해당 제약조건을 충족시키기 위해 프로토콜안에서 associated type 에 타입 제약조건(type constraints) 을 추가할 수 있습니다.

아래 코드의 Conatiner는 item 이 동일해야 합니다.

protocol Container {
    associatedtype Item: Equatable
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

이 버전의 Container를 준수하기 위해, 컨테이너의 Item 타입은 Equatable 프로토콜을 준수해야 합니다.

 

 

 

 

Using a Protocol in Its Associated Type’s Constraints (associated type의 제약조건에서 프로토콜 사용)

프로토콜은 자신의 요구사항의 일부로 나타날 수 있습니다.

 

suffix(_:) 메소드의 요구사항을 추가해 Container 프로토콜을 수정합니다. suffix(_:) 메소드는 컨테이너의 끝에서 지정된 수의 항목을 반환하여 Suffix 타입의 인스턴스에 저장합니다.

protocol SuffixableContainer: Container {
    associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
    func suffix(_ size: Int) -> Suffix
}

이 프로토콜에서 Suffix 는 associated type 입니다. Suffix 는 두 개의 제약 조건을 가집니다.

1. 반드시 SuffixableContainer 프로토콜을 준수해야 합니다(지금 정의된 프로토콜 말입니다).

2. Item 타입은 반드시 컨테이너의 Item 타입과 같아야 합니다.

Item의 제약 조건은 제네릭 where 클로져 입니다.

 

아래 코드는 Stack 타입의 확장입니다. SuffixableContainer 프로토콜을 준수합니다.

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을 위한 Suffix associated type 도 Stack 입니다. 따라서 Stack 에서의 suffix 동작은 다른 Stack을 리턴합니다. 또한 SuffixableContainer 을 준수하는 타입은 자신과 다른 Suffix 타입을 가질 수 있습니다. suffix 동작이 다른 타입을 리턴한다는 뜻입니다.

예를 들어 SuffixableContainer 준수를 추가한 제네릭 아닌(nongeneric) IntStack 타입에 대한 확장이 있습니다. IntStack 대신 suffix 타입으로써 Stack<Int>를 사용합니다.

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 (제네릭 where 클로져)

타입 제약조건(Type Constraints) 제네릭 함수, 첨작 구문, 타입과 연관된 타입 파라미터 요구사항을 정의할 수 있게 합니다.

 

Associated Types 의 요구사항을 정의하는 것도 유용할 수 있습니다. 제네릭 where 클로져(generic where clause)를 정의해서 이를 수행합니다. 제네릭 where 클로져는 Associated Types이 특정 프로토콜을 준수하거나 특정 타입 파라미터와 Associated Types 이 반드시 같게 하는 것을 가능하게 합니다.

제네릭 where 클로져는 where 키워드를 사용해 시작하여 Associated Types  또는 타입과 Associated Types의 동일 관계에 대한 제약조건이 뒤따릅니다. 제네릭 where 클로져는 타입 또는 함수 본문의 { 앞에 작성합니다.

 

아래 예제는 제레닉 함수 allItemsMatch를 정의합니다. 두 Container 인스턴스에 동일한 항목이 동일한 순서로 포함되어 있는지 확인합니다. 이 함수는 모든 항목이 일치하면 true, 일치하지 않으면 false를 리턴합니다.

 

확인할 두 컨테이너는 같은 타입의 컨테이너일 필요가 없습니다. 하지만 같은 타입의 항목을 가지고 있어야 합니다. 이 요구 사항은 타입 제약조건(Type Constraints) 과 제네릭 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 라는 2 개의 인수를 가집니다. someContainer 인수는 C1 타입이며 anotherContainer 인수는 C2 타입입니다. C1 과 C2 모두 함수가 호출될 때 결정되는 두 Container 타입에 대한 타입 파라미터 입니다.

 

함수의 두 가지 타입 파라미터에 아래 요건이 적용됩니다.

  • C1 은 반드시 Container 프로토콜을 준수해야 합니다 (C1:Container)
  • C2 도 반드시 Container 프로토콜을 준수해야 합니다 (C2:Container)
  • C1의 Item은 C2의 Item과 반드시 같아야 합니다 (C1.Item == C2.Item)
  • C1의 Item은 반드시 Equatable 프로토콜을 준수해야합니다 (C1.Item: Equatable)

첫 번째, 두 번째 요구사항은 함수의 타입 파라미터 목록에 정의됩니다. 세 번째, 네 번째 요구사항은 함수의 제네릭 where 클로져에 정의됩니다.

 

이 요구사항들은 의미합니다.

  • someContainer 는 C1 타입의 컨테이너이다
  • anotherContainer 는 C2 타입의 컨테이너이다
  • someContainer 와 anotherContainer 는 같은 타입의 item 을 가진다
  • someContainer 의 Item은 서로가 다른지 확인하기 위해 != 연산자로 확인되어야 한다

세 번째, 네 번째 요구사항은 anotherContainer 도 != 연산자로 확인될 수 있다는 의미를 포함합니다. someContainer의 item과 완전히 같은 타입을 가지기 때문입니다.

 

이 요구사항들은 컨테이너 타입이 다르더라도 allItemsMatch(_:_:) 함수가 두 컨테이너를 비교할 수 있게 합니다.

 

allItemsMatch(_:_:) 함수는 "두 개의 컨테이너가 같은 수의 items 을 가지고 있는지 확인하는 것"부터 시작합니다. 다른 수의 items 을 가진다면 false를 리턴합니다.

 

이 확인 이후에 함수는 for루프를 통해 someContainer 의 items을 반복(iterates) 합니다. 각 item에 대해 함수는 someContainer 의 item이 다른 컨테이너의 item 과 같은지 확인합니다. 두 item이 같지 않으면 false를 리턴합니다.

for 루프가 미스매치 없이 끝마치면, true를 리턴합니다.

 

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 개의 문자열을 스택에 푸시합니다.

스택에 있는 3 개의 문자열을 포함하는 배열 리터럴로 초기화된 배열 인스턴스도 생성합니다. 배열과 스택이 다른 타입이지만, 둘 다 Container 프로토콜을 준수하면서 같은 타입의 값을 가지고 있습니다.

allItemsMatch(_:_:) 함수를 호출하여 두 컨테이너를 인수로 사용합니다. 3개의 items이 모두 같으므로 allItemsMatch(_:_:) 함수는 true를 리턴합니다.

 

 

 

 

Extensions with a Generic Where Clause (제네릭 where 클로져 확장)

확장의 일부로 제네릭 where 클로져를 사용할 수 있습니다.

 

아래 예제는 제네릭 Stack 구조체에 isTop(_:) 메소드를 추가하여 확장합니다. 

extension Stack where Element: Equatable {
    func isTop(_ item: Element) -> Bool {
        guard let topItem = items.last else {
            return false
        }
        return topItem == item
    }
}

새 isTop(_:) 메소드는 먼저 스택이 비었는지 확인합니다. 그 다음 지정된 항목을 스택의 맨 위 항목과 비교합니다.

제네릭 where 클로져 없이 이를 시도하면 문제가 생깁니다. isTop(_:) 구현에서 == 연산자를 사용하지만, Stack의 정의에서는 항목이 동일할 필요가 없습니다. 따라서 ==를 사용하면 컴파일-타임 에러가 발생합니다.

제네릭 where 클로져를 사용하면 확장에 새로운 요구사항을 추가할 수 있습니다. 그에 따라 스택 항목이 동일할 때만  isTop(_:) 메소드를 추가합니다.

 

if stackOfStrings.isTop("tres") {
    print("Top element is tres.")
} else {
    print("Top element is something else.")
}
// Prints "Top element is tres."

항목이 동일하지 않은 스택에서 isTop(_:) 메소드를 호출하면 컴파일-타임 에러가 발생합니다.

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

 

프로토콜에 확장과 함께 제네릭 where 클로져를 사용할 수 있습니다.

기존 예제에 startsWith(_:) 메소드를 추가해 Container 프로토콜을 확장합니다.

extension Container where Item: Equatable {
    func startsWith(_ item: Item) -> Bool {
        return count >= 1 && self[0] == item
    }
}

 

startsWith(_:) 메소드는 먼저 컨테이너에 하나 이상의 항목이 있는지 확인합니다. 그 다음 컨테이너의 첫 번째 항목이 주어진 항목과 일치하는지 확인합니다. 이 새 startsWith(_:) 메소드는 컨테이너의 항목이 동일하다면, (위에서 사용한 스택과 배열을 포함하여) 컨테이너 프로토콜을 준수하는 모든 타입과 함께 사용할 수 있습니다.

if [9, 9, 9].startsWith(42) {
    print("Starts with 42.")
} else {
    print("Starts with something else.")
}
// Prints "Starts with something else."

위 예제에서의 제네릭 where 절을 사용하려면 Items 이 프로토콜을 준수해야 합니다. 하지만 Item이 특정 타입이어야 하는 제네릭 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 타입이 Double 인 컨테이너에 average() 메소드를 추가합니다. 컨테이너의 항목을 반복하면서 더하고, count 로 나눠 평균 값을 리턴합니다. 

 

확장의 일부인 제네릭 where 절에 여러 개의 요구사항을 포함할 수 있습니다. 다른 곳에서 쓰는 제네릭 where 절에 대해서도 마찬가지입니다. 각 요구사항을 , 로 구분합니다.

 

 

 

 

 

Contextual Where Clauses (문맥상 where 절)

제네릭 타입의 문맥(context) 에서 이미 작업 중인 경우, 제네릭 where 절을 자신의 제네릭 타입 제약조건을 가지지 않는 정의의 일부로 작성할 수 있습니다. 

예를 들어 제네릭 타입의 첨자구문이나 제네릭 타입 확장 안에 있는 메소드에서 제네릭 where 클로져를 작성할 수 있습니다. Container 구조체는 제네릭입니다. 아래 예제의 where 클로져는 컨테이너에서 새 메소드를 사용하기 위해 충족해야하는 제약조건을 명시합니다.

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"

예제는  항목이 정수(integers)일 때 Container 에 average() 메소드를 추가합니다. 항목이 동일 할 때 endWith(_:) 메소드를 추가합니다. 두 함수 모두 "컨테이너의 원래 선언에서 제네릭 Item 타입 파라미터에 타입 제약 조건"을 추가하는 제네릭 where 절을 포함합니다.

 

문맥상 where 클로져를 사용하지 않고 코드를 작성하려면, 각 where 클로져에 두 확장을 작성합니다. 위 예제와 아래 예제는 같은 동작을 수행합니다.

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 클로져를 사용하는 이 예제의 버전은 같은 확장에서 average() 와 endsWith(_:)를 구현합니다. 왜냐하면 각 메소드의 제네릭 where 클로져가 해당 메소드를 사용하기 위한 요구사항을 나타냅니다. 이러한 요구사항을 확장의 제네릭 where 클로져로 이동하는 것이 같은 상황에서 메소드를 사용할 수 있게 합니다. 하지만 하나의 요구사항에 하나의 확장이 필요합니다.

 

 

 

 

 

Associated Types with a Generic Where Clause (제네릭 where 클로절과 Associated Types)

Associated Types 에 제네릭 where 클로져를 포함할 수 있습니다.

예를 들어 반복자(iterator)를 포함하는 Container 버젼을 만들려고 합니다. Sequence 프로토콜 처럼 말입니다.

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 의 제네릭 where 클로져는 반복자의 타입에 관계없이 반복자가 컨테이너의 항목과 동일한 항목 타입의 요소를 통과해야함을 요구합니다. makeIterator() 는 컨테이너의 반복자에 대한 접근을 제공합니다.

 

다른 프로토콜에서 상속되는 프로토콜의 경우, 상속된 associated type에 프로토콜 선언에 제네릭 where 클로져를 포함하여 제약 조건을 추가합니다.

예를 들어 아래 코드는 Item이 Comparable을 준수해야 하는 ComparableContainer 프로토콜을 선언합니다.

protocol ComparableContainer: Container where Item: Comparable { }

 

 

 

 

 

Generic Subscripts (제네릭 첨자 구문)

첨자 구문(subscripts) 는 제네릭이 될 수 있습니다. 제네릭 where 클로져를 포함할 수도 있습니다.

 

subscript 뒤 <> 안에 placeholder 타입 이름을 작성합니다. subscript 본문 { 앞에 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 프로토콜에 대한 확장은 일련의 index 순서를 취하는 첨자 구문을 더합니다. 주어진 각 index 항목이 포함된 배열을 포함합니다.

 

제네릭 첨자구문(subscripts)은 아래와 같이 제한됩니다.

  • <> 안에 있는 제네릭 파라미터 Indices는 표준 라이브러리의 Sequence 프로토콜을 준수하는 타입이어야 합니다
  • 첨자 구문은 (해당 index 타입의 인스턴스인) 단일 파라미터 indices 를 가집니다
  • 제네릭 where 클로져를 사용하려면 시퀀스에 대한 반복자가 Int 타입의 요소(elements) 를 통과해야합니다. 시퀀스 안에 있는 indices 가 컨테이너에 사용되는 indices와 같은 타입임을 보장합니다

 

이러한 제약조건들을 종합하면, index 파라미터에 대해 전달된 값이 정수 순서임을 의미합니다.

 

 

 

 

 

 

제네릭(generice)은 말 그대로 일반 버전을 만들 수 있습니다.

이를 통해 코드의 재사용성과 효율을 높일 수 있습니다.

Associated Types 은 "프로토콜의 일부로 사용되는 타입"에 placeholder 이름을 줍니다.

Generic where Clause는 Associated Types이 특정 프로토콜을 준수하거나 특정 타입 파라미터와 Associated Types 이 반드시 같게 하는 것을 가능하게 합니다.

Generic where Clause를 통해 조건에 따라 프로토콜을 준수할 수 있습니다.

 

 

 

 

Generic where Clause 가 이해하기 쉽지 않다

열심히 반복해서 봐야겠다

Generics 끘 😄