프로토콜과 제네릭
-제네릭은 유연하고 재사용 가능한 타입을 작성하는데 도움이 되는 범용 타입이다.
즉, 제네릭은 범용성을 위한 기능이다.
그럼 Protocol에서 범용성을 가지기 위해 제네릭을 사용하는건 어떨까요?
protocol MyProtocol<T> { //An associated type named 'T' must be declared in the protocol 'MyProtocol' or a protocol it inherits
func performAction(with item: T)
}
위 코드는 컴파일 오류를 발생시킨다. 이렇게 프로토콜에서 제네릭을 적용하는 것은 허용되지 않는다.
대신 프로토콜에서는 'associatedtype' 을 사용하여 제네릭과 같은 기능을 구현할 수 있다.
associatedtype이란?
-associatedtype은 프로토콜에서 제네릭 타입의 역할을 하는 키워드이다.
프로토콜이 특정 작업을 수행하는데 필요한 타입을 정의하지 않고, 프로토콜을 채택하는 구체적인 타입에 의해 결정되도록 할 수 있다.
associatedtype의 사용
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
'associatedtype Item'
컨테이너 프로토콜이 다루는 항목의 타입을 나타낸다.
이 타입은 프로토콜을 준수하는 곳에서 결정되고, 이를 통해 어떤 타입의 항목을 저장할 지 범용성 있게 다룰 수 있다.
'mutating func append(_ item: Item)'
컨테이너에 새로운 항목을 추가하는 기능이다. 'item' 매개변수의 타입은 위에서 설명했듯이 프로토콜을 준수하는 곳에서 결정된다.
'var count: Int {get}'
컨테이너 내부 요소의 수를 나타낸다.
'subscript(i: Int) -> Item {get}'
컨테이너의 특정 위치에 있는 항목을 가져오는 서브스크립트.
프로토콜 채택하기
struct IntContainer: Container {
typealias Item = Int
private var items = [Int]()
mutating func append(_ item: Int) {
items.append(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
구조체 'IntContainer' 는 'Container' 프로토콜을 준수한다.
'typealias Item = Int'
'associatedtype' 으로 지정했던 'Item' 을 'Int' 타입으로 지정한다.
'private var items = [Int]()'
내부적으로 Int 타입의 값들을 저장하는 배열.
'mutating func append { }'
Int 타입의 요소를 배열에 추가.
'var count: Int { }'
배열의 요소 개수를 반환.
'subscript(i: Int) -> Int { }'
주여진 인덱스에 맞는 요소를 반환.
이렇게 해당 프로토콜을 채택하는 곳에서 associatedtype 으로 지정했던 'Item' 에 대한 구체적인 타입을 지정하여,
한 타입에 한정되지 않는 유연한 프로토콜을 만들 수 있다.
Swift의 타입추론 기능
struct IntContainer: Container {
private var items = [Int]()
mutating func append(_ item: Int) {
items.append(item)
}
...
}
Swift의 타입 추론 기능을 통해 이용하여
굳이 'typealias Item = Int' 코드를 작성하지 않고 사용하는 것도 가능하다.
associatedtype과 타입제약
-제네릭에 타입 제약을 걸었듯이,
associatedtype에도 'where' 절을 이용하여 타입에 제약을 걸 수 있다.
1. 'Stack' 프로토콜 정의.
protocol Stack {
associatedtype Element
mutating func push(_ item: Element)
mutating func pop() -> Element?
}
'push()' 메서드와 'pop()' 메서드를 통해 각각 스택에 항목을 추가하고, 항목을 제거하는 동작을 정의한다.
'Element' 라는 associatedtype 을 통해
메소드에 전달되고, 반환될 항목의 타입을 범용성 있게 만든다.
2. 'Stackable' 프로토콜 정의.
protocol Stackable {
associatedtype StackType: Stack
associatedtype ItemType where ItemType == StackType.Element
var stack: StackType { get set }
mutating func addToStack(item: ItemType)
mutating func removeFromStack() -> ItemType?
}
'associatedtype StackType: Stack'
연관 타입 'StackType' 은 'Stack' 프로토콜을 준수하는 타입이어야 한다.
'associatedtype ItemType where ItemType == StackType.Element'
연관 타입 'ItemType' 은 'StackType' 의 'Element' 타입과 동일해야 한다.
즉, Stack에 추가되거나 제거될 수 있는 아이템의 타입을 말한다.
'var stack: StackType {get set}'
'StackType' 의 스택을 나타낸다.
'mutating func addToStack(item: ItemType)'
'ItemType' 의 아이템을 스택에 추가한다.
'mutating func removeFromStack() -> ItmeType'
스택에서 'ItemType' 의 아이템을 제거하고, 반환한다.
스택이 비어있을 경우 'nil' 을 반환한다.
3. 'Stack' 및 'Stackable' 프로토콜 적용.
struct IntStack: Stack {
private var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int? {
return items.popLast()
}
}
struct StackUser: Stackable {
typealias StackType = IntStack
typealias ItemType = Int
var stack: IntStack
mutating func addToStack(item: Int) {
stack.push(item)
}
mutating func removeFromStack() -> Int? {
return stack.pop()
}
}
'IntStack' 은 'Stack' 프로토콜을 채택하며, 연관 타입 'Element' 를 타입 추론을 통하여 'Int' 타입으로 지정했다.
정수 배열 'Items' 를 내부적으로 선언하고, 'push()' 와 'pop()' 을 통해 배열 요소를 관리한다.
'StackUser' 는 'Stackable' 프로토콜을 채택하며,
연관타입 'StackType' 을 'Stack' 프로토콜을 채택한 'IntStack' 구조체로 선언하고,
연관타입 'ItemType' 을 'Stack' 프로토콜의 'Element' 타입과 동일한 'Int' 타입으로 선언했다.
정리하면..
'Stack' 과 'Stackable' 프로토콜을 사용하여 스택이라는 자료구조가 작동하는 구조를 보여준다.
1. 'IntStack' 구조체는 'Stack' 프로토콜을 준수하며, 정수 타입의 스택을 구현하고 있다.
'push()' 와 'pop()' 메서드를 통해 각각 스택에 항목을 추가하고 제거하는 기능을 수행한다.
2. 'Stackable' 프로토콜은 어떤 'Stack' 타입이든 다룰 수 있지만, 스택의 요소 타입과 'ItemType' 은 일치해야 한다.
'StackUser' 구조체는 'Stackable' 프로토콜을 준수하며, 'IntStack' 구조체를 사용하여 작동한다.
3. 'addToStack()' 과 'removeFromStack()' 메서드는 각각 'IntStack'에 항목을 추가하고 제거한다.
따라서 예제를 통해 associatedtype 과 타입제약을 사용하여 프로토콜의 유연성을 유지하면서도,
필요한 타입의 안전성을 보장하는 방법에 대해서 배울 수 있다.
Swift 표준 라이브러리에서 사용되는 associatedtype
-Swift에서 associatedtype과 타입 제약의 사용은 많이 있지만, 그 중에서 'Sequence' 프로토콜의 사례를 알아보자.
'Sequence' 프로토콜이란?
-순차적으로 접근할 수 있는 값들의 집합을 정의하며, for-in 구문을 통해 요소들을 순환하기 위해서는 'Sequence' 프로토콜을 준수하고 있어야 한다.
예를 들어 Array, Set, Dictionary 등 다양한 타입이 'Sequence' 프로토콜을 준수하고 있다.
public protocol Sequence {
associatedtype Element
}
Swift의 표준 라이브러리를 보면 위 코드와 같이 'Sequence' 프로토콜이 정의되어 있다.
associatedtype인 'Element' 는 시퀸스의 개별 요소의 타입을 나타낸다.
만약, [Int] 타입의 배열이면 'Element' 는 Int 타입이 되는 것이다.
associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
'Sequence' 프로토콜 정의에서는 이렇게 타입 제약을 사용한 코드도 볼 수 있다.
associatedtype인 'Iterator' 는 'IteratorProtocol' 을 준수한 타입이어야 하고,
'Iterator' 가 가지는 Element 타입은 'Sequence' 프로토콜의 'Element' 타입과 동일해야 한다는 뜻이다.
'Sequence' 프로토콜을 준수하기 위해서는 'func makeIterator()' 메서드를 구현해주어야 하는데,
이 메서드의 반환타입이 'Iterator' 타입이다.
우리가 for-in 구문을 통해 배열을 순회하려고 할 때, 내부적으로는 'makeIterator()' 가 실행되는 것이다.
따라서 타입제약을 통해 시퀸스의 요소타입과 반복을 도와주는 Iterator 타입의 일관성을 가져오고, 이것은 코드의 안전성을 향상시켜줄 수 있다.
associatedtype의 이점
1. 유연성
프로토콜의 제네릭과 같은 기능이기 때문에 특정 타입에 한정되지 않는 유연성을 가질 수 있다.
2. 재사용성
하나의 프로토콜을 다양한 타입과 함께 사용할 수 있도록 도와준다.
Swift의 'Sequence' 프로토콜에서 보았듯이 'Int' 배열, 'String' 배열 등 다양한 타입에서 사용하는
재사용성이 향상된다.
3. 타입 안전성
associatedtype 에 타입 제약을 추가하여, 해당 타입이 특정 프로토콜을 준수하도록 제약을 걸 수 있다.
이 기능을 통해 잘못된 타입의 사용을 방지할 수 있고, 안전성과 정확성을 향상시켜준다.
Reference:
https://github.com/apple/swift/blob/main/stdlib/public/core/Sequence.swift
'개린이 이야기' 카테고리의 다른 글
Git에 관하여 (0) | 2023.06.25 |
---|---|
Swift의 API Design Guidelines (0) | 2023.06.12 |
Protocol Composition에 관하여 (0) | 2023.06.06 |
Protocol-Extension에 관하여 (0) | 2023.06.05 |
타입으로서 프로토콜에 관하여 (0) | 2023.06.05 |