BOID
[swift] 제네릭이란?(2/2) 본문
안녕하세요 HoonIOS입니다 :)
저번에는 제네릭의 개념과 제네릭 함수, 타입, 타입 확장에 대해 알아봤는데요, 이번에는 타입 제약 즉 특정 타입에 한정되게 제약을 두는 것과 프로토콜과 서브 스크립트에서 제네릭을 사용하는 것에 대해 알아보겠습니다.
제네릭의 타입 제약
- 앞 포스팅에서는 제네릭 타입 제약을 받지 않는 제네릭 함수를 구현해 보았는데, 만약 특정 타입에서만 한정되어야 할 처리가 있다던가, 제네릭 타입을 특정 프로토콜을 따른느 타입만 사용할 수 있도록 제약을 두어야 하는 상황이 있으면, 이때 타입 제약을 통해서 제약사항을 지정해 줄 수 있습니다.
- 타입 제약은 클래스 또는 프로토콜에서만 줄수 있습니다. 이 말은 즉 타입 매개변수 자리에 사용할 실제 타입이 특정 클래스를 상속받는 타입이어야 한다든지, 특정 프로토콜을 준수하는 타입이어야 한다는 제약을 줄 수 있다는 말입니다.
- 따라서 위의 조건으로 인해 열거형, 구조체 타입은 제네릭 타입제약을 사용할 수 없다는 말이 되겠습니다
- 표준 라이브러리인 Dictionary 라이브러리를 통해 예를 들어 설명을 해보겠습니다.
public struct Dictionary<key: Hashable,Value>: Collection, ExpressibleByDictionaryLiteral{/*...*/}
- 위 Dictionary 제네릭 코드를 보면 플레이이스 홀드(key) 뒤에 보면 클론( : )을 붙인 다음 Hashable이라고 명시되어 있는 것을 확인할 수 있는데 이것을 이 타입 매개변수인 key는 Hashable 프로토콜을 준수한다는 뜻이 됩니다.
- 위 예제처럼 제네릭 타입에 제약을 주고싶으면 타입 매개변수 뒤에 클론을 붙인 후 원하는 클래스 타입 또는 프로토콜을 명시해주면 됩니다.
- 여러 제약을 추가하고 싶으면 콤마로 구분해주는게 아니라 where 절을 사용하여 제약 조건을 추가해 줄 수 있습니다. (where 절은 추후에 포스팅할 예정입니다.)
- 예시를 통해 추가 제약조건을 주는 방법을 알아보겠습니다.
func swapTwoValues<T: BinaryInteger>( _ a: inout T, _ b: inout T) where T: FloatingPoint {
//함수 구현
}
- 위 swapTowValue라는 함수에 T의 제약조건을 BinaryInteger과 where을 통해 FloatingPoint라는 프로토콜 제약조건을 추가해 주었습니다. 따라서 이 두 프로토콜을 준수하는 타입만 사용할 수 있습니다.
- 그렇지만 BinaryInteger은 apple doucumentation을 보면 An integer type with a binary representation. 라고 명시되는 것을 볼 수 있는데요 정수형 타입이라는 것을 볼 수 있습니다.
- FloatingPoint 프로토콜은 A floating-point numeric type.인 즉 부동소수점이라고 명시되어 있습니다.
- 따라서 BinaryInteger, FloatingPoint 프로토콜을 만족하는 기본타입은 없어 함수를 중복 정의하거나, 새로운 프로토콜 타입을 정의해서 사용하는 등 다른 방법을 사용해야 합니다 ㅎㅎㅎㅎㅎㅎㅎㅎ
- 그러면 이제 기본 조건을 사용할수 있는 타입 제약 예시를 한번 들어보겠습니다.
func substract TwoValue<T>( _ a: T, _ b: T) -> T {
return a-b
}
- 위 예시를 보면 위 제네릭 함수는 결과값이 a-b이므로 그에 맞는 타입이 들어가야 되는 조건이 있어야 됩니다. 다시 말해 뺄셈 연산자를 사용할 수 있는 타입이 있어야 한다는 말입니다!
- 위 코드를 아래와 같이 제약 조건을 한번 걸어줘보겠습니다.
func substract TwoValue(T: BinaryInteger>( _ a: T, _ b: T) -> T {
return a - b
}
- T의 타입을 BinaryInteger 프로토콜을 준수하는 타입으로 한정해주어 뺄셈 계산이 가능하게 해 주었습니다.
- 위 예제처럼 타입 제약은 함수 내부에서 실행해야 할 연산에 따라 적절한 타입을 전달받을 수 있도록 제약을 둘 수 있습니다.
- 여기서 잠깐 표준 라이브러리에서 정의되어 있는 프로토콜 중 타입 제약에 자주 사용될만한 프로토콜에 대해 알아보겠습니다.!
- Hashable, Equatable, Comparable, Indexable, IteratorProtocol, Error, Collection, CustomStringConvertible 등이 있습니다.
혹시 이거 말고 자주 쓰는 프로토콜 있으면 말해주세요!
프로토콜의 연관 타입
- 연관 타입은 프로토콜에서 사용할 수 있는 플레이스 홀더 이름입니다.
- 다시 말해서, 제네릭에서는 어떤 타입이 들어있을지 모를 때, 타입 매개변수를 통해 '종류는 알 수 없지만, 어떤 타입이 여기에 쓰일 거예요'라고 표현해주면 연관 타입은 타입 매개변수의 그 역할을 프로토콜에서 수행할 수 있도록 만들어진 기능입니다.
- 프로토콜을 정의할 때, 연관 타입을 함께 정의하면 유용할 때가 있어요!
- 그냥 말로 설명하면 모르니깐 예제를 사용해 보겠습니다.
protocol Container {
associatedtype itemType
var count: Int { get }
mutating func append(_ item: itemType)
subscript(i: Int) -> ItemType { get }
}
- 프로토콜을 보면 associatedType인 연관 타입에 존재하지 않는 타입인 itemType을 연관 타입으로 지정하여 프로토콜 정의에서 타입 이름으로 활용합니다.
- 제네릭의 타입 매개변수와 유사한 기능으로, 프로토콜 정의 내부에서 사용할 타입이 '그 어떤 것이어도 상관없지만, 하나의 타입임은 분명하는 의미로 무조건 하나 타입은 있습니다'라는 뜻입니다.
- 그럼 이제 Container 프로토콜에 있는 itemType의 타입이 구현해야 할 기능을 생각해 보겠습니다
- 새로운 아이템을 컨테이너에 append(_:) 메서드를 통해 추가할 수 있어야 합니다.
- 아이템 개수를 확인할 int형 변수 count를 구현해야 합니다.
- 아이템 개수를 확인할 수 있도록 Int타입의 값을 갖는 count프로퍼티를 구현해야 한다.
- Int 타입의 인덱스 값으로 특정 인덱스에 해당하는 아이템을 가져올 수 있는 서브 스크립트를 구현해야 합니다.
- 위 Container프로토콜을 클래스에 적용해 프로토콜에 충족할 수 있는 예제를 작성해 보겠습니다.
class MyContainer: Container {
var items: Array<Int> = Array<Int>()
var count: Int {
return self.items.count
}
func append( _ item: Int) {
items.append(item)
}
subscript(i: Int) -> Int {
return items[i]
}
}
- 위 MyContainer 클래스를 보시면 Container 프로토콜을 충족한 것을 확인할 수 있고 ItemType은 실제 타입인 Int타입으로 구현해 준 것을 확인할 수 있습니다.
- 프로토콜에서 ItemType이라는 연관 타입만 지정했고, 특정 타입을 지정하지 않았으므로 프로토콜의 요구사항을 모두 충족하는 데는 큰 문제는 없습니다.
- 실제 프로토콜 정의를 준수하기 위해서는 연관 타입을 하나의 타입으로 일관성 있게 구현해주면 됩니다.
-이제 Stack에다가 Container 프로토콜을 정의해 보겠습니다.
struct Stack<Element>: Container {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
//Container 프로토콜 구현하기
mutating func append(_ item: Element) {
self.push(item)
}
var count: Int {
return self.items.count
}
subscript(i: Int) -> Element {
return items[i]
}
}
- Stack 구조체에서 Container에서 정의했던 ItemType이라는 연관 타입 대신에 Element라는 타입 매개변수를 사용했습니다. 그럼에도 Stack 구조체는 Container 프로토콜을 완벽히 준수했습니다.
따라서, 구조체에서 타임 매개변수를 다시 정의해서 그 매개변수를 사용했던 상관없다는 것을 알 수 있습니다.
서브 스크립트에서 제네릭 사용
- 서브 스크립트도 제네릭을 활용하여 타입에 큰 제한 없이 유현하게 구현할 수 있습니다. 물론 타입 제약을 사용하여 제네릭을 활용하는 타입에 제약을 줄 수도 있습니다.
- extension(확장) Stack에서 제네릭이 있는 서브 스크립트를 적용해 보겠습니다.
extension Stack {
subscript<Indices: Sequence>(indices: Indicies) -> [Element] where Indicies.Iterator.Element == Int {
var result = [ItemType]()
for index in indicies {
result.append(self[Index])
}
return result
}
}
var integerStack: Stack<Int> = Stack<Int>()
integerStack.append(1)
integerStack.append(2)
integerStack.append(3)
integerStack.append(4)
integerStack.append(5)
print(integerStack[0...2]) //[1,2,3]
- 위 코드의 subscript함수를 해석해보면 서브 스크립트는 indices라는 플레이스 홀더를 사용하여 매개변수르르 제네릭하게 받아들일수 있습니다.
- subscript의 매개변수인 indicies는 Sequence프로토콜을 준수하는 타입으로 제약이 추가되어 있습니다. 또, where조건을 보시면 Indicies타입 Iterator의 Element 타입이 Int타입이어야 하는 제약이 추가되었습니다.
- 서브스크립트는 Indicies 타입의 indicies라는 매개변수(indices: Indicies)로 인덱스 값을 받을 수 있습니다. 그 결과 indicies 시퀀스의 인덱스 값에 해당하는 스택 요소의 값을 배열로 받아볼 수 있습니다.
예를 들어, 위에 print값을 보면은 [0... 2]의 인덱스를 통해 [1,2,3] 값을 받는 것을 확인할 수 있습니다.
총 두 번의 포스팅을 통해서 제네릭을 설명해 보았는데요, 공부하면서 느낀 거지만 제네릭을 유용하게 사용하면 간단하고 쉽게 코딩을 할 수 있겠다는 생각을 했습니다.
앞으로 코딩할 때 제네릭을 사용하도록 노력해봐야겠습니다 ㅎㅎ
긴 글 읽어주셔서 감사합니다 :) 오늘도 좋은 하루 되시고 내용이 도움이 되었으면 좋겠네요 ㅎㅎ
'swift 시작기' 카테고리의 다른 글
[swift] 스위프트에서 사용하는 패턴(와일드카드 패턴, 식별자 패턴, 바인딩 패턴, 튜플 패턴) - HoonIOS (3) | 2021.03.26 |
---|---|
[swift] 타입 중첩이란? (0) | 2021.03.23 |
[Swift] 프로토콜 지향 프로그래밍이란? (1) | 2021.03.21 |
[swift] 제네릭이란?(1/2) (0) | 2021.03.18 |
[swift] 확장(extensions)이란? (0) | 2021.03.16 |