제네릭(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) |
| |
제네릭 함수는 실제 타입 이름을 써주는 대신에 플레이스홀더(Placeholder, 위 예제 함수에서는 T)를 사용합니다.
플레이스홀더 타입 T의 실제 타입은 함수가 호출되는 그 순간 결정됩니다.
또한 플레이스홀더 타입 T는 함수의 매개변수의 타입으로 사용할 수 있으며,
함수의 반환 타입으로 사용할 수도 있으며,
함수 내부 변수의 타입 지정을 위해 사용할 수도 있습니다.
타입 매개변수의 이름은 타입 이름이기도 하므로 대문자 카멜케이스를 사용하여 표현합니다.
ex) T
, U
, V
, Key
, Value
, Element
22.2 제네릭 타입
제네릭 함수에 이어 제네릭 타입을 구현할 수도 있습니다. 제네릭 타입을 구현하면 사용자 정의 타입인 구조체, 클래스, 열거형 등이 어떤 타입과도 연관되어 동작할 수 있습니다.
모든 타입을 대상으로 동작할 수 있는 스택 기능을 구현해보겠습니다.
| |
| struct Stack<Element> { |
| var items = [Element]() |
| mutating func push(_ item: Element) { |
| items.append(item) |
| } |
| mutating func pop() -> Element { |
| return items.removeLast() |
| } |
| } |
| |
| |
| var doubleStack: Stack<Double> = Stack<Double>() |
| |
| doubleStack.push(1.0) |
| print(doubleStack.items) |
| doubleStack.push(2.0) |
| print(doubleStack.items) |
| doubleStack.pop() |
| print(doubleStack.items) |
| |
| |
| var stringStack: Stack<String> = Stack<String>() |
| |
| stringStack.push("1") |
| print(stringStack.items) |
| stringStack.push("2") |
| print(stringStack.items) |
| stringStack.pop() |
| print(stringStack.items) |
만약 요소로 모든 타입을 수용할 수 있도록 구현하려고 한다면
Stack
의 item
배열을 Any
타입으로 정의하는 방법
- 제네릭을 사용하여
Element
의 타입으로 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) |
| anyStack.push("2") |
| print(anyStack.items) |
| anyStack.push(3) |
| print(anyStack.items) |
| anyStack.pop() |
| print(anyStack.items) |
| |
| 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) |
| anyStack.push("2") |
| print(anyStack.items) |
| anyStack.push(3) |
| print(anyStack.items) |
| anyStack.pop() |
| print(anyStack.items) |
두 가지 방법 중, 제네릭을 사용했을 때 훨씬 유연하고 광범위하게 사용할 수 있으며, Element
의 타입을 정해주면 그 타입에만 동작하도록 제한할 수 있어 더욱 안전하고 의도한 대로 기능을 사용하도록 유도할 수 있습니다.
22.3 제네릭 타입 확장
만약 익스텐션을 통해 제네릭을 사용하는 타입에 기능을 추가하고자 한다면, 익스텐션 정의 타입 매개변수를 명시하지 않아야 합니다.
대신 기존의 제네릭 정의에 명시한 타입 매개변수를 익스텐션에서 사용할 수 있습니다.
| extension Stack<Element> { |
| |
| extension Stack { |
| var topElement: Element? { |
| return self.items.last |
| } |
| } |
22.4 타입 제약
타입 제약(Type Constraints)은 타입 매개변수가 가져야 할 제약사항을 지정할 수 있는 방법입니다.
타입 매개변수 자리에 사용할 실제 타입이 특정 클래스를 상속받은 타입이어야 한다든지, 특정 프로토콜을 준수하는 타입이어야 한다는 등의 제약을 줄 수 있습니다.
타입 제약은 클래스 타입 또는 프로토콜로만 줄 수 있습니다. (열거형, 구조체 타입 X)
| |
| func swapTwoValues<T: BinaryInteger>(_ a: inout T, _ b: inout T) { |
| |
| } |
여러 제약을 추가하고 싶다면 where
절을 활용합니다.
| |
| 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() |
| } |
| |
| |
| 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() |
| } |
| |
| |
| 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]) |
야곰, 스위프트 프로그래밍