Generics

Swift/Language Guide 2018. 9. 18. 00:08
반응형

[최종 수정일 : 2018.09.07]

원문 : 애플 개발 문서 Swift 4.2 Language Guide - Generics

제네릭(Generics)

제네릭 코드(Generic code)는 모든 타입으로 작업할수 있는 유연하고 제사용가능한 함수와 타입을 작성하는게 가능하며, 정의한 요구사항에 따라 달라집니다. 중복을 피하고 의도를 명확하고, 추상적으로 관리하는 코드를 작성할 수 있습니다.

제네릭은 Swift에서 가장 강력한 기능중에 하나이고, Swift 표준 라이브러리의 대부분은 제네릭 코드로 만들어졌습니다. 사실상, 깨닷지 못하였을지라도, 언어 가이드(Language Guide) 전체에서 제네릭을 사용해 왔습니다. 예를들어, Swift의 Array와 Dictionary타입은 모두 제네릭 컬렉션입니다. Int값을 저장하는 배열이나 String값을 저장하는 배열, Swift에서 만들수 있는 다른 모든 타입으로 된 배열을 생성할 수 있습니다. 비슷하게, 특정 타입의 값을 저장하기 위해, 딕셔너러를 생성할 수 있고, 그 타입에 제한이 없습니다.

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

다음은 표준적인, 두개의 Int값을 바꿔주는(swap) 제네릭이 아닌 함수 swapTwoInts(_:_:) 함수입니다.

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

이 함수는 In-Out 매개변수(In-Out Parameters)에 설명된것 처럼, a와 b의 값을 바꿔주기 위해 in-out 매개변수를 사용합니다.

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 값을 바꿔주길 원하는 경우에, 아래에서 보는 것처럼 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(_:_:)함수의 본문이 동일하다는 것을 눈치챘을지도 모릅니다. 유일하게 다른 점은 허용하는 값의 타입입니다.(Int, String, Double)

모든(any) 타입의 값 2개를 바꿔주는 하나의 함수로 작성하는게 훨씬 더 유용하고 매우 유연할 것입니다. 제네릭 코드는 하나의 함수로 작성하는 것이 가능합니다. (이 함수의 제네릭 버젼은 아래에 정의됩니다)

주의
함수 3개 모두, 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(_:_:) 함수의 본문과 같습니다. 하지만, swapTwoValues(_:_:)의 첫번째 줄은 swapTwoInts(_:_:)와는 약간 다릅니다. 다음은 첫번째 줄을 비교한 것입니다.

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

함수의 제네릭 버젼은 실제(actual)타입 이름(Int, String, Double) 대신에 견본(placeholder) 타입 이름(이 경우에는 T)을 사용합니다. 견본(placeholder) 타입 이름은 T가 무엇이어야 하는지에 대해서 말하지 않지만, T가 무엇이든간에, a와 b는 같은 타입 T가 되야 한다고 말합니다. T의 위치에 사용는 실제 타입은 swapTwoValues(_:_:) 함수가 호출될때 마다 결정됩니다.

제네릭 함수와 제네릭이 아닌 함수간의 다른 차이점은 제네릭 함수의 이름(swapTwoValues(_:_:))이 견본 타입 이름(T)이 꺽쇠 괄호안에 넣는 것입니다(<T>). Swift에서 이 꺽쇠 괄호(<>)는 T가 swapTwoValues(_:_:) 함수의 정의에서 견본 타입 이름인 것을 말합니다. T는 견본(placeholder)이기 때문에, Swift는 T의 실제타입에 대해 찾지 않습니다.

이제 swapTwoValues(_:_:) 함수는 두개의 값들이 서로 같은 종류의 타입인 한, 모든(any) 타입의 값 두개를 전달할 수 있는 것만 제외하면, 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에 의해 영감(inspired)을 얻었습니다. 코드에서 swapTwoValues(_:_:) 함수의 동작이 필요한 경우,직접 구현을 제공하는 것보다 Swift에 있는 swap(_:_:)함수를 사용할 수 있습니다.

타입 매개변수(Type Parameters)

위의 swapTwoValues(_:_:) 예제에서, 견본 타입 T는 타입 매개변수(type parameter)의 예제입니다. 타입 매개변수는 꺽쇠 괄호(<>)쌍 사이에, 견본(placeholder) 타입을 지정하고 이름을 지정하고, 함수의 이름 바로 뒤에 작성합니다. (<T>)

한번 타입 매개변수를 지정하면, 함수의 매개변수나 함수의 반환 타입, 함수 본문에서 해석(annotation)하는 타입 정의에 사용할 수 있습니다. 각 경우에, 타입 매개변수는 gkatnrk ghcnfehlfEoakek 실제(actual) 타입으로 교체됩니다. (위의 예제 swapTwoValues(_:_:에서, T는 첫번째 함수가 호출될때 Int로 교체되었고, 두번째 호출 되었을때에는 String으로 교체되었었습니다 ))

꺽쇠 괄호 사이에 콤마로 구분해서, 여러개의 타입 매개변수 이름을 작성해서, 하나 이상의 타입 매개변수를 제공할 수 있습니다

타입 매개변수 이름짓기(Naming Type Parameters)

대부분의 경우에, 타입 매개변수는 타입 매개변수와 사용된 제네릭 타입이나 함수간의 관계에 대해 알려주는, Dictionary<Key, Value>에서의Key와 ValueArray<Element>에서의 Element처럼, 설명하는 이름입니다. 하지만, 그것들 사이에 의미있는 관계가 없을때, 위의 swapTwoValues(_:_:) 함수에서의 T와 같이, 전통적으로 T, U, V처럼 하나의 단어로된 이름을 사용합니다.

주의
값이 아니라 타입에 대한 견본을 가리키기 위해, 항상 타입 매개변수에 대문자 카멜표기법 이름(T, MyTypeParameter을 입력합니다.

제네릭 타입(Generic Types)

제네릭 함수 외에도, Swift는 자신만의 제네릭 타입(generic types)을 정의하는 것이 가능합니다. Array와 Dictionary와 비슷한 방법으로, 사용자정의 클래스, 구조체, 열거형을 모든(any) 타입으로 작업할 수 있습니다.

이번 섹션은 제네릭 컬렉션 타입 Stack을 작성하는 방법을 보여줍니다. 스택(stack)은 정렬된 값을 설정하며, 배열과 비슷하지만, 설정하는 기능은 Swift의 Array타입보다는 더 제한됩니다. 배열은 새로운 항목을 배열내의 모든 위치에서 삽입하거나 제거하는것이 가능합니다. 하지만, 스택(stack)은 새 항목을 컬렉션의 끝에서만 추가할 수 있습니다(스택에 새 값을 밀어넣기(puhsing)). 비슷하게, 스택(stack)은 항목들을 컬렉션의 끝에서만 제거할수 있습니다(스택에서 값을 빼내기(popping))

주의
스택의 개념은 네비게이션 계층구조에서 뷰 컨트롤러를 모델링하는 UINavigationController클래스에서 사용됩니다. 네비게이션 스택에 뷰 컨트롤러를 추가(push) 하기 위해 UINavigationController 클래스의 pushViewController(_:animated:) 메소드를 호출하고, 네비게이션 스택에 뷰 컨트롤러를 제거(pop)하기 위해 popViewControllerAnimated(_:) 메소드를 호출합니다. 스택은 유용한 후입 선출(last in, first out)방식의 컬렉션을 관리할때 유용한 모델입니다.

아래 그림은 스택에 push와 pop하는 동작을 보여줍니다.

  1. 현재 스택에 3개의 값이 있습니다.
  2. 4번재 값이 스택의 맨 위에 밀어넣습니다.(pushed)
  3. 이제 스택은 4개의 값을 가지고 있으며, 맨 위의 하나는 가장 최근 값입니다.
  4. 스택에서 맨 위의 항목을 꺼냅니다.(popped)
  5. 값을 하나 꺼내서(popping), 스택은 다시 3개의 값을 가지고 있습니다.

다음은 Int 값의 스택에 대한 경우에, 스택의 제네릭이 아닌 버젼을 작성하는 방법입니다.

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

이 구조체는 스택에서 값을 저장하기 위해 Array 프로퍼티 items를 사용합니다. Stack은 스택에 on/off값을 밀어 넣고 빼내기 위해 2개의 메소드 push와 pop을 제공합니다. 이 메소드들은 구조체의 items 배열을 수정(변경 mutate)할 필요가 있기때문에 mutating으로 표시됩니다.

위에서 IntStack타입은 Int값만 사용할 수 있는 것을 볼수 있습니다. 하지만, 모든(any) 타입의 값 스택을 관리할수 있는, 제네릭(generic)Stack 클래스를 정의하는 것이 훨씬 더 유용할 것입니다.

다음은 같은 코드의 제네릭 버젼입니다.

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

Stack의 제네릭 버젼은 본질적으로(essentially) 제네릭이 아닌 버젼과 동일하지만, 실제 Int 타입 대신에 타입 매개변수 Element를 사용합니다. 이 타입 매개변수는 구조체의 이름 바로 뒤에 꺽인 괄호 쌍(<>) 사이에 작성합니다. (<Element>)

Element는 나중에 제공될 타입에 대한 견본이름을 정의합니다. 이 타입은 구조체 정의에서 Element가 있는 어디에서든 나중에 참조될 수 있습니다. 이 경우에, Element는 3군데에서 견본으로 사용되었습니다.

  • Element 타입 값의 빈 배열로 초기화된, 프로퍼티 items 을 생성하기 위해
  • Element 타입이어야 하는, item 매개변수를 가지는 push(_:) 메소드를 지정하기 위해
  • pop()메소드에 의해 반환되는 값이 Element 타입의 값이 되도록 지정하기 위해

제네릭 타입이기 때문에, Stack은 Swift에서 허용한 모든(any) 타입의 스택을 생성하는데 사용될 수 있으며, ArrayDictionary와 비슷하게 관리합니다.

스택에서 저장될 타입을 꺽인 괄호(<>) 안에 작성하여 새로운 Stack인스턴스를 생성합니다. 예를들어, 새로운 문자열의 스택을 생성하기 위해, Stack<String>()을 작성합니다.

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

다음은 stackOfStrings 스택에 4개의 값을 집어 넣는 방법을 보여줍니다.

스택에서 값을 꺼내면 맨 위의 값 "cuatro"을 제거하고 반환합니다.

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

다음은 맨 위의 값을 꺼낸 뒤의 스택의 모습입니다.

제네틱 타입 확장하기(Extending a Generic Type)

제네릭 타입을 확장할때, 확장의 정의에서 타입 매개변수 목록을 제공하지 않습니다. 그 대신에, 원래(original) 타입으로부터 정의된 타입 매개 변수 목록은 확장의 본문에서 사용가능하고, 원래 타입 매개변수 이름은 원래 정의에서 타입 매개변수를 참조하는데 사용됩니다.

다음 예제는 스택에서 꺼내지 않고 맨 위의 항목을 반환하는 읽기 전용(read-only) 계산 프로퍼티 topItem을 추가하기 위해, 제네릭 Stack타입을 확장합니다.

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

topItem프로퍼티는 Element타입의 옵셔널 값을 반환합니다. 스택이 비어 있는 경우에, topItem은 nil을 반환하며, 스택이 비어 있지않으면, topItem은 items배열의 마지막 항목을 반환 합니다.

이 확장은 타입 매개변수 목록을 정의하지 않는 것을 주의합니다. 그 대신에, Stack 타입의 기존 타입 매개변수 이름 Element는 확장에서 계산 프로퍼티 topItem의 옵셔널 타입을 가리키기 위애 사용됩니다.

계산 프로퍼티 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."

아래 제네릭 Where절로 확장하기(Extensions with a Generic Where Clause)에 설명된것처러, 제네릭 타입의 확장은 새로운 기능을 얻기 위해, 확장된 타입의 인스턴스 요구사항을 반드시 만족하도록 포함할 수 있습니다.

타입 제약사항(Type Constraints)

swapTwoValues(_:_:)함수와 Stack타입은 모든 타입으로 작업할 수 있습니다. 하지만, 그것은 가끔씩 제네릭 함수나 제네릭 타입으로 사용할수 있는 타입에서 특정 타입 제약사항(type constraints)을 적용하는 것이 유용할때가 있습니다. 타입 제약사항은 타입 매개변수가 특정 클래스에서 상속되어야 하거나, 특정 프로토콜이나 프로토콜 합성을 준수하는 것을 지정합니다.

예를들어, Swift의 Dictionary타입은 딕셔너리의 키로 사용될 수있는 타입을 제한(limitation)합니다. 딕셔너리(Dictionaries)에서 설명된 것 처럼, 딕셔너리의 키의 타입은 반드시 hashable해야 합니다. 그것은 스스로 고유하게 표현가능한 방법을 제공해야 합니다. Dictionary는 특정 키에 대한 값을 이미 가지고 있는지 확인할수 있는, hashable한 키가 필요합니다. 이러한 요구사항 없이는, Dictionary는 특정 키에 대한 값을 삽입하거나 교체할 수 없으며, 이미 딕셔너리에 있는 키의 값을 찾을수도 없습니다.

이 요구사항은 키 타입이 반드시 Hashable프로토콜, Swift의 표준 라이브러리에 정의된 특별한 프로토콜을 준수해야 하는 것을 지정하며,Dictionary의 키 타입에 대한 타입 제약사항에 의해 시행(enforced)됩니다.

사용자정의 제네릭 타입을 생성할때 고유한 타입 제약사항을 정의할 수 있고, 이러한 제약사항은 제네릭 프로그래밍을 막강하게 해줍니다. Hashable특징의 타입은 개념적 특징의 관점에서는 구체적인(concrete) 타입 보다는 추상화(abstract) 개념과 비슷합니다.

타입 제약사항 문법(Type Constraint Syntax)

단일 클래스나 타입 매개변수 이름 뒤에 타입 매개변수 목록으로 콜론(:)으로 구분된, 프로토콜 제약사항을 위치시켜서 타입 제약사항을 작성합니다. 제네릭 함수에서의 타입 제약사항의 기본 문법은 아래 보여집니다(이 문법은 제네릭 타입과 같습니다)

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

위의 가상의 함수는 두개의 타입 매개변수를 가집니다. 첫번째 타입 매개변수, T는 T 가 SomeClass의 하위클래스가 되는 요구사항을 있는 타입 제약사항을 가집니다. 두번째 타입 매개변수 U는 U가 SomeProtocol 프로토콜을 준수하는 요구사항이 있는 타입 제약사항을 가집니다.

타입 제약사항 동작(Type Constraints in Action)

다음은 배열의 String값을 찾기 위해 String값을 주는 제네릭이 아닌(nongeneric) 함수 findIndex(ofString:in:)입니다. findIndex(ofString:in:) 함수는 배열에서 처음 일치하는 문자열의 인덱스나 문자열을 찾지못하면 nil을 가져오는 옵셔널 Int값을 반환합니다.

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"

배열에 잇는 값의 인덱스를 찾는 원리는 문자열에서만은 유용하지 않습니다. 하지만, 문자열이 언급된 모든 것들을 T 타입의 값으로 교체해서 제네릭 함수로 동일한 기능을 작성할 수 있습니다.

다음은 findIndex(ofString:in:)의 예상했던 제네릭 버젼인 findIndex(of:in)을 작성하는 방법입니다. 함수의 반환 값은 배열의 옵셔널 값이 아니라, 옵셔널 인덱스 숫자이기 때문에, 이 함수의 반환 타입이 여전히 Int?인 것을 주의합니다. 이 함수는 컴파일되지 않는다는 경고를 받게 되며, 그 이유는 예제 뒤에 설명됩니다.

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 == valeToFind" 동일한지 검사하는 것입니다. Swift에서 모든 타입이 동등 연산자(==)로 비교할 수 있는 것이 아닙니다. 좀 더 복잡한 모델을 표현하기 위해 자신만의 클래스나 구조체를 생성하는 경우, 예를 들어, 클래스나 구조체에 대해 동일하다(equal to) 라는 의미는 Swft가 추측할 수 있는 것이 아닙니다. 이 때문에, T타입이 코드에서 사용가능한 모든(every) 타입에서 동작하는 것을 보증하는 것이 불가능하고, 코드를 컴파일 할때 적절한 오류가 보고 됩니다.

하지만, 희망이 없는 것은 아닙니다(all is not lost). Swift 표준 라이브러리는 모든 타입의 두개의 값을 비교하기 위해, 동등 연산자(==)와 비동등 연산자(!=)에 대한 구현하는 요구사항을 준수하는 Equatable프로토콜을 정의합니다. Swift의 모든 표준 타입은 자동으로 Equatable프로토콜을 지원합니다.

findIndex(of:in:) 함수에서 Equatable인 모든 타입은 동등 연산자를 지원하는 것을 보장하기 때문에, 안전하게 사용될 수 있습니다. 이 사실을 표현하기 위해, 함수를 정의할때 타입 매개변수의 정의에서 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프로토콜을 준수합니다를 의미하는T: Equtable로 작성됩니다

findIndex(of:in:) 함수는 이제 컴파일이 성공하고 Double나 String처럼, 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 type)은 프로토콜에서 사용되는 타입에 견본(placeholder) 이름을 줍니다. 연관된 타입에 사용하는 실제 타입은 프로토콜이 채택될때까지 지정되지 않습니다. 연관된 타입은 associatedtype 키워드로 지정됩니다.

연관된 타입 동작(Associated Types in Action)

다음 예제는 연관된 타입 Item을 선언하는 프로토콜 Container입니다.

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

Container프로토콜은 모든 컨테이너(container)가 반드시 제공해야 하는 3가지 필수기능을 정의합니다.

  • append(_:)메소드로 컨테이너에 새로운 항목을 추가할 수 있어야 합니다.
  • Int값을 반환하는 count프로퍼티를 이용해서 컨테이너에 있는 항목들의 갯수을 접근할 수 있어야 합니다.
  • Int인덱스 캆을 가져오는 서브스크립트를 사용해서 컨테이너에 있는 각 항목을 가져올수 있어야 합니다.

이 프로토콜은 컨테이너에서 항목들을 저장하는 방법을 허용되는 타입을 지정하지 않습니다. 이 프로토콜은 모든 타입이 Container가 되도록하기 위해서 제공하는, 3개의 기능만 지정합니다. 이러한 3개의 요구사항을 만족하는 한, 준수하는 타입은 추가적인 기능을 제공할수 있습니다.

Container프로토콜을 준수하는 모든 타입은 저장하는 값의 타입을 지정할 수 있어야 합니다. 특히, 올바른 타입의 항목들만 컨테이너에 추가되도록 해야하고, 서브스크립트에 의해 반환된 항목들의 타입을 명확하게 해야 합니다.

이러한 요구사항을 정의하기 위해, Container프로토콜은 특정 컨테이너에 대한 타입이 무엇인지 모른채, 컨테이너가 가지고 있어야 할 요소들의 타입을 참조하는 방법이 필요합니다. Container프로토콜은 
append(_:) 메소드에 전달되는 모든 값이 컨테이너의 요소 타입과 동일한 요소여야하는 것을 지정하는 것이 필요하고, 컨테이너의 스크립트에 의해 컨테이너의 요소 타입과 동일한 타입이 되는 값을 반환됩니다.

이를 위해서(to achieve this), Container프로토콜은 associatedtype Item으로 작성해서, 연관된 타입 Item을 선언합니다. 그 프로토콜은 Item이 무엇인지 정의하지 않습니다.- 그 정보는 준수하는 모든 타입이 제공하도록 남겨둡니다. 그럼에도 불구하고(nonetheless), Item 별칭은 Container에서의 항목의 타입을 참조하는 방법을 제공하고, append(_:) 메소드와 서브스크립트로 사용하는 타입을 정의하며, 모든 Container의 예상되는 동작이 시행되도록 보증해야 합니다.

다음은 위의 제네릭 타입(Generic Types) 제네릭이 아닌(nongeneric) 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타입의 기존 기능의 일부를 래핑(wraps)합니다.

게다가(moreover), IntStack은 Container의 구현에 대해 지정하며, 적절한 Item은 Int의 타입을 사용합니다. typealias Item = Int의 정의는 Container프로토콜의 구현에서 Item의 추상화 타입이 Int 구체적인 타입으로 변환(turns)됩니다.

Swift의 타입 추론 덕분에, 실제로 IntStack의 정의에서 처럼, 구체적으로 Item을 Int로 선언하는 것을 구현할 필요하지 않습니다. IntStack은 Container프로토콜의 모든 요구사항을 준수하기 때문에, Swift는 append(_:) 메소드의 item 매개변수 타입과 스크립트의 반환 타입에서 찾아서 사용할적절한 Item을 추론할 수 있습니다, 대신에, 위의 코드에서 typealias Item = Int줄을 제거하는 경우, Item이 어떤 타입으로 사용되야 하는지 명확하기 때문에, 모든것은 여전히 동작합니다.

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)

확장으로 프로토콜 준수 추가하기(Adding Protocol Conformance with an Extension)에 설명된것 처럼, 기존 타입에 프로토콜을 준수하는 것을 추가하기 위해 확장할 수 있습니다. 이것은 연관된 타입의 프로토콜이 포함합니다.

Swift의 Array타입은 이미 append(_:)메소드, count프로퍼티, Int인덱스로 요소를 가져오는 서브스크립트를 제공합니다. 이러한 3가지 기능들은 Container프로토콜의 요구사항과 일치합니다. 이것은 단순히 Array가 프로토콜을 채택했다고 선언함으로써 Container프로토콜을 준수하기 위해 Array확장할 수 있는 것을 의미합니다. 확장으로 프로토콜 채택 선언하기(Declaring Protocol Adoption with an Extension)에서 설명된 것 처럼, 빈 확장으로 처리합니다.

extension Array: Container {}

배열의 존재하는 append(_:)메소드와 서브스크립트는 Swift가 Item에 사용할 적절한 타입으로 추론하는게 가능하며, 위의 제네릭 Stack타입과 마친가지 입니다. 확장을 정의한 후에, 모든 ArrayContainer처럼 사용할 수 있습니다.

연관된 타입에 제약사항 추가하기(Adding Constraints to an Associated Type)

프로토콜에서 연관된 타입에 타입 제약사항을 추가해서 타입이 해당 제약사항을 만족하도록 요구할 수 있습니다. 예를 들어, 다음 코드는 컨테이너에서 항목들을 eauatable하게 해야하는 Container 버젼을 정의합니다.

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)

프로토콜은 자체 요구사항을 나타낼수 있습니다. 예를 들어, 다음은 Container프로토콜을 개선하는 프로콜이 있으며, suffix(_:) 메소드의 요구사항을 추가합니다. suffix(_:) 메소드는 컨테이너의 끝에서 요소들의 개수를 반환하며, Suffix타입의 인스턴스에 저장합니다.

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

프로토콜에서, Suffix는 연관된 타입이며, 위 예제 Container에서의 Item타입과 같습니다. Suffix는 두개의 제약사항을 가집니다: SuffixableContainer프로토콜(현재 정의된 프로토콜)을 반드시 준수해야 하고, Item타입은 컨테이너의 Item타입과 반드시 같아야 합니다. Intem에서의 제약사항은 제네릭 where절이며, 아래의 제네릭 Where 절과 연관퇸 타입(Associated Types with a Generic Where Clause)에 설명되어 있습니다.

다음은 위의 SuffixableContainer프로토콜에 대한 적합성을 추가하는 클로져에 대한 강한 순환참조(Strong Reference Cycles for Closures)의 Stack타입의 확장입니다.

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은 Stack이며, Stack에서 suffix동작은 다른 Stack를 반환합니다. 또는 SuffixableContainer를 준수하는 타입은 그 자체와 다른 Suffix타입을 가질수 있습니다.- 접미사(suffix) 작업은 다른 타입을 반환할 수 있다는 것을 의미합니다. 예를들어, 다음은 제네릭이 아닌 IntStack타입에 SuffixableConatiner를 준수하도록 추가하는 확장이며, 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>.
}

제네릭 Where 절(Generic Where Clauses)

타입 제약사항(Type Constraints)에서 설명된 것처럼, 타입 제약사항은 제네릭 함수나 서브스크릡트, 또는 타입과 연관된 타입 매개변수에 대한 요구사항을 정의하는게 가능합니다.

또한, 연관된 타입에 대한 요구사항을 정의하는 것은 유용할 수 있습니다. 제네릭 where 절(generic where clause)정의 합니다. 제네릭 where 절은 특정 프로토콜을 반드시 준수하는 연관된 타입을 가져오거나 특정 타입 매개변수와 연관된 타입이 동일하게 하는것이 가능합니다. 제네릭 where절은 where키워드로 시작합니다. 연관된 타입 또는 타입과 연관된 타입간의 동등한 관계에 대한 제약사항이 뒤따릅니다. 제네릭 where절을 타입이나 함수의 본문의 열린 중괄호({}) 바로 앞에 작성합니다.

아래 예제는 제네릭 함수 allItemsMatch를 정의합니다.

검사할 두개의 Container 인스턴스가 같은순서로 같은 항목을 가질 필요는 없지만(그럴수 있지만), 같은 타입의 항목들을 가져야 합니다. 이 요구사항은 타입 제약사항과 제네릭 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타입이고, anotherContainer인자는 C2타입입니다. C1과 C2 모두 함수가 호출될때 결정되는 두개의 컨테이너 타입에 대한 타입 매개변수입니다.

다음 요구사항은 함수의 두가지 타입 매개변수에 적용됩니다(placed).

  • 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에는 동일한 타입의 항목들을 포함하고 있습니다
  • someContainer에 있는 항목들은 서로 다른지 비동등 연산자(!=)로 검사할수 있습니다.

세번째와 네번재 요구사항은 someContainer에서의 항목들과 정확히 같은 타입이기 때문에, anotherContainer에서의 항목들도 !=연산자로 검사할수 있다는 것을 의미로 결합됩니다.

이러한 요구사항들은 allItemsMatch(_:_:)함수는 서로 다른 컨테이너 타입일지라도 컨테이너를 비교하는 것이 가능합니다.

allItemsMatch(_:_:) 함수는 두 컨테이너가 모두 같은 항목의 개수를 포함하지 검사하는 것으로 시작합니다. 포함한 항목의 개수가 다른 경우, 일치시킬수 있는 방법이 없고, 함수는 false를 반환합니다.

이를 확인한 후에, 그 함수는 for-in반복문과 반 개방 범위 연산자(half-open range operator..<) 으 someContainer에 있는 항목들을 반복합니다. 각 항목에 대해, 함수는 someContainer의 항목이 anotherContainer에 대한 항목(corresponding item)과 같지 않는지 검사합니다. 두 항목이 같이 않는 경우, 두 컨테이너는 일치하지 않고, 함수는 false를 반환합니다.

반복문이 일치하지 않는 것을 찾지못하고 끝나는 경우, 두 컨테이너는 일치하고, 함수는 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개의 문자열을 밀어 넣습니다(pushes). 또한 예제는 스택과 같은 3개의 문자열 배열 리터럴을 포함해서 초기화된 Array인스턴스를 생성합니다. 스택과 배열은 서로 다른 타입일지라도, 둘 다 Container 프로토콜을 준수하고, 둘 다 같은 타입의 값을 가지고 있습니다. 2개의 컨테이너를 인자로 allItemsMatch(_:_:) 함수를 호출합니다. 위 예제에서, allItemsMatch(_:_:) 함수는 2개의 컨테이너의 항목들이 모두 정확히 일치하는 것을 보고합니다.

제네릭 Where 절이 있는 확장(Extensions with a Generic Where Clause)

확장(extension)에서 제네릭 where절을 사용할 수 있습니다. 아래 예제는 이전 예제에서 isTop(_:) 메소드를 추가하기 위해 제네릭 Stack구조체를 확장합니다.

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

이 새로운 isTop(_:) 메소드는 처음에 stack이 비어있는지 검사를 하고, 주어진 항목과 스택의 최상위 항목을 비교합니다. isTop(_:)의 구현은 ==연산자를 사용하지만, Stack의 정의에서는 그 항목이 동일한지 비교할 필요가 없으며, ==연산자는 컴파잀 오류가 발생합니다. 제네릭 where절을 사용하여 확장에 새로운 요구사항을 추가하며, 스택에서의 항목들이 동일한 경우에만 확장에 isTop(_:) 메서드를 추가합니다.

다음은 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(_:) 메소드는 컨테이너의 항목들이 동일하는 한, 위에서 사용된 스택과 배열을 포함해서, Container프로토콜을 준수하는 모든 타입에서 사용될 수 있습니다.

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

위의 예제에서 제네릭 where 절은 Item이 프로토콜을 준수하는지 필요하지만, 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)를 부동소수점(floating-point) 나누기를 할수도 있도록 Int를 Double로 변환합니다.

확장에서 제네릭 where절에서 여러개의 요구사항을 포함할수 있으며, 다른곳에서 작성하는 것처럼, 제네릭 where절을 사용할 수 있습니다. 그 목록에서 각 요구사항은 콤마(,)로 구분합니다.

제네릭 Where 절이 있는 연관된 타입(Associated Types with a Generic Where Clause)

연관된 타입에서 제네릭 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절은 iterator의 타입과는 관계없이, iterator가 컨테이너의 항목들과 동일한 항목 타입의 요소들이어야 합니다. makeIterator() 함수는 컨테이너의 iterator에 접근할 수 있도록 제공합니다.

다른 프로토콜로부터 상속된 프로토콜은, 프로토콜 선언에서, 제네릭 where 절이 포함된, 상속받은 연관된 타입에 제약사항을 추가합니다. 예를들어, 다음 코드는 Item이 Comparable을 준수하는 ComparableContainer프로토콜을 선언합니다.

protocol ComparableContainer: Container where Item: Comparable { }

제네릭 서브스크립트(Generic Subscripts)

서브스크립트는 제네릭이 될수 있고, 제네릭 where절을 포함할 수 있습니다. 견본(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프로토콜에 대한 확장은 시퀀스 인덱스와 주어진 인덱스마다 항목들을 포함하고 있는 배열을 반환하는 서브스크립트를 추가합니다. 제네릭 서브스크립트는 다음을 포함합니다.

  • 꺽인 괄호(<>)안에 있는 제네릭 매개변수 Indices는 표준 라이브러리로부터 Sequence 프로토콜을 준수하는 타입이어야 합니다.
  • 서브스크립트는 indices타입의 인스턴스인 하나의 매개변수 indices를 가집니다.
  • 제네릭 where절은 시퀀스에 대한 iterator가 반드시 Int타입의 요소여야 합니다. 이렇게 하면 시퉌스의 인덱스는 컨테이너에 사용된 인덱스와 동일한 타입입니다.

    종합해보면(taken together), 이러한 제약조건은 indices 매개변수에 대해 전달된 값이 정수형의 시퀀스 입니다.


반응형

'Swift > Language Guide' 카테고리의 다른 글

Advanced Operators  (0) 2018.09.18
Access Control  (0) 2018.09.18
Memory Safety  (0) 2018.09.18
Automatic Reference Counting  (0) 2018.09.18
Protocols  (0) 2018.09.18
Extensions  (0) 2018.09.18
Nested Types  (0) 2018.09.18
Type Casting  (0) 2018.09.18
Posted by 까칠코더
,