[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
는 다음과 같은 기능을 요구한다
append(_:)
메서드를 사용하여 새 항목을 추가Int
값을 반환하는count
프로퍼티를 통해 아이템의 수를 반환하여야 함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]
}
}
IntStack
은 Container
프로토콜
의 모든 요구사항을 구현하였다.
눈여겨 볼 코드는 typealias Item = Int
이다.
이 코드로 인해 Item
을 Int
타입이라는 구체적인 타입으로 지정한다.
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
의 타입 파라미터 Element
를 append(_:)
메소드의 입력 매개변수와
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)
제네릭 타입에서 타입 파라미터에 제약조건을 추가한 것 처럼 연관 타입에도 제약조건을 추가, 설정할 수 있다.
예를 들어 다음 코드는 Container
의 Item
이 동등을 비교할 수 있는 타입이어야 한다는 제약조건을 설정한
코드이다
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
타입의 인스턴스를 반환하는 메서드이다.
Suffix
는 Container
의 Item
과 마찬가지로 연관 타입이며 두 가지 제약조건이 있다.
SuffixableContainer
프로토콜을 채택한 타입이어야 한다.Suffix
타입의Item
은Container
의Item
과 같아야 한다.
이 제약조건은 제네릭 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을 만족한다
result
는 Stack
타입의 인스턴스이며 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
에서 연관 타입 Suffix
는 Stack<Int>
타입이다.
Stack<Int>
타입은 SuffixableContainer
를 확장으로 채택하였기 때문에 조건 1을 만족한다
Stack<Int>
타입의 Item
타입은 Int
이며 IntStack
의 Item
또한 Int
이기 때문에
조건 2를 만족한다.
이렇듯 연관 타입의 조건만 만족한다면 다른 타입의 인스턴스를 반환할 수 있다
제네릭 Where절 (Generic Where Clauses)
타입 제약 조건을 사용하면 제네릭 함수, subscript, 타입과 연관타입에 대한 요구사항을 정의할 수 있다
이 때 요구사항은 Where절을 사용하여 정의하고 이를 제네릭 Where절이라고 한다.
제네릭 Where절을 사용하면
- 연관 타입이 특정 프로토콜을 준수해야 하거나
- 특정 타입 파라미터와 연관 타입이 동일해야 한다고 요구할 수 있다
제네릭 Where절은 where로 시작하고 그 뒤에 연관 타입의 제약조건, 연관 타입과 타입간의 동등 관계를 정의할 수 있다.
여는 중괄호 {
앞에 where
키워드를 작성한다
아래 예를 보자
func allItemsMatch<C1: Container, C2: Container>
(_ someContainer: C1, _ anotherContainer: C2) -> Bool
where C1.Item == C2.Item, C1.Item: Equatable {}
제네릭 함수 allItemsMatch
는 다음과 같은 요구 사항이 존재한다
C1
은Container
를 준수하는 타입이어야 한다C2
는Container
를 준수하는 타입이어야 한다C1
과C2
의Item
타입이 같아야 한다C1
의Item
타입은Equatable
프로토콜을 준수하는 타입이어야 한다
C1
과 C2
의 Item
타입이 같다면 C1
의 Item
타입만 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(_:)
메서드는 전달받은 item
이 items
의 마지막 item
과 같은지 비교하는 메서드이다.
동등을 비교하기 위해서는 Equtable
프로토콜을 채택한 타입만 가능하기 때문에
Element
가 Equtable
을 채택한 타입일 때만 확장하여 isTop(_:)
메서드를 사용하도록
확장에 제약 조건을 정의할 수 있다
제네릭 타입이 아닌 프로토콜을 확장할 때에도 where절을 사용할 수 있다
extension Container where Item: Equtable{
func startWith(_ item: Item) -> Bool {
return count >= 1 && item == self[0]
}
}
연관 타입 Item
이 Equtable
을 채택한 타입일 때만 확장하여 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(_:)
메서드는 Item
이 Equatable
프로토콜을 준수할 때만 사용할 수 있다
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
}
위 코드는 연관타입 Iterator
는 IteratorProtocol
을 준수하는 타입이어야 하며
Iterator
의 Element
타입이 Container
의 Item
타입과 같아야 하는 제약조건을 가지는 코드이다
만일 프로토콜이 다른 프로토콜을 상속하는 경우 프로토콜의 선언에서 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
}
}
댓글남기기