Swift/Swift 가이드

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

유정주 2022. 8. 1. 16:04
반응형

새로 배운 점

  • 제네릭은 Swift에서 가장 강력한 기능 중 하나이며, 대부분의 Swift 표준 라이브러리는 제네릭 코드로 빌드됩니다.
  • 하나 이상의 타입 파라미터를 사용할 수 있는데, 각 타입 파라미터는 꺽쇠 괄호 내에서 콤마로 구분해 작성하면 됩니다.
  • 타입 파라미터가 사용되는 제네릭 타입 또는 함수 간의 관계를 내포하는 이름을 사용합니다.
  • 의미있는 관계가 없다면 T, U, V처럼 영문자를 관례로 사용합니다.
  • 제네릭 타입을 확장할 때, 익스텐션(extension)의 정의의 일부로 타입 파라미터 리스트를 제공하지 않아도 됩니다.
  • 원본 타입 정의에서 타입 파라미터 리스트를 익스텐션의 바디 내에서 사용할 수 있고, 원본 타입 파라미터의 이름들은 원본 정의에서의 타입 파라미터를 참조하는데 사용됩니다.
  • 타입 제약은 타입 파라미터가 특정 클래스를 상속하거나 특정 프로토콜 또는 프로토콜 composition을 준수하도록 지정합니다.
  • 연관 타입은 프로토콜의 일부로 사용되는 타입의 placeholder 이름을 제공합니다. 이 연관 타입에 사용되는 실제 타입은 프로토콜이 적용될 때까지 구체화 되지 않습니다.
  • 이미 존재하는 타입에 프로토콜을 준수하도록 확장할 수 있습니다. 여기에는 연관 타입을 사용하는 포로토콜도 포함합니다.
  • 제네릭 where절을 사용하면 연관 타입이 특정 프로토콜을 준수해야 하거나 특정 파라미터와 연관 타입이 반드시 같도록 요구할 수 있습니다.
  • 연관 타입에 제네릭 where절을 포함시킬 수 있습니다.
  • 서브스크립트 또한 제네릭할 수 있고, 제네릭 where절을 포함할 수 있습니다.

 

Generics

제네릭(Generic) 코드는 요구사항에 따라 모든 타입에서 동작할 수 있는

유연하고 재사용 가능한 함수와 타입을 작성할 수 있도록 해줍니다.

제네릭 코드를 사용하면 중복을 피할 수 있으며 명확하고 추상적인 방법으로

그 의도를 표현할 수 있는 코드를 작성할 수 있습니다.

 

제네릭은 Swift에서 가장 강력한 기능 중 하나이며,

대부분의 Swift 표준 라이브러리는 제네릭 코드로 빌드됩니다.

Swift의 Array, Dictionary같은 Generic Collection 등 여기저기서 제네릭을 사용하고 있습니다.

Int 값을 포함하는 배열, String 값을 포함하는 배열 등을 생성할 수 있습니다.

마찬가지로 지정된 타입의 값을 저장하는 딕셔너리를 만들 수 있으며, 이 타입에는 제한이 없습니다.

 

The Problem That Generics Solve

제네릭을 사용하는 이유를 조금 더 알아봅시다.

다음의 함수 swapTwoInts(_:_:)는 non-generic 함수이며, 두 Int 값을 서로 바꿔주는 역할을 합니다.

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

이 함수는 inout 파라미터를 사용하여 a와 b의 값을 서로 바꿔줍니다.

swapTwoInts(_:_:) 함수는 다음과 같이 사용합니다.

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 temp = a
    a = b
    b = temp
}
 
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
    let temp = a
    a = b
    b = temp
}

swapTwoInts, swapTwoStrings, swapTwoDoubles는 모두 내용이 같지만 매개변수 타입만 다릅니다.

 

모든 타입의 값들을 서로 바꾸는 함수를 하나만 작성하면 매우 유용하고 상당히 유연하며,

이럴 때 제네릭이 유용합니다.

 

Generic Functions

제네릭 함수는 어떠한 타입에서도 동작 가능한데,

위에서 살펴봤던 swapTwoInts의 제네릭 버전은 다음과 같습니다.

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

 

swapTwoValues 함수의 바디는 swapTwoInts 함수와 동일합니다.

제네릭 버전의 함수와 일반 함수의 차이점은 아래와 같습니다.

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

제네릭 버전의 함수는 placeholder 타입 이름(ex. T)을 실제 타입 이름 대신 사용합니다.

placeholder 타입 이름에서 T가 무엇을 나타내는지 알려주지는 않지만,

a, b 모두 동일한 타입 T라는 것을 알려줍니다.

T 대신 사용할 실제 타입은 함수가 호출될 때마다 결정됩니다.

 

제네릭 함수와 일반 함수의 다른 차이점은 제네릭 함수 이름 뒤에

꺽쇠 괄호(<>) 사이에 placeholder 타입 이름이 나옵니다.

이 괄호는 Swift에게 swapTwoValues 내에서 T가 placeholder 타입 이름이라는 것을 알려줍니다.

T가 placeholder이기 때문에 Swift는 T를 실제 타입으로 보지 않습니다.

 

swapTwoValues 함수는 swapTwoInts와 동일한 방법으로 사용할 수 있는데,

모든 타입을 전달할 수 있다는 차이점이 있습니다.

아래 예제를 보면 Int 타입과 String 타입에 대해서 swapTwoValues를 호출하고 있으며,

매 호출마다 함수에 전달된 값의 타입에 의해 T에 사용할 타입이 추론됩니다.

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"

 

Type Parameters

위 예시의 T 처럼

타입 파라미터는 placeholder 타입을 지정하고 명명하고,

함수 이름 바로 뒤에 <>와 함께 작성됩니다.

 

타입 프로퍼티가 지정되면 이 타입을 함수 파라미터의 타입으로  사용하거나

함수의 리턴 타입, 함수 바디 내에서 타입 annotation으로 사용할 수 있습니다.

각각의 경우에서 타입 파라미터는 함수가 호출될 때마다 실제 타입으로 대체됩니다.

위 예에서 swapTwoValues의 T는 매 호출에서 Int, String으로 대체되었습니다.

 

하나 이상의 타입 파라미터를 사용할 수 있는데,

각 타입 파라미터는 꺽쇠 괄호 내에서 콤마로 구분해 작성하면 됩니다.

 

Naming Type Parameters

대부분의 경우 타입 파라미터는 딕셔너리의 Key, Value나 배열의 Element처럼

타입 파라미터가 사용되는 제네릭 타입 또는 함수 간의 관계를 내포하는 이름을 사용합니다.

의미있는 관계가 없다면 T, U, V처럼 영문자를 관례로 사용합니다.

타입 파라미터의 이름은 upper camel case로 명명하는 것을 권장합니다.
Swift에서 기본 제공되는 타입은 모두 upper camel case로 명명되어 있고
값이 아닌 타입이라는 것을 알려줍니다.

 

Generic Types

제네릭 함수 외에도 Swift 에서는 자신만의 제네릭 타입을 정의할 수 있습니다.

Array나 Dictionary처럶 어떠한 타입과 함께 동작할 수 있는

커스텀 클래스, 구조체, 열거형 등을 정의할 수 있습니다.

 

여기서 Stack 이라는 제네릭 콜렉션을 작성하는 방법에 대해 살펴보겠습니다.

스택은 Array와 유사하지만 FILO 특성을 갖는 자료구조입니다. (스택에 대한 설명은 여기로)

 

아래 코드는 non-generic 버전의 스택입니다.

이 스택은 Int 값에 대해 동작합니다.

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

이 구조체는 items라는 Array 프로퍼티를 사용하여 값들을 스택에 저장합니다.

이 스택에서는 push와 pop이라는 두 개의 메서드를 제공합니다.

 

IntStack 타입은 오직 Int값에 대해서만 사용할 수 있습니다.

여기서 제네릭 버전의 Stack 구조체를 사용하면 타입에 관계없이 스택을 관리할 수 있습니다.

다음은 제네릭 버전의 스택입니다.

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

제네릭 버전의 스택 기능은 non-generic 버전과 동일한데

Int 라는 실제 타입 대신 Element라는 타입 파라미터를 사용한 것만 다릅니다.

이 타입 파라미터는 <> 내에 작성되고 함수 이름 바로 뒤에 작성합니다.

 

Element는 나중에 제공되는 타입에 대한 placeholder 이름을 정의합니다.

미래에 추론되는 타입인 Element는 구조체 정의 내 어디에서나 사용될 수 있습니다.

 

위 Stack은 제네릭 타입이기 때문에 Swift에서 유효한 어떠한 타입의 스택을 생성하는데

사용할 수 있으며, Array나 Dictionary와 비슷한 방법으로 사용됩니다.

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// the stack now contains 4 strings
let fromTheTop = stackOfStrings.pop()
// fromTheTop is equal to "cuatro", and the stack now contains 3 strings

 

Extending a Generic Type

제네릭 타입을 확장할 때, 익스텐션(extension)의 정의의 일부로

타입 파라미터 리스트를 제공하지 않아도 됩니다.

대신 원본 타입 정의에서 타입 파라미터 리스트를 익스텐션의 바디 내에서 사용할 수 있고,

원본 타입 파라미터의 이름들은 원본 정의에서의 타입 파라미터를 참조하는데 사용됩니다.

 

다음 코드는 위에서 정의한 제네릭 Stack 타입을 확장하여

하나의 read-only 연산 프로퍼티를 추가합니다.

이 프로퍼티는 스택에서 pop하지 않고 top item을 리턴합니다.

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

topItem 프로퍼티는 Element 타입의 옵셔널 값을 리턴합니다.

만약 스택이 비어있다면 nil을 리턴하고 그렇지 않으면 items 배열의 마지막 아이템을 리턴합니다.

 

이 익스텐션은 타입 파라미터 리스트를 정의하지 않습니다.

그럼에도 Stack 타입에 존재하는 타입 파라미터인 Element를 익스텐션 내에서 사용할 수 있습니다.

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

제네릭 타입의 익스텐션에는 새로운 기능을 위한 확장된 타입의 인스턴스가 만족해야할

요구사항도 포함될 수 있는데,

이에 대해서는 Extensions with a Generic Where Clause 문단에서 다룹니다.

 

Type Constraints

위에서 살펴본 swapTwoValues(_:_:)와 Stack 타입은 어떠한 타입과도 함께 동작할 수 있습니다.

그러나 때때로 특정 타입에 대해서 타입 제약을 강제하는 것이 유용할 때가 있습니다.

타입 제약은 타입 파라미터가 특정 클래스를 상속하거나

특정 프로토콜 또는 프로토콜 composition을 준수하도록 지정합니다.

 

예를 들어 Swift의 Dictionary 타입엥서는 key로 사용되는 제약이 있습니다.

딕셔너리 key의 타입은 반드시 Hashable 해야 합니다.

즉, 반드시 유니크한 표현값으로 만드는 방법을 제공해야 합니다.

딕셔너리는 이러한 key를 특정 key에 대해 이미 값을 포함하고 있는지 체크하는데 사용합니다.

이러한 요구사항은 딕셔너리의 key 타입은 반드시 Hashable 프로토콜을 준수하도록 제약합니다.

 

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

아래 코드는 non-generic 함수 findIndex(ofString:in:)을 정의합니다.

func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

이 함수는 아래처럼 쓸 수 있습니다.

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"

하지만 이 함수는 오직 문자열에 대해서 동작하므로 유용하지 않습니다.

 

이때 String이 아닌 타입 T를 사용하여 제네릭 함수로 정의할 수 있습니다.

func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

하지만 위 코드는 컴파일이 되지 않습니다.

line 3의 ==에서 에러가 발생합니다.

Swift의 모든 타입이 == 연산자로 비교할 수 있는 것은 아니기 때문입니다.

따라서 모든 타입 T에 대해 == 연산자가 동작한다고 보장할 수 없으며 코드를 컴파일 할 때 에러가 발생합니다.

 

Swift 표준 라이브러리는 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
}

위 코드는 정상적으로 컴파일이 되고 동작합니다.

 

이제 이 함수는 Double이나 String 등에 대해서도 사용할 수 있습니다.

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)을 선언하는 것이 유용할 때가 있습니다.

연관 타입은 프로토콜의 일부로 사용되는 타입의 placeholder 이름을 제공합니다.

이 연관 타입에 사용되는 실제 타입은 프로토콜이 적용될 때까지 구체화 되지 않습니다.

연관 타입은 associatedtype 키워드로 지정합니다.

 

Associated Types in Action

아래 코드는 Container 라는 프로토콜을 정의합니다.

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

여기서 Item 이라는 연관 타입을 선언하고 있습니다.

 

이 프로토콜은 컨테이너에서 아이템이 저장되는 방법이나

어떤 타입이 허용되는지 구체적으로 지정하고 있지 않습니다.

이 프로토콜은 오직 Container로 사용되는 3가지 기능만을 지정합니다.

 

Container 프로토콜을 준수하는 어떤 타입은

저장하는 값의 타입을 지정해야 합니다.

구체적으로 올바른 타입의 아이템만 컨테이너에 추가되도록 보장해야 하고,

서브스크립트로 리턴되는 아이템의 타입에 대해서도 명확해야 합니다.

 

이러한 요구사항들을 정의하기 위해서 특정 컨테이너에 대한 타입이 무엇인지 모른채

컨테이너가 가질 수 있는 원소의 타입을 참조할 수 있는 방법이 필요합니다.

 

Item 이라는 연관 타입을 선언하여 Container 아이템의 타입을 참조할 수 있고,

append(_:)와 서브스크립트에서 사용할 수 있도록 타입을 정의합니다.

 

다음 코드는 Generic Types 문단에서 살펴본 non-generic 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 프로토콜의 모든 요구사항을 구현하고 있으며,

이러한 요구사항을 충족하기 위해 기존 기능의 일부를 wrapping 합니다.

또한 IntStack은 "typealis Item = Int"를 통해 Item을 Int 타입이라고 지정합니다.

 

Swift의 타입 추론 때문에 IntStack 정의에서 Int를 구체적인 Item으로 선언할 필요가 없습니다.

IntStack은 Container 프로토콜의 모든 요구사항을 준수하기 때문에

Swift는 단순히 append(_:)의 파라미터 타입과 서브스크립트의 리턴 타입을 보고

적절한 Item을 추론합니다.

따라서 "typealis Item = Int"을 지워도 무방합니다.

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(_:)의 파라미터 타입과 서브스크립트의 리턴 타입으로

사용되었습니다.

그래서 Swift는 Elemenet가 특정 컨테이너의 Item으로 사용하기에

적절한 타입이라고 추론할 수 있습니다.

 

Extending an Existing Type to Specify an Associated Type

이미 존재하는 타입에 프로토콜을 준수하도록 확장할 수 있습니다.

여기에는 연관 타입을 사용하는 포로토콜도 포함합니다.

 

Swift의 Array 타입은 이미 append(_:), count, 서브스크립트를 제공합니다.

이 3가지 기능은 Containere 프로토콜 요구사항과 일치합니다.

이는 Array가 이 프로토콜을 준수하도록 선언만 해주면

Array가 Container를 준수하도록 확장할 수 있다는 것을 의미합니다.

extension Array: Container {}

 

Adding Constraints to an Associated Type

프로토콜의 연관 타입에 타입 제약을 추가해서 이를 준수하는 타입이

이러한 제약을 만족하도록 제한할 수 있습니다.

예를 들어, 아래 코드는 컨테이너의 아이템이 Equatable 하도록 요구하기 위해

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 Protocal in Its Associated Type's Constraints

프로토콜은 자신의 요구사항 중 일부를 표현할 수 있습니다.

예를 들어, 아래 프로토콜은 suffix(_:) 요구사항을 추가하여 Container 프로토콜을 개량합니다.

suffix(_:)는 컨테이너 끝에서부터 시작해서 주어진 수의 원소들을 리턴하며

Suffix 타입의 인스턴스에 이들을 저장합니다.

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

이 프로토콜에서 Suffix는 Container 프로토콜에서의 Item 타입처럼 연관 타입입니다.

Suffix는 두 가지 제약을 가집니다.

하나는 SuffixableContainer 프로토콜을 준수해야 하고,

다른 하나는 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

위 코드에서 Suffix 연관 타입은 Stack이 됩니다.

따라서 Stack에서 suffix 연산은 Stack 타입을 리턴합니다.

 

SuffixableContainer를 준수하는 타입은 자신의 타입과 다른 Suffix 타입을 가질 수 있습니다.

예를 들어, 다음 코드는 IntStack 대신 Stack<Int>를 Suffix 타입으로 사용하여

SuffixableContainer를 준수하는 non-generic IntStack 타입에 대한 익스텐션입니다.

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절을 정의하는 방법으로 적용할 수 있습니다.

제네릭 where절을 사용하면 연관 타입이 특정 프로토콜을 준수해야 하거나

특정 파라미터와 연관 타입이 반드시 같도록 요구할 수 있습니다.

 

제네릭 where 절은 where 키워드로 시작하고,

그 뒤에 연관 타입에 대한 제약이나 타입과 연관 타입 간의 동등 관계가 옵니다.

제네릭 where절은 타입이나 함수의 본문의 { } 앞에 작성합니다.

 

다음 예제는 allItemsMatch라는 제네릭 함수를 정의합니다.

이 함수는 두 Container 인스턴스가 같은 순서로 같은 아이템을 포함하는지 체크합니다.

모든 아이템들이 같으면 true, 아니면 false를 반환합니다.

검사하는 두 개의 컨테이너는 동일한 타입일 필요는 없지만,

컨테이너의 아이템은 동일한 타입이어야 합니다.

이 요구사항은 타입 제약과 제네릭 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
}

함수의 두 파라미터 C1, C2는 다음의 요구사항들이 적용됩니다.

  • C1은 Container 프로토콜을 준수해야 합니다. (C1: Container)
  • C2는 Container 프로토콜을 준수해야 합니다. (C2: Container)
  • C1의 Item은 C2의 Item과 동일해야 합니다. (C1.Item == C2.Item)
  • C2의 Item은 Equatable 프로토콜을 준수해야 합니다. (C1.Item: Equatable)

첫 번째와 두 번째 요구사항은 타입 파라미터 리스트에서 정의되며,

세 번째와 네 번째 요구사항은 함수의 제네릭 where 절에서 정의됩니다.

 

이 요구사항들은 비록 다른 컨테이너 타입일지라도 두 컨테이너의 아이템을 비교할 수 있도록 해줍니다.

 

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."

stackOfString는 Stack 타입이고, arrayOfStrings는 Array 타입으로 

두 개의 컨테이너 타입은 다릅니다.

하지만 아이템의 타입은 둘다 String이므로 allItemsMatch(_:_:)로 비교가 가능합니다.

 

Extensions with a Generic Where Clause

제네릭 where절을 익스텐션의 일부로 사용할 수도 있습니다.

아래 예제 코드는 제네릭 Stack 구조체를 확장하여 isTop(_:) 메서드를 추가하고 있습니다.

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

제네릭 where 절을 사용하면 위와 같이 Stackd의 아이템이 오직 Equatable할 때만

익스텐션이 isTop(_:) 메서드를 추가하도록 할 수 있습니다.

 

isTop(_:)은 다음과 같이 사용 가능합니다.

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

 

제네릭 where절을 프로토콜의 익스텐션에 사용할 수도 있습니다.

아래 예제는 Container 프로토콜을 확장하여 startsWith(_:)를 추가합니다.

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

startsWith(_:)는 먼저 컨테이너가 최소한 하나의 항목을 가지고 있는지 확인한 다음,

컨테이너의 첫 번째 항목이 주어진 아이템과 동일한지 체크합니다.

이 startsWith(_:)는 Item이 Equatable 하다면

Container 프로토콜을 준수하는 어떤 타입에서도 사용할 수 있습니다.

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

 

위 코드처럼 제네릭 where 절은 Item이 프로토콜을 준수하도록 제한할 수도 있지만,

제네릭 where절을 사용하여 Item이 지정된 타입이 되도록 제한할 수도 있습니다.

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"

위 코드는 컨테이너에 average()를 추가하는데,

Item 타입이 Double인 컨테이너에만 적용됩니다.

 

익스텐션의 일부로 사용되는 제네릭 where절에 하나 이상의 요구사항들을 포함할 수도 있습니다.

각 요구사항은 콤마(,)로 구분합니다.

 

Contextual Where Clauses

이미 제네릭 타입인 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"

이 예제 코드는 Container의 아이템 타입이 정수일 때 average()를 추가하고,

아이템이 Equatable할 때 endsWith(:)를 추가합니다.

두 함수는 모두 제네릭 where절을 사용하여 제네릭 Item 타입 파라미터에 타입 제약을 추가합니다.

 

만약 contextual 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
    }
}

contextual where절을 사용한 버전에서는 average()와 endWith(_:)를 하나의 익스텐션에서 구현하고 있습니다.

하지만 이 요구사항을 익스텐션의 제네릭 where절로 이동시키면

각 요구사항마다 하나의 익스텐션이 필요하게 됩니다.

 

Associated Types with a Generic Where Clause

연관 타입에 제네릭 where절을 포함시킬 수 있습니다.

예를 들어, 표준 라이브러리의 Sequence 프로토콜이 사용하는 것과 같은

iterator를 포함하는 Container 버전을 만들고 싶다고 가정하겠습니다.

그러면 다음과 같이 작성할 수 있습니다.

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절을 사용하여

상속된 연관 타입에 제약을 추가합니다.

다음 코드는 ComparableContainer 프로토콜을 선언하는데,

Item이 Comparable을 준수하도록 요구합니다.

protocol ComparableContainer: Container where Item: Comparable { }

 

Generic Subscripts

서브스크립트 또한 제네릭할 수 있고, 제네릭 where절을 포함할 수 있습니다.

subscript 뒤에 <> 내부에 placeholder 타입 이름을 작성하고,

함수 바디의 { } 바로 전에 제네릭 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
    }
}

 

참고

https://docs.swift.org/swift-book/LanguageGuide/Generics.html

 

 

반응형