Search

[Swift 공식 문서] 23. Generics

Generics

제네릭 코드를 사용하면, 함수와 타입을 매우 유연하고 재사용가능하게 만들 수 있습니다. 중복코드를 피하면서, 구현의도를 분명하게 작성할 수 있습니다.
제네릭은 Swift의 강력한 기능중 하나이며 Swift standard library 내장되어 있습니다. 사실 그동안 여러분은 제네릭을 사용해 왔습니다. 예를들어 Swift 의 Array 와 Dictionary 타입은 둘다 제네릭 collection 입니다. Array 와 Dictionary 모두 담고 있는 타입을 다양하게 커스터마이징 할 수 있죠.

The Problem That Generics Solve

아래는 제네릭하지 않은 함수의 예시입니다.
func swapTwoInts(_ a: inout Int, _ b: inout Int) { let temporaryA = a a = b b = temporaryA }
Swift
복사
위 함수는 in-out 파라미터를 사용해서 ab 의 값을 swap 하고 있습니다.
var someInt = 3 var anotherInt = 107 swapTwoInts(&someInt, &anotherInt) print("someInt is now \(someInt), and anotherInt is now \(anotherInt)") // Prints "someInt is now 107, and anotherInt is now 3"
Swift
복사
위 함수는 매우 유용하지만, 이 함수는 Int 값에대해서만 사용할 수 있죠.
만약 String, Double 값에 대해서 swap 하고 싶다면 비슷한 함수를 여러번 새로 정의해야할 겁니다. 아래처럼요
func swapTwoStrings(_ a: inout String, _ b: inout String) { let temporaryA = a a = b b = temporaryA } func swapTwoDoubles(_ a: inout Double, _ b: inout Double) { let temporaryA = a a = b b = temporaryA }
Swift
복사
위 3개의 함수는 타입을 제외한 logic이 완벽하게 동일한 것을 볼 수 있습니다.
바로 이러한 경우에 제네릭이 코드의 중복을 줄여줄 수 있습니다.

Generic Functions

제네릭 함수는 모든 타입에 대해서 동작합니다.
아래는 위 3개함수에 대한 제네릭한 버전입니다.
func swapTwoValues<T>(_ a: inout T, _ b: inout T) { let temporaryA = a a = b b = temporaryA }
Swift
복사
함수의 body 부분은 위 함수와 똑같지만, 정의부 즉, 첫줄은 약간 다른데요
func swapTwoInts(_ a: inout Int, _ b: inout Int) func swapTwoValues<T>(_ a: inout T, _ b: inout T)
Swift
복사
이렇게 보면 그 차이가 명확해보입니다.
제네릭함수의 또다른 특징은 <T> 이 부분인데요. 이 괄호 안의 T 는 placeholder 타입의 이름을 의미합니다.
제네릭버전의 함수는 placeholder 타입 이름인 T 를 사용합니다. placeholder 타입은 T 가 어떤 타입이어여야 하는지 명시하지 않습니다만, T로 적힌 타입 둘은 같은 타입이어야 된다고 명시하고 있습니다. T 는 실제 함수가 호출될 때마다 타입이 결정됩니다.

Type Parameters

T 는 type 파라미터의 한 예시입니다. 타입 파라미터들은 placeholder 타입의 이름을 의미하는데요, 함수 뒤에 바로 작성해주면 됩니다. <T> 이렇게요
타입 파라미터를 정의하면, 해당 placeholder 타입인 T 를 타입의 이름처럼 사용할 수 있습니다. 다른 파라미터의 타입으로서 사용도 가능하며, 리턴 타입으로도 사용이 가능합니다.
<T1, T2> 이런식으로 여러개의 타입 파라미터를 정의할 수도 있습니다.

Naming Type Parameters

대부분의 경우에 타입 파라미터는 Key Value 처럼 설명가능한 이름을 갖습니다. Dictionary<Key, Value>Array<Element> 도 마찬가지죠. Key, Value, Element 모두 타입 파라미터의 이름을 의미하며, 어떠한 타입이든 사용이 가능합니다.
타입 파라미터들 간에 의미있는 연관성이 없다면 관례대로 T,U,V 와 같은 하나의 단어를 사용하는 것이 좋습니다.

Generic Types

generic 함수에 추가로 Swift에서는 제네릭 타입을 정의할 수 있습니다. 커스텀 클래스, 구조체, 열거형 모두에 모든 타입을 적용시킬 수 있게 만들 수 있습니다.
이 부분에서는 Stack 이라고 불리는 제네릭 컬렉션 타입을 작성하는 방법을 보여드리도록 하겠습니다. stack 은 순서가 있는 값들의 집합이며, 어레이와 비슷하지만, 더 제한된 형태의 동작을 합니다. 어레이는 모든 부분에서의 삽입 및 삭제가 가능하지만, stack 의 경우에는 collection 의 끝에서만 추가 및 삭제가 가능합니다.
UINavigationController 클래스 도 또한 뷰 컨트롤러들을 stack 의 개념을 사용해서 저장합니다.
struct IntStack { var items: [Int] = [] mutating func push(_ item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() } }
Swift
복사
위 구조체를 Int 형이 아닌 다른 타입에 대해서도 사용하고 싶다면?
제네릭 타입을 정의해서 사용하면됩니다. 아래 처럼요
struct Stack<Element> { var items: [Element] = [] mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() } }
Swift
복사
구조체를 정의하는 부분에서 Element 타입을 구체화해서 사용하면 됩니다.
var stackOfStrings = Stack<String>() stackOfStrings.push("uno") stackOfStrings.push("dos") stackOfStrings.push("tres") stackOfStrings.push("cuatro") // the stack now contains 4 strings
Swift
복사
위에서는 String 타입으로 Element 를 구체화하여 사용하는 모습입니다.
let fromTheTop = stackOfStrings.pop() // fromTheTop is equal to "cuatro", and the stack now contains 3 strings
Swift
복사

Extending a Generic Type

제네릭 타입을 확장하고자 할 때는, 타입 파라미터를 extension 구문에 작성하지 않아도 됩니다. 아래처럼요. 대신, 타입 파라미터의 리스트는 원본의 타입 정의를 따라갑니다.
extension Stack { var topItem: Element? { return items.isEmpty ? nil : items[items.count - 1] } }
Swift
복사
타입 파라미터의 리스트를 작성해주지 않았지만, 원본의 타입 파라미터인 Element 를 그대로 사용할 수 있습니다.
if let topItem = stackOfStrings.topItem { print("The top item on the stack is \(topItem).") } // Prints "The top item on the stack is tres."
Swift
복사

Type Constraints

이렇게 모든 타입에 적용이 가능한 제네릭 타입을 만드는 것도 좋지만, 어떤 경우에서는 이러한 타입들에게 제약조건을 걸어주는 것이 유용할 때가 있습니다.
제약조건의 예시는 아래와 같습니다.
특정 클래스를 상속받아야 한다.
특정 프로토콜을 준수해야한다.
예를들어 Dictionary 타입의 Key 의 경우 Hashable 이라는 프로토콜을 준수하는 타입만을 사용할 수 있도록 제약조건이 걸려있습니다. key 들은 Hashable 해야, 특정 key 값에 대한 value 가 존재하는지 빠르게 접근할 수 있기에 이러한 제약조건을 걸어 놓은 것인데요. 이러한 제약조건이 없다면 Dictionary 는 특정 key 값에 대해 값을 대체하거나, 주어진 key 값에 대한 값이 존재하는지 찾을 수 없을 것입니다.
이런식으로 커스텀 제네릭 타입에 제약조건을 걸 수 있습니다. 유연하고 재사용가능하면서, 맥락을 구체화해서 더 디테일한 기능을 제공할 수 있게되는거죠

Type Constraint Syntax

타입 파라미터의 이름 옆에 : 과 함께 상속받아야하는 클래스의 이름 또는 준수해야하는 프로토콜을 작성해주면 됩니다.
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) { // function body goes here }
Swift
복사
위 예시에서 TSomeClass 를 상속받은 클래스로 제약되었고, U 의 경우에는 SomeProtocol 을 준수해야만 합니다.

Type Constraints in Action

아래는 값의 인덱스를 찾아 리턴하는 제네릭 함수입니다.
( 아래 함수는 컴파일되지 않습니다 )
func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { return index } } return nil }
Swift
복사
위 함수가 컴파일 되지 않는 이유는 바로 if value == valueToFind { 이 부분 때문인데요, 타입 파라미터인 valuevalueToFind 는 동등연산자(==) 를 통해 비교가 가능할지 알 수 없습니다. Swift 에서는 Equatable 한 타입에 대해서만 == 의 적용을 지원받습니다.
따라서 위 함수를 컴파일 가능하게 만드려면 Equatable 을 타입파라미터의 제약조건으로 걸어주어야 하는 것이죠. 아래 처럼요
func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { return index } } return nil }
Swift
복사
위 예시에서 타입 파라미터 T 는 다음을 의미합니다
Equatable 을 준수하는 모든 타입”
이제 위 함수는 성공적으로 컴파일 되며, 사용이 가능해졌습니다.
let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25]) // doubleIndex is an optional Int with no value, because 9.3 isn't in the array let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"]) // stringIndex is an optional Int containing a value of 2
Swift
복사

Associated Types

프로토콜을 정의할 때,프로톨의 정의부로서 하나이상의 associated types 를 선언하는 것이 유용할 때가 있습니다. associated 타입은 프로토콜의 일부분에 대한 타입 placeholder 역할을 수행합니다. 단순히 말해서 프로토콜의 실질적인 타입을 구체적으로 명시해놓지 않는 것입니다. 프로토콜이 채택될 때 이 타입을 구체화하게 됩니다. Associated 타입은 associatedtype 키워드를 사용해서 정의하면 됩니다.

Associated Types in Action

아래는 Container 라는 프로토콜에서 Item 이라는 associated type 을 선언한 예시입니다.
protocol Container { associatedtype Item mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } }
Swift
복사
Container 프로토콜에는 아래의 3가지 요구사항을 정의해놓았습니다.
1.
append 함수를 사용해서 새 아이템을 container 에 추가할 수 있어야 함.
2.
container 안에 있는 아이템들의 갯수를 count 프로퍼티를 통해 접근할 수 있어야함.
3.
containerInt 타입의 인덱스 값으로 불러올 수 있어야함.
위 프로토콜은 ‘어떤 타입의 값이 저장될 수 있는지' 와 ‘어떤 방식으로 아이템을 저장해야하는지’ 를 구체화하지 않았습니다. 위 프로토콜은 모든 타입에 적용이 가능한 3개의 기능을 명시해놓았을 뿐입니다.
Container 프로토콜은 준수하는 모든 타입은 저장하게 되는 값의 타입을 구체화해야합니다. 특히, 올바른 타입의 아이템만 container 에 저장될 수 있도록 보장해야하며, 첨자접근을 통해 리턴되는 타입을 분명하게해야합니다.
이러한 요구사항을 정의하기위해, Container 프로토콜은 어떤 타입을 저장하게 될지 모르는 채 컨테이너가 저장하게될 요소의 타입을 참조해야만합니다. Container 프로토콜은 append 메서드를 사용해서 같은 타입의 아이템을 추가할 수 있어야하며, 컨테이너의 첨자를 통해 리턴된 값은 element 타입과 동일해야합니다.
이러한 사항들을 달성하기위해, Container 프로토콜은 Item 이라는 associated 타입을 선언해 놓았습니다. 프로토콜은 Item 의 타입을 정의하지 않습니다. 타입을 정의해놓지 않았지만 Item 을 통해 해당 타입을 참조할 수 있게 됩니다.
struct IntStack: Container { // original IntStack implementation var items: [Int] = [] mutating func push(_ item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() } // conformance to the Container protocol // 아래를 구체화 하게되면 그에 따라오는 Item 의 타입이 Int 로 구체화됨. typealias Item = Int mutating func append(_ item: Int) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> Int { return items[i] } }
Swift
복사
제네릭 Stack 타입 또한 Container 프로토콜을 준수하도록 할 수 있다.
struct Stack<Element>: Container { // original Stack<Element> implementation var items: [Element] = [] mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() } // conformance to the Container protocol mutating func append(_ item: Element) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> Element { return items[i] } }
Swift
복사
이 경우에는 Element 라는 제네릭 타입이 전달되었기 때문에, Swift 가 Element 라는 제네릭 타입이 Item 을 대신해서 사용되고 있음을 추론할 수 있다. 따라서 이 경우 associatedType 을 따로 정의해주지 않아도 컴파일에러가 발생하지 않는다.

Extending an Existing Type to Specify an Associated Type

extension 을 이용해 이미 존재하는 타입에 추가적으로 프로토콜을 준수시키려할 때, associated 타입을 가진 프로토콜도 준수가 가능합니다.
Swift 의 어레이는 이미 Container 프로토콜의 요구사항을 이미 충족하고 있습니다. 따라서 빈 extension 을 사용해 프로토콜을 준수함을 선언할 수 있습니다.
extension Array: Container {}
Swift
복사
이때 Swift 는 어레이의 이미 존재하는 append, subscript 를 통해 Item 에 사용할 적절한 타입을 추론하게 됩니다.

Adding Constraints to an Associated Type

associated type 에도 제약조건을 부여할 수 있습니다. 프로토콜의 구현 목적에 따라 적절한 제약조건을 부여함으로써 디테일한 기능을 구현할 수 있습니다.
protocol Container { associatedtype Item: Equatable mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } }
Swift
복사
제약조건은 위 코드처럼 콜론과 함께 준수사항을 작성해주기만 하면됩니다.

Using a Protocol in Its Associated Type’s Constraints

프로토콜은 자기자신의 요구사항의 일부분으로서 등장할 수 있습니다. 예를들어, Container 프로토콜에 suffix(_:) 메서드를 추가한 프로토콜을 정의해보겠습니다. suffix 메서드는 컨테이너의 끝부분부터 주어진 숫자만큼 요소들을 리턴하고 이를 Suffix 타입의 인스턴스에 저장합니다.
protocol SuffixableContainer: Container { associatedtype Suffix: SuffixableContainer where Suffix.Item == Item func suffix(_ size: Int) -> Suffix }
Swift
복사
이 프로토콜에서, Suffix 는 associataed type입니다. Suffix 는 두가지 제약조건을 갖는데,
1.
SuffixableContainer 프로토콜을 준수해야함.
2.
Item 타입은 컨테이너의 Item 타입과 동일해야 함.
제네릭 타입으로부터의 Stack 타입을 확장해보겠습니다.
extension Stack: SuffixableContainer { func suffix(_ size: Int) -> Stack { //자기자신이 SuffixableContainer 을 준수함. var result = Stack() for index in (count-size)..<count { result.append(self[index]) } return result } // Inferred that Suffix is Stack. } var stackOfInts = Stack<Int>() stackOfInts.append(10) stackOfInts.append(20) stackOfInts.append(30) let suffix = stackOfInts.suffix(2) // suffix contains 20 and 30
Swift
복사
위 예시에서 Suffix라는 Stack 을 위한 associated type 은 또한 Stack 입니다. 그래서 Stack의 suffix 동작이 Stack 을 리턴하는 거죠. SuffixableContainer 를 준수하는 타입은 자기 자신과는 다른 Suffix 타입을 보유할 수 있습니다. 예를 들어 제네릭 타입이 아닌 IntStack 타입에 SuffixableContainer 를 준수시켜보곘습니다.
extension IntStack: SuffixableContainer { func suffix(_ size: Int) -> Stack<Int> { //IntStack 을 리턴하지 않음. var result = Stack<Int>() for index in (count-size)..<count { result.append(self[index]) } return result } // Inferred that Suffix is Stack<Int>. }
Swift
복사
Stack<Int>IntStack 은 아니지만, Stack<Int>
1.
SuffixableContainer 를 준수하고
2.
IntStackItem 타입과 Stack<Int> 타입이 동일하므로,
자기 자신과는 다른 타입이지만 Suffix 타입으로 지정해 사용할 수 있게 되는 겁니다.

Generic Where Clause

타입 제약조건을 사용하면 제네릭 함수의 파라미터 타입에 요구사항을 명시할 수 있습니다.
associated type 에 대한 요구조건을 정의하고자 하는데도 유용합니다. where 조건을 사용하면, associated 타입이 특정 프로토콜을 준수하도록 할수 있고, 또는 특정 파라미터 또는 associated type 이 같아야 한다고 요구할 수도 있습니다. 제네릭 where 조건은 where 키워드로 시작합니다. 제네릭 조건의 동등연사자는 값이 아닌 타입을 비교하게 됩니다.
func allItemsMatch<C1: Container, C2: Container> (_ someContainer: C1, _ anotherContainer: C2) -> Bool where C1.Item == C2.Item, C1.Item: Equatable { // Check that both containers contain the same number of items. if someContainer.count != anotherContainer.count { return false } // Check each pair of items to see if they're equivalent. for i in 0..<someContainer.count { if someContainer[i] != anotherContainer[i] { return false } } // All items match, so return true. return true }
Swift
복사
위 함수에서 구체화한 바는
1.
C1Item 타입과 C2Item 타입이 동일해야한다.
2.
C1 의 아이템 타입은 Equatable 을 준수해야한다. ( 두 타입이 동일하기 때문에, 하나만 준수하는지 확인하면 되니까요 !)
입니다.
따라서 위 요구사항을 만족시키지 못한 맥락에서 함수를 호출시, 컴파일 에러가 발생합니다.
var stackOfStrings = Stack<String>() stackOfStrings.push("uno") stackOfStrings.push("dos") stackOfStrings.push("tres") var arrayOfStrings = ["uno", "dos", "tres"] if allItemsMatch(stackOfStrings, arrayOfStrings) { print("All items match.") } else { print("Not all items match.") } // Prints "All items match."
Swift
복사
위 예시에서
stackOfStringsItem 타입은 String arrayOfStringsItem 타입은 String 으로 같습니다.

Extensions with a Generic Where Clause

extension 의 일부분에 제네릭의 where 조건을 붙일 수 있습니다. 아래 예제에서는 Stack 을 확장하지만, 이 확장사항은 Element 타입이 Equatable 을 만족하는 경우에 한해서 확장됩니다.
extension Stack where Element: Equatable { func isTop(_ item: Element) -> Bool { guard let topItem = items.last else { return false } return topItem == item } }
Swift
복사
이 새로운 isTop 메서드는 함수의 진입부에서 스택이 비어있는지 확인한 후, 스택의 최상단에 존재하는 항목이 item 과 같은지의 여부에 대한 Bool 값을 리턴합니다. ElementEquatable 하다는 조건이 붙지 않았다면 == 동등연산자의 사용이 불가능하기 때문에 컴파일 에러가 발생했을 것입니다. 하지만 StackElementEqautable 하다는 조건하에 isTop 이라는 메서드가 확장되었으므로 해당 내용은 컴파일에러가 발생하지 않습니다.
if stackOfStrings.isTop("tres") { print("Top element is tres.") } else { print("Top element is something else.") } // Prints "Top element is tres."
Swift
복사
만약 Element 가 Equatable 을 만족하지 않는 Stack 에 해당 메서드를 호출하려 한다면 컴파일 에러가 발생합니다.
struct NotEquatable { } var notEquatableStack = Stack<NotEquatable>() let notEquatableValue = NotEquatable() notEquatableStack.push(notEquatableValue) notEquatableStack.isTop(notEquatableValue) // Error
Swift
복사
확장기능을 사용하고자 하는 조건을 만족하지 못했기 때문입니다.
extension Container where Item: Equatable { func startsWith(_ item: Item) -> Bool { return count >= 1 && self[0] == item } } if [9, 9, 9].startsWith(42) { print("Starts with 42.") } else { print("Starts with something else.") } // Prints "Starts with something else."
Swift
복사