제네릭(Generic)을 이용해 코드를 구현하면 어떤 타입에도 유연하게 대응할 수 있습니다.
제네릭으로 구현한 기능과 타입은 재사용하기도 쉽고, 코드의 중복을 줄일 수 있기에 깔끔하고 추상적인 표현이 가능합니다.
// 제네릭을 사용하지 않은 예
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA: Int = a
a = b
b = temporaryA
}
func swapTwoStrings(_ a: inout String, _ b: inout String) {
let temporaryA: String = a
a = b
b = temporaryA
}
var numberOne: Int = 5
var numberTwo: Int = 10
swapTwoInts(&numberOne, &numberTwo)
var stringOne: String = "A"
var stringTwo: String = "B"
swapTwoStrings(&stringOne, &stringTwo)
Double
타입의 값 교환을 원한다면 또 다른 함수를 구현해야 합니다. 그리고 타입마다 다른 함수를 써줘야 하는 불편함도 있습니다.
22.1 제네릭 함수
제네릭 함수를 사용하면, 같은 타입인 두 변수의 값을 교환한다는 목적을 타입에 상관없이 할 수 있도록 단 하나의 함수로 구현할 수 있습니다.
// 제네릭을 사용한 예
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA: T = a
a = b
b = temporaryA
}
swapTwoValues(&numberOne, &numberTwo)
swapTwoValues(&stringOne, &stringTwo)
swapTwoValues(&numberOne, &stringOne) // ERROR! 같은 타입끼리만 교환 가능
제네릭 함수는 실제 타입 이름을 써주는 대신에 플레이스홀더(Placeholder, 위 예제 함수에서는 T)를 사용합니다.
플레이스홀더 타입 T의 실제 타입은 함수가 호출되는 그 순간 결정됩니다.
또한 플레이스홀더 타입 T는 함수의 매개변수의 타입으로 사용할 수 있으며,
함수의 반환 타입으로 사용할 수도 있으며,
함수 내부 변수의 타입 지정을 위해 사용할 수도 있습니다.
타입 매개변수의 이름은 타입 이름이기도 하므로 대문자 카멜케이스를 사용하여 표현합니다.
ex) T
, U
, V
, Key
, Value
, Element
22.2 제네릭 타입
제네릭 함수에 이어 제네릭 타입을 구현할 수도 있습니다. 제네릭 타입을 구현하면 사용자 정의 타입인 구조체, 클래스, 열거형 등이 어떤 타입과도 연관되어 동작할 수 있습니다.
모든 타입을 대상으로 동작할 수 있는 스택 기능을 구현해보겠습니다.
// 제네릭을 사용한 Stack 구조체 타입
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
// Element의 타입으로 Double을 사용
var doubleStack: Stack<Double> = Stack<Double>()
doubleStack.push(1.0)
print(doubleStack.items) // [1.0]
doubleStack.push(2.0)
print(doubleStack.items) // [1.0, 2.0]
doubleStack.pop()
print(doubleStack.items) // [1.0]
// Element의 타입으로 String을 사용
var stringStack: Stack<String> = Stack<String>()
stringStack.push("1")
print(stringStack.items) // ["1"]
stringStack.push("2")
print(stringStack.items) // ["1", "2"]
stringStack.pop()
print(stringStack.items) // ["1"]
만약 요소로 모든 타입을 수용할 수 있도록 구현하려고 한다면
Stack
의item
배열을Any
타입으로 정의하는 방법- 제네릭을 사용하여
Element
의 타입으로Any
를 사용하는 방법
// 1. Stack의 item 배열을 Any 타입으로 정의하는 방법
struct AnyStack {
var items = [Any]()
mutating func push(_ item: Any) {
items.append(item)
}
mutating func pop() -> Any {
return items.removeLast()
}
}
var anyStack: AnyStack = AnyStack()
anyStack.push(1.0)
print(anyStack.items) // [1.0]
anyStack.push("2")
print(anyStack.items) // [1.0, "2"]
anyStack.push(3)
print(anyStack.items) // [1.0, "2", 3]
anyStack.pop()
print(anyStack.items) // [1.0, "2"]
// 2. 제네릭을 사용하여 Element의 타입으로 Any를 사용하는 방법
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
var anyStack: Stack<Any> = Stack<Any>()
anyStack.push(1.0)
print(anyStack.items) // [1.0]
anyStack.push("2")
print(anyStack.items) // [1.0, "2"]
anyStack.push(3)
print(anyStack.items) // [1.0, "2", 3]
anyStack.pop()
print(anyStack.items) // [1.0, "2"]
두 가지 방법 중, 제네릭을 사용했을 때 훨씬 유연하고 광범위하게 사용할 수 있으며, Element
의 타입을 정해주면 그 타입에만 동작하도록 제한할 수 있어 더욱 안전하고 의도한 대로 기능을 사용하도록 유도할 수 있습니다.
22.3 제네릭 타입 확장
만약 익스텐션을 통해 제네릭을 사용하는 타입에 기능을 추가하고자 한다면, 익스텐션 정의 타입 매개변수를 명시하지 않아야 합니다.
대신 기존의 제네릭 정의에 명시한 타입 매개변수를 익스텐션에서 사용할 수 있습니다.
extension Stack<Element> { // error
extension Stack {
var topElement: Element? { // 기존의 제네릭 정의에 명시된 Element
return self.items.last
}
}
22.4 타입 제약
타입 제약(Type Constraints)은 타입 매개변수가 가져야 할 제약사항을 지정할 수 있는 방법입니다.
타입 매개변수 자리에 사용할 실제 타입이 특정 클래스를 상속받은 타입이어야 한다든지, 특정 프로토콜을 준수하는 타입이어야 한다는 등의 제약을 줄 수 있습니다.
타입 제약은 클래스 타입 또는 프로토콜로만 줄 수 있습니다. (열거형, 구조체 타입 X)
// T는 BinaryInteger 프로토콜을 준수하는 타입
func swapTwoValues<T: BinaryInteger>(_ a: inout T, _ b: inout T) {
// ...
}
여러 제약을 추가하고 싶다면 where
절을 활용합니다.
// T는 BinaryInteger 프로토콜과 FloatingPoint 프로토콜을 준수하는 타입
func swapTwoValues<T: BinaryInteger>(_ a: inout T, _ b: inout T) where T: FloatingPoint {
// ...
}
Tip 타입 제약에 자주 사용할 만한 프로토콜에는
Hashable
,Equatable
,Comparable
,Indexable
,IteratorProtocol
,Error
,Collection
,CustomStringConvertible
등이 있습니다.
22.5 프로토콜의 연관 타입
연관 타입(Associated Type)은 프로토콜에서 사용할 수 있는 플레이스홀더 이름입니다.
어떤 타입이 들어올지 모를 때, 연관 타입을 통해 '종류는 알 수 없지만, 어떤 타입이 여기에 쓰일 것이다'라고 표현해주는 역할을 프로토콜에서 수행할 수 있도록 만들어진 기능입니다.
protocol Container {
associatedtype ItemType // 연관 타입
mutating func append(_ item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}
struct DoubleStack: Container {
var items = [Double]()
mutating func push(_ item: Double) {
items.append(item)
}
mutating func pop() -> Double {
return items.removeLast()
}
// Container 프로토콜 준수를 위한 구현
mutating func append(_ item: Double) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Double {
return items[i]
}
}
만약 ItemType
을 어떤 타입으로 사용할지 조금 더 명확히 해주고 싶다면,
구현부에 typealias ItemType = Double
라고 타입 별칭을 지정해줄 수 있습니다.
struct DoubleStack: Container {
typealias ItemType = Double
var items = [ItemType]()
mutating func push(_ item: ItemType) {
items.append(item)
}
mutating func pop() -> ItemType {
return items.removeLast()
}
// Container 프로토콜 준수를 위한 구현
mutating func append(_ item: ItemType) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> ItemType {
return items[i]
}
}
책에서는
count
타입과 서브스크립트 매개변수i
의 타입 또한ItemType
으로 명시되어있는데, 아이템의 개수와 인덱스 값을 의미하므로Int
로 명시해주는 것이 더 좋을 것같아 예제를 조금 수정했습니다.
22.6 제네릭 서브스크립트
제네릭 메서드를 구현할 수 있었던 것처럼 서브스크립트도 제네릭을 활용할 수 있습니다.
extension Stack {
subscript<Indices: Sequence>(indices: Indices) -> [Element]
where Indices.Iterator.Element == Int {
var result = [ItemType]()
for index in indices {
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]
' > Swift' 카테고리의 다른 글
17. 서브스크립트 (0) | 2021.01.30 |
---|