-
Swift Language ) GenericsSwift/Swift Language 2021. 9. 23. 13:46
Generics
Generic 은 유연하고 재사용 가능하도록 함수와 타입을 다룰 수 있게 해주고, 어떠한 type, subject 와도 함께 사용될 수 있어요. 이를 통해 중복되는 코딩을 하지 않을 수 있습니다.
대부분의 Swift standard library 는 generic 으로 이루어져있고, Generic 은 Swift 의 가장 강력한 기능 중 하나에요.
예를 들면, Swift 에서 array, dictionary 는 모두 generic collections 이에요. 따라서 Int, String 뿐만 아니라 Swift 내에서 생성될 수 있는 어떠한 타입이든 array 의 element 로서 존재할 수 있습니다. (dictionary 내 element 도 마찬가지에요)
1.1 The Problem That Generics Solve
Nongeneric function 인 swapTwoInts(_:_:) 가 있습니다. (두 Int 값을 서로 교체)
func swapTwoInts( _ a: inout Int, _ b: inout Int) { let temp = a a = b b = temp }
이 기능을 String, Double Type 에 대해서도 사용하고 싶다면 아래 두 함수 역시 만들어주어야해요.
func swapTwoStrings( _ a: inout String, _ b: inout String) { let temp = a a = b b = temp } func swapTwoDoubles( _ a: inout Double, _ b: inout Double) { let temp = a a = b b = temp }
이 세 functions 에서, 각 function 의 body 가 모두 같음을 알 수 있어요. (타입만 다른 것을 주목해주세요.)
Generic 은 위 코드들을 훨씬 유연하게, 하나의 function 으로 모든 Types 을 이용할 수 있도록 해줍니다.
Generic Functions
Generic functions 는 어떤 타입과도 함께 사용될 수 있어요. 아래 함수는 모든 type 에 사용될 수 있는 swap function 이에요.
func swapTwoValues<T>( _ a: inout T, _ b: inout T) { let temp = a a = b b = temp }
이제 두 함수들을 비교해보도록 할게요. (body 부분은 똑같아서 생략했어요)
func swapTwoValues<T>( _ a: inout T, _ b: inout T) func swapTwoInts ( _ a: inout Int, _ b: inout Int)
Generic version 에서 function 은 실제 type name (Int, String, or Double) 대신
placeholder type name 을 사용했어요. (T)
placeholder type name 은 T 가 어떤 type 이어야 하는지 직접 정해주지 않아요.
대신 해당 예제에서는 a 와 b 모두 같은 타입 T (어떤 타입이든지 같은 타입) 이어야 한다고 정해줍니다.
실제 T 자리에 사용될 타입은 swapTwoValues(_:_:) function 이 호출될 때마다 정해집니다.
더보기만약 swapTwoValues(_:_:) function 필요할 때면 직접 만들어서 사용할 필요 없이 Swift Standard Library 에서 사용하면 돼요~
( swap(_:_:) )
Type Parameters
swapTwoValues(_:_:) function 에서 placeholder type T 는 type parameter 의 예시에요. Type parameters 는 placeholder type 을 지정하고 이름을 지어줘요. Function name 바로 뒤 '< >' 사이에 적어주시면 됩니다 (<T>) . 해당 과정이 끝나면, parameters 또는 return 값의 type 으로 사용할 수 있어요. 해당 function 이 호출될 때마다 type parameter 는 actual type 으로 교체돼요. 여러개의 type parameter 을 만들어 주고 싶을 때는 Comma(,) 로 각각을 구분해주시면 됩니다. (<T, U, ..>)
Naming Type Parameters
많은 경우, Type Parameters 는 이해하기 쉽도록 이름이 지어져요. Dictionary<Key, Value> , 그리고 Array<Element> 와 같이요. 그러나, 의미있는 관계가 딱히 없을 때에는 T, U, V 와 같은 Single Letter 로 이름을 짓는게 관례입니다. (placeholder 라는 것을 알려주기 위해 UpperCamelCase 로 이름을 지어주세요!)
Generic Types
Generic Function 뿐만 아니라 Generic Type 도 정의할 수 있어요. Array, Dictionary 와 같은 Custom classes, structs, enums 을 만들어 줄 수 있습니다.
이번 예시에서는 Generic Collection Type 으로 Stack 을 만들어볼거에요.
먼저, Int 만을 이용하는 Stack 을 만들어볼게요.
struct IntStack { var items : [Int] = [] mutating func push(_ item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() } }
이제, Type parameter 이름으로 "Element" 를 갖는 Generic Collection 을 만들어보겠습니다.
struct Stack<Element> { var items: [Element] = [] mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() } }
다음으로, 위 Stack<Element> 를 이용해서 String Type 을 Element 로 갖는 Stack instance 를 생성해볼게요.
var stackOfStrings = Stack<String>() stackOfStrings.push("uno") stackOfStrings.push("dos") stackOfStrings.push("tres") stackOfStrings.push("cuatro") stackOfStrings.pop() // cuatro
Extending a Generic Type
Generic type 을 extend 할 때, Type parameters (T, U, ..) 는 extension 의 정의에서 제공해주지 않아도 됩니다. 대신, 기존의 type definition 에 있던 type parameters 를 extension 에서 사용할 수 있고, type parameter names 은 기존에 있던 type parameters 을 가리키는 데에 사용됩니다. (기존에 선언했던 parameter names 그대로 사용하시면 돼요)
아래 예시에서는 generic Stack type 를 read-only computed property 인 'topItem' 을 추가하기 위해 extend 합니다(topItem: stack 내 가장 위에 있는 item 을 제거하지 않고 반환합니다) .
extension Stack { var topItem: Element? { return items.isEmpty ? nil : items[items.count - 1] } }
topItem property 는 Element? 형의 value 를 반환합니다. Stack 이 비어있는 경우 topItem 은 nil 을, 그렇지 않은 경우 가장 마지막에 있는 item 을 반환합니다. 해당 extension 에서 type parameter list 를 정의하지 않는다는 것을 다시 한번 봐주세요. Stack type 에서 존재하는 type paramenter name 인 'Element' 가 extension 내에서 topItem 의 optional type 으로 사용되었습니다. topItem 은 어떠한 Stack 의 instance 에서도 사용될 수 있습니다.
if let topItem = stackOfStrings.topItem { print("topItem: \(topItem)") }
Generic type 은 extension 을 통해 requirements 또한 추가해줄 수 있습니다. (Extensions with a Generic Where Clause 에서 다룹니다. 하단에 있어요~ )
Type Constraints
swapTwoValues, Stack 과 같은 generic function, generic custom structs 등의 Type 에 종종 제한조건을 두어야 할 때가 있어요. 이 제한은 특정 parameters 가 어떤 class 를 inherit 하거나, protocol 또는 protocol composition 을 conform 해야하는 등의 조건이 될 수 있습니다.
예를 들어, Dictionary 에서 'Key' 는 Hashable protocol 을 conform 해야 합니다. 그렇지 않은 경우 Dictionary 에서 Value 를 Replace 해야하는지, Insert 해야하는지 구분할 수 없을테니까요. ( Swift 내의 모든 basic Types 는 Hashable 입니다. Int, Double, String, Bool 등)
Generic Contstraints 는 Generic type 을 정의할 때 정해줄 수 있어요.
Type Constraints Syntax
Type parameter list 내에서, type parameter's name 뒤에 하나의 class 또는 protocol 를 써줌으로 type 을 제한시킬 수 있어요.
func someFunction<T: SomeClass, U: SomeProtocol> (someT: T, someU: U) { //function body goes here }
위 someFuntion 함수에서 type parameters T 와 U 는 다음과 같습니다.
T: SomeClass 의 subClass
U: SomeProtocol 을 conform 하는 type
아래 함수는 String Array 내에서 target String 을 검색한 후 일치하는 값이 있으면 해당 index 를 return 하는 함수예요.
func findIndex(ofString valueToFind: String, in array: [String]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { return index } } return nil }
위 함수를 String 뿐만 아니라 다른 자료형에서도 쓰게 하기 위해 String 대신 Generic Type 으로 만들어서 아래 함수를 만들어보았어요.
하지만 아래 코드는 위 코드와는 달리 작성 시 Compile Error 를 내며 작동하지 않습니다.
func findIndex<T>(of valueToFind: T, in array: [T]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { return index } } return nil }
문제는 '==' 에 있습니다. 모든 type 이 equality check가 가능하지는 않기 때문이죠. 만약 우리가 어떤 복잡한 class 나 struct 를 만들게 되면 '==' 이 예상하는 대로 작동하지 않을 수 있어요. 바로 이런 문제 때문에, 모든 type 에 대해 '==' 를 사용하기가 어렵습니다. 하지만 방법이 없는 것은 아니에요. Swift standard library 에서는 'Equatable' 이라는 protocol 을 정의하고, 이를 conform 하는 type 들은 모두 '==' (Equality check) 사용이 가능합니다.
어떤 타입이든지 Equatable protocol 을 conform 하는 type 은 안전하게 findIndex<T> 에 사용될 수 있습니다. 이를 표현하기 위해, 위 함수를 다음과 같이 constraint 을 이용하여 재정의합니다.
func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { return index } } return nil }
이제 findIndex(of: in:) 은 성공적으로 complie 되고, Equatable protocol 을 conform 하는 type 이라면 모두 해당 함수에 사용될 수 있습니다.
AssociatedTypes
Protocol 을 정의할 때, associated types 를 정의하는게 도움이 될 때가 있어요.
Associated type 은 protocol 내에서 쓰이는 type 에게 placeholder name 을 주는 역할을 해요.
Associated 에 쓰일 실제 type 은 protocol이 adopt 되기 전까지는 정해지지 않습니다.
Associated 타입은 'associatedtype' keyword 로 정의해줄 수 있어요.
Associated Types in Action
아래는 Container protocol 에서 associatedtype 이 쓰이는 예시에요. 여기서는 Item 이라는 associated type 을 정의했어요.
protocol Container { associatedtype Item // 쓰일 어떤 Type 에게 'Item' 이라는 이름을 주고있음. mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } }
Container protocol 에서는 어떻게 데이터가 저장되어야 하는지, 그리고 Item 이 어떠한 type 이어야 하는지에 대해 지정해 주고 있지 않아요. 어떤 타입이든 Container protoocol 을 adopt 하는 type 은 저장하는 type 이 어떤 것인지에 대해 지정해주어야해요.
Requirements 을 정의하기 위해, Container protocol 은 container 가 이용하는 실제 type은 모른 채로 elements 의 type 에 대해 가리킬 수 있게 하는 방법이 필요해요. Container protocol 은 append(_:) method 에 전달 되는 값의 type과 container의 element type, subscript 에서 return 되는 값의 type 이 모두 같다고 지정해주어야 해요.
더보기The Container protocol needs to specify that any value passed to the append(_:) method must have the same type as the container’s element type, and that the value returned by the container’s subscript will be of the same type as the container’s element type.
이러한 목적을 이루기 위해 Container protocol 에서는 Item 이라는 associated type 을 정의합니다. Protocol 에서는 Item 이 무엇인지 정해주지 않아요 (해당 역할은 conform 하는 type 에게 떠넘깁니다). 이때, Item typealias가 Container 에서의 item type 을 가리키는 역할과, 여러 함수들 (append, subscript 등)에 쓰이는 Item type 에 대해 정의해주는 방법을 제공합니다.
아래 코드는 IntStack 이 Container protocol 을 adopt 하는 경우에 대해 보여주고 있어요.
struct IntStack: Container { // original Intstack implmentation // conformance to the Container protocol typealias Item = Int // still works if it is not provided thanks to swift's type inference var items: [Int] = [] mutating func push(_ item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() } mutating func append(_ item: Int) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> Int { return items[i] } }
struct IntContainer: Container { typealias Item = Int // typealias Item = Int var items: [Item] = [] mutating func push(_ item: Item) { items.append(item) } mutating func pop() -> Item { return items.removeLast() } mutating func append(_ item: Item) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> Item { return items[i] } }
Generic Type 을 다루는 Stack 에 대해서는 다음과 같이 이용할 수 있습니다.
struct Stack<Element>: Container { 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] } }
Extending an Existing Type to Specify an Associated Type
이미 존재하는 type 에 대해 protocol 을 conform 하도록 extend 할 수 있습니다. Associated type 을 가지고 있는 protocol 의 경우도 포함됩니다.
Swift 의 Array type 은 이미 append(_:) method, count property, Int index 를 사용하는 subscript 를 가지고있어요. 이 세가지 기능이 Container protocol 의 requirements 와 대응됩니다. 따라서 Array 가 Container protocol 을 adopt 한다고 선언하는 것 만으로도 해당 protocol 을 conform 하도록 extend 해줄 수 있습니다.
extension Array: Container {}
위 generic Stack type 예시에서처럼 Array 에서 이미 존재하는 append(_:) method 와 subscript 를 통해 Swift 는 Item 에 적용될 type 이 어떤 것인지 알 수 있습니다. 이 extension 을 정의 해줌으로써 Array 를 Container type 으로도 사용할 수 있습니다.
Adding Constraints to an Associated Type
Conforming type 이 일정 조건을 만족시키도록 하기 위해 Type 에 대한 제약조건을 protocol 내 associated type 에 추가할 수 있습니다. 아래는 Container protocol 내 item 이 'Equatable' 하도록 만들어주는 코드입니다.
protocol Container { associatedtype Item: Equatable mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } }
해당 Container 를 conform 하기 위해서는 container 내 item type 이 Equatable protocol 을 conform 해야 합니다.
Using a Protocol in Its Associated Type’s Constraints
Protocol 은 그 자신의 requirements 내에서도 사용될 수 있습니다. 아래는 Container protocol 의 requirements 에 suffix(_:) method 를 추가하도록 하는 코드입니다. suffix(_:) method 는 container 의 끝에서부터 주어진 수 만큼의 elements 를 반환하고, Suffix type 의 instance 에 저장합니다.
protocol SuffixableContainer: Container { associatedtype Suffix: SuffixableContainer where Suffix.Item == Item func suffix(_ size: Int) -> Suffix }
Container 에 있는 Item 와 같이, 이 protocol 에서 Suffix 는 associated type 입니다.
Suffix 는 두개의 constraints 를 가지고있습니다:
SuffixableContainer protocol 을 conform 해야하고,
Suffix 의 Item type 이 container 의 Item type 과 일치해야합니다.
Item 에 있는 제한조건은 generic where clause 입니다. (아래에 서술되어있습니다 ~ )
아래는 Stack type 에 SuffixableContainer protocol 에 대한 conformance 를 추가하는 extension 입니다.
extension Stack: SuffixableContainer { func suffix(_ size: Int) -> Stack { 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
위 예시에서 Stack 에 대한 'Suffix' associated type 또한 Stack 이며, 그렇기 때문에 Stack 에 대한 suffix operation 은 또다른 Stack 을 return 합니다. Alternatively, SuffixableContainer 를 conform 하는 type 은 그 자신과 다른 Suffix type 을 가질 수 있습니다. - suffix operation 이 다른 type 을 return 할 수 있다는 것을 의미합니다. 아래는 SuffixableContainer 에 대한 conformance 를 추가하는 IntStack type 의 extension 이고, 여기서는 IntStack 이 아닌 Stack<Int> 을 suffix type 으로 사용합니다.
extension IntStack: SuffixableContainer { func suffix(_ size: Int) -> Stack<Int> { var result = Stack<Int>() for index in (count-size)..<count { result.append(self[index]) } return result } // Inferred that Suffix is Stack<Int>. }
Generic Where Clauses
Type constaints 는, (Generic function, subscript, type 과 관련된 ) type parameters 에 대한 requirements 를 정의할 수 있도록 합니다.
Type constaints 는 associated types 에 requirements 를 정의하는 데에도 쓰일 수 있습니다. Generic where clause 를 정의함으로써 구현할 수 있습니다.
Generic where clause 는 associated type 이 어떤 protocol 을 반드시 conform 하도록 하거나, 특정 type parameters 와 associated types 가 반드시 같아야 한다는 등의 조건을 둘 수 있도록 해줍니다.
Generic where clause 는 'where' keyword 로 시작하고, 그 뒤엔 associated types 에 대한 제한조건들 또는 types 과 associated types 의 equality relationships 이 붙습니다. Generic where clause 는 type 또는 function's body 의 '{' 바로 전에 써줍니다.
아래 예시에서 allItemsMatch 라는 generic function 을 정의하고, 이 함수는 두 Container instances 가 같은 order 에 같은 items 를 포함하는지 확인합니다. 이 함수는 모든 items 가 같으면 true 를, 그렇지 않으면 false 를 반환합니다.
함수에 쓰일 두 containers 는 같은 type 의 container 일 필요는 없으나, 같은 type 의 items 를 가지고 있어야 합니다. 해당 제한조건이 type constaints 와 generic where clause 의 조합으로 표현되었습니다.
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 }
이 함수는 두 someContainer, anotherContainer 두 arguments 을 받습니다.
someContainer argument 는 type C1,
anotherContainer argument 는 type C2 입니다.
C1 과 C2 는 두 container types 에 대한 type parameters 입니다. (함수가 호출될 때 정해집니다. )
아래 제한조건들이 function 의 두 type parameters 에 설정됩니다.
- C1 이 Container protocol 을 conform 할 것. (C1: Container)
- C2 가 Container protocol 을 conform 할 것. (C2: Container)
- C1 의 Item (Type) 과 C2 의 Item 이 같을 것. ( C1.Item == C2.Item)
- C1 의 Item 이 Equatable protocol 을 conform 할 것. (C1.Item: Equatable)
처음 두 제한조건들은 함수의 type parameter list 에 정의되어있고, 나머지 조건들은 generic where clause 에 정의되어있습니다.
이 조건들이 의미하는 것은 아래와 같습니다.
- someContainer 가 C1 type 의 container 일 것.
- anotherContainer 가 C2 type 의 container 일 것.
- someContainer 와 anotherContainer 가 같은 type 의 items 를 가질 것.
- someContainer 의 items 가 != operator 를 통해 확인 될 수 있을 것. (두 items 가 서로 다른지 확인하기 위해)
세번쨰와 네번째 requirements 는 anotherContainer 의 items 가 '!=' operator 를 이용해서 확인할 수 있는지 체크하기 위해 합쳐졌습니다. ( someContainer 의 items 와 anotherContainer 의 items 는 같은 타입이기 때문에)
The third and fourth requirements combine to mean that the items in anotherContainer can also be checked with the != operator, because they’re exactly the same type as the items in someContainer.
이 제한조건들은 allItemsMatch(_:_:) function 이 두 containers 가 서로 다른 container type 이라도 비교할 수 있게 해줍니다.
아래는 allItemsMatch(_:_:) 의 사용 예시입니다.
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") }
위 예시에서는 stack 과 array 는 다른 type 이지만, 둘 모두 Container protocol 을 conform 하는 동시에 같은 type 의 값들을 갖습니다. 그러므로 allItemsMatch(_:_:) function 의 arguments 로 두 containers 를 넣을 수 있습니다.
Extensions with a Generic Where Clause
Generic where clause 를 extension 에서도 사용할 수 있습니다. 아래 예시에서는 generic Stack structure 에 isTop(_:) method 을 넣고 있습니다.
extension Stack where Element: Equatable { func isTop(_ item: Element) -> Bool { guard let topItem = items.last else { return false } return topItem == item } }
isTop(_:) method 는 처음에 stack 이 비어있지 않은지 먼저 확인하고, stack 의 가장 위에 있는 item 과 비교합니다. 만약 generic where clause 를 사용하지 않고 만들려고 할 경우, 다음과 같은 문제가 생깁니다 : isTop(_:) 의 구현과정에서 == operator 를 사용하는데, Stack 은 items 에 equatable 을 요구하지 않으므로 == operator 를 사용하면 결국 compile-time error 가 발생합니다.
generic where clause 가 extension 에 새로운 요구사항을 넣게 해주어서 Stack 의 items 가 equatable 일 때만 isTop(_:) method 를 만들 수 있습니다.
( 여기서부터 꼬임 )
if stackOfStrings.isTop("tres") { print("Top element is tres") } else { print("Top element is something else") } // Prints Top element is tres
만약 isTop(_:) method 를 elements 가 equatable 이 아닌 stack 에 호출하게되면, compile-time error 가 발생합니다.
struct NotEquatable {} var notEquatableStack = Stack<NotEquatable>() let notEquatableValue = NotEquatable() notEquatableStack.push(notEquatableValue) //notEquatableStack.isTop(notEquatableValue) struct EquatableStr { var title: String } extension EquatableStr : Equatable { } var equatableStack = Stack<EquatableStr>() let equatableValue = EquatableStr(title: "hi") equatableStack.push(equatableValue) equatableStack.isTop(equatableValue)
protocol 의 extensions 에도 generic where clause 를 사용할 수 있습니다. 아래 예시에서는 Container protocol 에 startsWith(_:) method 를 extends 합니다.
extension Container where Item: Equatable { func startsWith(_ item: Item) -> Bool { return count >= 1 && self[0] == item } }
startsWith(_:) method 에서는 먼저 container 에 하나 이상의 item 이 있는지 확인한 후, container 의 첫번째 item 이 주어진 item 과 같은지 확인합니다. startsWith(_:) method 는 container 의 items 가 equatable 이라는 조건 안에서 Container protocol 을 conform 하는 어떠한 type 에도 사용할 수 있습니다.
if [9,9,9].startsWith(42) { print("Starts with 42") } else { print("Starts with something else") } // Prints Starts with something else
위 예시에서 generic where clause 는 Item 이 어떤 protocol 을 conform 하기를 요구하고 있으나, 다른 경우 어떤 특정한 type 이 될 것을 요구할 수도 있습니다.
extension Container where Item == Double { func average() -> Double { var sum = 0.0 for index in 0 ..< count { sum += self[index] } return sum / Double(count) } } print([1260.0, 1200.0, 98.6, 37.0].average()) // Prints 648.9
이 예시에서는 average() method 를 Item type 이 Double 인 containers 에 추가해주었습니다.
다른 곳에서 generic where clause 를 쓰는 것과 마찬가지로, extension 내 generic where clause 에도 여러개의 조건들을 포함시킬 수 있습니다. 이때 각 조건들 사이는 comma 로 구분해주세요.
Contextual Where Clauses
만약 이미 generic types 를 사용하고 있는 곳에서 작업하고 있다면, Generic where clauses 를 그 자체의 generic type constarints 를 갖지 않는 곳에서 선언부분에 사용할 수 있습니다. 예를 들어, generic where clauses 를 generic type 의 subscript 에 사용하거나, generic type 에 대한 extension 내의 method 에 사용할 수 있습니다. 예시에서 Container structure 는 generic 이며, 아래 예시에서 where clauses 는 새로운 methods 가 container 내에서 사용되기 위해 어떤 type constraints 가 충족되어야 하는지 정해주고 있습니다.
extension Container { func average() -> Double where Item == Int { var sum = 0.0 for index in 0..<count { sum += Double(self[index]) } return sum / Double(count) } func endsWith(_ item: Item) -> Bool where Item: Equatable { return count >= 1 && self[count-1] == item } } let numbers = [1260, 1200, 98, 37] print(numbers.average()) // Prints "648.75" print(numbers.endsWith(37)) // Prints "true"
위 예시에서는 items 가 integers 일 때 average() method 를 Container 에 추가해주고 있습니다. 그리고, endsWith(_:) method 는 item 이 equatable 일 때 더해주고 있습니다. 두 functions 모두 초기 Container 의 선언부에 있던 generic Item type parameter 에 type constraints 를 더해주는 generic where clause 를 포함합니다.
만약 위 코드를 contextual where clauses 없이 사용하고 싶다면, 두개의 extensions 를 작성하면 됩니다 (각각 다른 generic where clause). 위 예시와 아래 예시는 동일하게 작동합니다.
extension Container where Item == Int { func average() -> Double { var sum = 0.0 for index in 0..<count { sum += Double(self[index]) } return sum / Double(count) } } extension Container where Item: Equatable { func endsWith(_ item: Item) -> Bool { return count >= 1 && self[count-1] == item } }
Contextual where clauses 를 사용하는 이번 예시에서, average() 와 endsWith(_:) 의 구현부는 모두 같은 extension 에 위치해있습니다. 왜냐하면 각 method's generic where clause 가 해당 method 가 사용되기 위한 필요 조건을 언급했기 때문입니다. 이 필요조건들을 extensions 의 generic where clauses 로 옮겨도 상황은 같으나, 각 requirement 마다 다른 extension 이 필요해집니다.
Associated Types with a Generic Where Clause
Generic where clause 를 associated type 에 포함시킬 수 있습니다. 예를들어, 이 Container 가 iterator 를 갖기를 원한다고 가정해봅시다(standard library 의 Sequence protocol 이 사용하는 것처럼). 이때는, 아래 코드와 같이 작성하면 됩니다.
protocol Container { associatedtype Item mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } associatedtype Iterator: IteratorProtocol where Iterator.Element == Item func makeIterator() -> Iterator }
Iterator 의 Generic where clause 는 iterator 가 반드시 동일한 모든 item type (iterator's type 과는 무관하게) 'traverse' ... .
The generic where clause on Iterator requires that the iterator must traverse over elements of the same item type as the container’s items, regardless of the iterator’s type.
makeIterator() function 은 container 의 iterator 로의 접근을 제공합니다.
다른 protocol 을 상속받는 protocol 에서는, generic where clause 를 protocol 선언부에 넣는 방식으로 상속받은 associated type 에 constraint 를 더해줄 수 있습니다. 예를 들어, 아래 코드에서는 ComparableContainer protocol 의 Item 이 Comparable 을 conform 하도록 요구하는 코드입니다.
protocol ComparableContainer: Container where Item: Comparable { }
Generic Subscripts
Subscripts 는 generic 이 될 수 있고, genric where clauses 를 포함할 수 있습니다. '<>' 안에, 그리고 subscript 뒤에 placeholder type name 을 넣고 generic where clause 를 subscript's body 의 '{' 바로 전에 쓰면 됩니다. 예를 들면 :
extension Container { subscript<Indices: Sequence>(indices: Indices) -> [Item] where Indices.Iterator.Element == Int { var result: [Item] = [] for index in indices { result.append(self[index]) } return result } }
이 Container protocol 의 extension 은 ('A sequence of indices' 를 받고, 각 index 에 items 를 가지고 있는 array 를 반환하는 ) subscript 를 더해줍니다. Generic subscript 는 다음과 같이 제한됩니다:
1. '<>' 내의 geneic parameter Indices 는 standard library 의 Sequence protocol 을 따르는 type 이어야 합니다.
2. Subscript 는 하나의 parameter (indicies, indices type 의 instance ) 만을 받습니다.
3. Generic where clause 는 iterator (for the sequence) 가 반드시 type 이 Int 인 모든 elements 을 traverse 할 것을 요구한다. 이에 따르면, sequence 의 indices 가 container 에 사용되는 indices 와 같은 type 임을 확신할 수 있다.
이것들을 고려했을 때, 위 conststraints 는 "indices parameter 로서 전달되는 값은 'A sequence of Int' 이다." 를 의미함을 알 수 있다.
sourceCode:
func swapTwoInts(_ a: inout Int, _ b: inout Int) { let temporaryA = a a = b b = temporaryA } func some() { 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" } 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 } func swapTwoValues<T>(_ a: inout T, _ b: inout T) { let temporaryA = a a = b b = temporaryA } func some2() { var someInt = 3 var anotherInt = 107 swapTwoValues(&someInt, &anotherInt) // someInt is now 107, and anotherInt is now 3 var someString = "hello" var anotherString = "world" swapTwoValues(&someString, &anotherString) // someString is now "world", and anotherString is now "hello" } struct IntStack { var items: [Int] = [] mutating func push(_ item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() } } struct Stack<Element> { var items: [Element] = [] mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() } } func some3() { var stackOfStrings = Stack<String>() stackOfStrings.push("uno") stackOfStrings.push("dos") stackOfStrings.push("tres") stackOfStrings.push("cuatro") // the stack now contains 4 strings let fromTheTop = stackOfStrings.pop() // fromTheTop is equal to "cuatro", and the stack now contains 3 strings if let topItem = stackOfStrings.topItem { print("The top item on the stack is \(topItem).") } // Prints "The top item on the stack is tres." } extension Stack { var topItem: Element? { return items.isEmpty ? nil : items[items.count - 1] } } //func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) { // // function body goes here //} func findIndex(ofString valueToFind: String, in array: [String]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { return index } } return nil } func some4() { let strings = ["cat", "dog", "llama", "parakeet", "terrapin"] if let foundIndex = findIndex(ofString: "llama", in: strings) { print("The index of llama is \(foundIndex)") } // Prints "The index of llama is 2" } func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { return index } } return nil } func some5() { 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 } protocol Container { associatedtype Item mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } } struct IntStack2: 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 typealias Item = Int mutating func append(_ item: Int) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> Int { return items[i] } } struct Stack2<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] } } extension Array: Container {} protocol EquatableContainer { associatedtype Item: Equatable mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } } protocol SuffixableContainer: Container { associatedtype Suffix: SuffixableContainer where Suffix.Item == Item func suffix(_ size: Int) -> Suffix } extension Stack2: SuffixableContainer { func suffix(_ size: Int) -> Stack2 { var result = Stack2() for index in (count-size)..<count { result.append(self[index]) } return result } // Inferred that Suffix is Stack. } func some6() { var stackOfInts = Stack2<Int>() stackOfInts.append(10) stackOfInts.append(20) stackOfInts.append(30) let suffix = stackOfInts.suffix(2) // suffix contains 20 and 30 } extension IntStack2: SuffixableContainer { func suffix(_ size: Int) -> Stack2<Int> { var result = Stack2<Int>() for index in (count-size) ..< count { result.append(self[index]) } return result } } func allItemsMatch<C1: Container, C2: Container>(_ someContainer: C1, _ anotherContainer: C2) -> Bool where C1.Item == C2.Item, C1.Item: Equatable { if someContainer.count != anotherContainer.count { return false } for i in 0 ..< someContainer.count { if someContainer[i] != anotherContainer[i] { return false } } // All items match, so return true return true } func some8() { var stackOfStrings = Stack2<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") } // if stackOfStrings.istop } //extension Stack2 where Element: extension Stack where Element: Equatable { func isTop(_ item: Element) -> Bool { guard let topItem = items.last else { return false } return topItem == item } } func some9() { var stackOfStrings = Stack<String>() stackOfStrings.push("uno") stackOfStrings.push("dos") stackOfStrings.push("tres") var arrayOfStrings = ["uno", "dos", "tres"] if stackOfStrings.isTop("tres") { print("Top element is tres") } else { print("Top element is something else ") } } struct NotEquatable {} func some10() { var notEquatableStack = Stack<NotEquatable>() let notEquatableValue = NotEquatable() notEquatableStack.push(notEquatableValue) //notEquatableStack.isTop(notEquatableValue) } struct EquatableStr { var title: String } extension EquatableStr : Equatable { } func some11() { var equatableStack = Stack<EquatableStr>() let equatableValue = EquatableStr(title: "hi") equatableStack.push(equatableValue) equatableStack.isTop(equatableValue) } extension Container where Item: Equatable { func startsWith(_ item: Item) -> Bool { return count >= 1 && self[0] == item } } func some12() { if [9,9,9].startsWith(42) { print("Starts with 42") } else { print("Starts with something else") } // Prints Starts with something else } extension Container where Item == Double { func average() -> Double { var sum = 0.0 for index in 0 ..< count { sum += self[index] } return sum / Double(count) } } func some13() { print([1260.0, 1200.0, 98.6, 37.0].average()) // Prints 648.9 }
출처: https://docs.swift.org/swift-book/LanguageGuide/Generics.html
'Swift > Swift Language' 카테고리의 다른 글
Swift Language ) Closure (0) 2021.10.06 Swift Language) ARC ( Automatic Reference Counting) (0) 2021.10.04 Swift Language ) Access Control (0) 2021.09.16 Swift Language ) Protocol (0) 2021.09.16 Swift Language ) Extensions (0) 2021.09.14