[Swift] 제네릭2 (Generic)

연관 타입(Associated Types)

연관 타입은 프로토콜에서 자리 표시자의 이름을 제공

프로토콜은 제네릭의 타입 파라미터를 사용하지 못함

연관타입에 사용할 실제 타입은 프로토콜이 채택되어 구현될 때 까지 정해지지 않는다

연관 타입은 키워드 associatedtype로 선언할 수 있다

연관 타입의 사용

다음은 Item이라는 연관 타입을 선언하는 Container 프로토콜의 예시이다.

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

프로토콜 Container는 다음과 같은 기능을 요구한다

  1. append(_:) 메서드를 사용하여 새 항목을 추가
  2. Int 값을 반환하는 count 프로퍼티를 통해 아이템의 수를 반환하여야 함
  3. Int 타입의 인덱스 값을 사용하는 subscript를 사용하여 항목을 검색할 수 있어야함

이 프로토콜은 Item이 어떤 타입이어야 하는지 정하지 않는다

다만, Container 프로토콜을 준수하는 타입에서 정할 수 있도록함

Item이 어떤 타입인지는 모르나 append(_:) 메서드의 매개변수와

subscript를 통해 반환되는 타입이 Item 타입, 즉 동일한 타입이어야 한다는 건 알 수 있음

아래의 예는 일반 Type의 IntStack에서 Container 프로토콜을 채택하는 예이다

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

    typealias Item = Int
    mutating func append(_ item: Int) {
        push(item)
	}
    var count: Int { return items.count }
    subscript(i: Int) -> Int {
        return items[i]
    }
}

IntStackContainer 프로토콜의 모든 요구사항을 구현하였다.

눈여겨 볼 코드는 typealias Item = Int 이다.

이 코드로 인해 ItemInt 타입이라는 구체적인 타입으로 지정한다.

Swift의 타입 추론이 있어 append(_ item: Int) 메서드의 item 매개변수 타입과

subscript(i: Int)를 보고 Item에 적용할 구체적인 타입을 타입을 추론할 수 있다.

따라서 위의 코드에서 typealias Item = Int 코드는 삭제하여도 잘 동작한다

제네릭 타입에서 연관타입이 있는 프로토콜을 사용하는 경우도 살펴보자

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

    mutating func append(_ item: Element) {
        push(item)
    }
    var count: Int { return items.count }
    subscript(i: Int) -> Element {
        items[i]
    }
}

위 코드에서는 Stack의 타입 파라미터 Elementappend(_:) 메소드의 입력 매개변수와

subscript(i: Int)의 반환 타입으로 사용한것을 볼 수 있다.

Stack이 실제로 인스턴스화 되었을 때 Stack타입 파라미터Container연관 타입의 타입이

결정될 것이다.

// 이 때 Stack의 Element, Container의 Item의 타입이 결정
var stack = Stack<String>() 

기존 타입을 확장하여 연관 타입을 지정 (Extending an Existing Type to Specify an Associated Type)

기존 타입을 확장하여 프로토콜을 채택할 수 있는다.

여기에는 연관 타입도 포함되어 적용할 수 있다.

extension Array: Container { }

Array 타입은 Container의 요구사항인 append(_:), count, subscript(i: Int)

이미 모두 구현하고 있기 때문에 Container를 채택하기만 한다면 아무 추가 작업 없이

Array를 확장할 수 있다.

Container 프로토콜의 연관 타입인 Item은 위에 제네릭 타입의 Stack 예제에서와 같이

Array의 타입 파라미터 Element 타입으로 유추되어 사용되어 질 것이다.

@frozen public struct Array<Element>

연관 타입의 제약조건 추가 (Adding Constraints to an Associated Type)

제네릭 타입에서 타입 파라미터에 제약조건을 추가한 것 처럼 연관 타입에도 제약조건을 추가, 설정할 수 있다.

예를 들어 다음 코드는 ContainerItem이 동등을 비교할 수 있는 타입이어야 한다는 제약조건을 설정한

코드이다

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

Equatable 프로토콜을 지원하는 타입만 동등을 비교하는 연산자 ==, != 을 사용할 수 있기 때문에

제약 조건으로 Equatable을 설정한 코드이다.

프로토콜에서 연관 타입 제약조건 사용

프로토콜은 자체 요구사항의 일부로 사용될 수 있다.

다음 코드를 보자

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

Container 프로토콜을 상속받아 새로운 요구사항인 suffix(_:)를 추가한 SuffixableContainer 프로토콜이다.

suffix(_:) 메서드는 주어진 size만큼 Container 타입의 끝에서 반환하여

Suffix 타입의 인스턴스를 반환하는 메서드이다.

SuffixContainerItem과 마찬가지로 연관 타입이며 두 가지 제약조건이 있다.

  1. SuffixableContainer 프로토콜을 채택한 타입이어야 한다.
  2. Suffix 타입의 ItemContainerItem과 같아야 한다.

이 제약조건은 제네릭 Where절을 사용하여 지정한다

이러한 SuffixableContainer 프로토콜을 채택하여 사용하는 예를 보자

extension Stack: SuffixableContainer {
    func suffix(_ size: Int) -> Stack {
        var result = Stack()
        for index in (count - size) ..< count {
            result.append(self[index]
        }
        return result
    }	
}

suffix(_:) 메서드는 Stack을 반환하며 이에 Suffix 연관 타입은 Stack 타입으로 구체화 되었다.

Stack은 제네릭 타입으로 extension을 사용하며 파라미터 타입이 생략되었지만

Stack의 파라미터 타입은 Element이다.

suffix(_:) 메서드는 Stack<Element> 타입을 반환하는 메서드이다.

Stack 타입은 SuffixableContainer를 채택한 타입이다. 위의 조건 1을 만족한다

resultStack 타입의 인스턴스이며 Stack() 또한 Stack<Element>() 를 생략한 것이다

suffix(_:) 메서드의 반환 타입 Stack<Element>result의 타입은 Stack<Element>로 동일하기 때문에 조건 2를 만족한다.

이러한 조건만 만족한다면 다른 타입을 반환할 수 있다.

아래 예제를 보자

extension IntStack: SuffixableContainer {
    func suffix(_ size: Int) -> Stack<Int> { 
        var result = Stack<Int>()
        for index in (count - size) ..< size {
            result.append(self[index])
        }
        return result
    }
}

Stack<Int>를 반환하고 있다. 즉 IntStack에서 연관 타입 SuffixStack<Int> 타입이다.

Stack<Int> 타입은 SuffixableContainer를 확장으로 채택하였기 때문에 조건 1을 만족한다

Stack<Int> 타입의 Item 타입은 Int이며 IntStackItem 또한 Int 이기 때문에

조건 2를 만족한다.

이렇듯 연관 타입의 조건만 만족한다면 다른 타입의 인스턴스를 반환할 수 있다

제네릭 Where절 (Generic Where Clauses)

타입 제약 조건을 사용하면 제네릭 함수, subscript, 타입과 연관타입에 대한 요구사항을 정의할 수 있다

이 때 요구사항은 Where절을 사용하여 정의하고 이를 제네릭 Where절이라고 한다.

제네릭 Where절을 사용하면

  1. 연관 타입이 특정 프로토콜을 준수해야 하거나
  2. 특정 타입 파라미터와 연관 타입이 동일해야 한다고 요구할 수 있다

제네릭 Where절은 where로 시작하고 그 뒤에 연관 타입의 제약조건, 연관 타입과 타입간의 동등 관계를 정의할 수 있다.

여는 중괄호 { 앞에 where 키워드를 작성한다

아래 예를 보자

func allItemsMatch<C1: Container, C2: Container>
    (_ someContainer: C1, _ anotherContainer: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {}

제네릭 함수 allItemsMatch는 다음과 같은 요구 사항이 존재한다

  1. C1Container를 준수하는 타입이어야 한다
  2. C2Container를 준수하는 타입이어야 한다
  3. C1C2Item 타입이 같아야 한다
  4. C1Item 타입은 Equatable 프로토콜을 준수하는 타입이어야 한다

C1C2Item 타입이 같다면 C1Item 타입만 Equatable 하면 C2 또한 Equatable을 준수하는 타입일 것이다.

확장에서 제네릭 Where절 사용 (Extensions with a Generic Where Clause)

확장에서도 제네릭 Where절을 사용할 수 있다.

다음은 제네릭 Stack을 확장하는 예제이다.

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

isTop(_:) 메서드는 전달받은 itemitems의 마지막 item과 같은지 비교하는 메서드이다.

동등을 비교하기 위해서는 Equtable 프로토콜을 채택한 타입만 가능하기 때문에

ElementEqutable채택한 타입일 때만 확장하여 isTop(_:) 메서드를 사용하도록

확장에 제약 조건을 정의할 수 있다

제네릭 타입이 아닌 프로토콜을 확장할 때에도 where절을 사용할 수 있다

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

연관 타입 ItemEqutable을 채택한 타입일 때만 확장하여 startWith(_:) 메서드를 사용할 수 있다.

위의 예제에서 제네릭 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)
    }
}

문맥적 Where절 (Contextual Where Clauses)

제네릭 타입의 컨텍스트에서 작업 중일 때 개별 메서드, subscript에 where절을 작성할 수 있다.

예를 들어 Container의 구조는 제네릭이며 이를 확장하여 새로운 메서드를 정의할 때 where절을 사용하여

새로운 메서드를 사용하기 위해 충족해야 하는 타입 제약 조건을 정의할 수 있다

extension Container {
    func average() -> Double where Item == Int {
        var sum = 0.0
        for idx in 0 ..< count {
            sum += Double(self[idx])
        }
        return sum / Double(count)
    }

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

average() 메서드는 Item 타입이 Int 타입일 때 사용이 가능하며

endWith(_:) 메서드는 ItemEquatable 프로토콜을 준수할 때만 사용할 수 있다

let intArr = [1,2,3,4,5]
let strArr = ["hello","sweet","food"]
print(intArr.average()) // 3.0
print(strArr.average()) // 컴파일 에러
print(strArr.endWith("food")) // true

만약 extension 자체에 where절을 사용한다면 where절에 대해 각각 하나씩 확장을 하여

작성해야 한다

extension Container where Item == Int {
    func average() -> Double { ... }
}
extension Container where Item: Equatable { 
    func endWith(_ item: Item) -> Bool { ... }
}

연관 타입과 제네릭 Where절

앞서 SuffixableContainer 코드 처럼 연관 타입에도 제네릭 Where절을 사용할 수 있다.

protocol Container {
    associatedtype Item
    ...
    associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
    func makeIterator() -> Iterator
}

위 코드는 연관타입 IteratorIteratorProtocol을 준수하는 타입이어야 하며

IteratorElement 타입이 ContainerItem 타입과 같아야 하는 제약조건을 가지는 코드이다

만일 프로토콜이 다른 프로토콜을 상속하는 경우 프로토콜의 선언에서 where절을 작성하여

상속받는 연관타입에 제약 조건을 설정할 수 있다.

protocol ComparableContainer: Container where Item: Comparable { }

ComparableContainer 프로토콜은 Container 프로토콜을 상속 하기 때문에

Container의 연관 타입 Item을 상속 받는데 이 때 Where절을 사용하여 Item

제약조건을 설정할 수 있다

제네릭 Subscripts (Generic Subscript)

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

댓글남기기