Swift Language Guide: Generics
출처 : https://docs.swift.org/swift-book/LanguageGuide/Generics.html
Remind
- 제네릭 타입은 static stored 프로퍼티를 정의할 수 없다
🐶 서론
제네릭은 어떤 타입과도 작업할 수 있는 유연하고 재사용가능한 함수와 타입을 쓸 수 있게 해줍니다 ('어떤 타입'의 범위를 우리가 정의할 수 있습니다)
제네릭 코드를 통해, 반복코드를 피하고 명확하고 추상화된 방식으로 의도를 표현할 수 있습니다
제네릭은 Swift의 가장 강력한 기능 중 하나로, 많은 Swift 기본 라이브러리가 제네릭으로 만들어져 있습니다
예로, Array
와 Dictionary
타입은 모두 제네릭 Collection입니다
Array가 Int/String 등 어떤 타입이든 보관할 수 있도록 만들 수 있습니다
유사하게 Dictionary가 key와 value를 어떤 타입이로든 만들 수 있습니다
🐱 Generic으로 해결가능한 문제
먼저 두 개의 Int를 swap하는 함수를 non-Generic으로 구현한 예시입니다
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
이 함수는 유용하지만 Int 타입만 사용가능하다는 문제가 있습니다
만약 Double 타입 swap이 필요하다면 별도의 함수를 만들어주어야 합니다
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
let temporaryA = a
a = b
b = temporaryA
}
우리는 두 함수의 body가 동일함을 알아챌 수 있습니다
유일한 차이점은 parameter와 retun 타입만 다릅니다
따라서, Any 타입이든 swap해주는 하나의 함수를 정의하는 것이 더 유용하고 훨씬 유연합니다
Generic은 그런 함수를 정의할 수 있게 해줍니다
🐭 Generic 함수
Generic 함수는 어떤 타입과도 작업할 수 있습니다
예시를 봅시다
뭔가 첫 줄의 생김새가 다릅니다
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
기존의 non-Generic 함수와 비교해봅시다
func swapTwoInts(_ a: inout Int, _ b: inout Int)
func swapTwoValues<T>(_ a: inout T, _ b: inout T)
Generic 함수는 Double/Int같은 실제 타입대신 placeholder
타입을 사용합니다
placeholder는 위 예제처럼 T
가 아니어도 되고, T가 어떤 타입인지에 대해선 아무말도 하지 않습니다
다만, 두 parameter가 동일한 타입일 것을 말하고 있습니다
T 대신 사용될 실제 타입은 Generic함수가 호출될 때 결정됩니다
swapTwoValues
는 parameter로 Any 타입이든 올 수 있다는 점과, 두 parameter의 타입이 같아야 한다는 점을 제외하면 swapTwoInts
와 같은 방식으로 호출됩니다
swapTowValues
가 호출될 때마다 T로 사용될 타입은 추론과정을 거칩니다
(argument로 전달되는 값의 타입을 기반으로)
🐭 Type Parameter
위 예제에서 사용하던 placeholder, T
에 대해 알아봅시다
'placeholder type T'는 type parameter의 한 예시입니다
type paramter는 placeholder type을 명시하고 이름을 부여합니다
그리고 함수 이름 바로 뒤에 '<>'과 함께 붙습니다
한번 type paramter를 명시하면, Generic 함수에서 다뤄질 paramter/return/기타(body에서다룰) 타입을 정의하기 위해 이를 사용할 수 있습니다
함수가 호출될 때, 각 type parameter는 실제 타입으로 대체됩니다
type paramter는 여러 개 제공될 수 있습니다
--
🐹 Type Parameter Naming
대부분의 경우, type parameter의 이름은 Generic과 type paramter 간 관계에 대한 '설명'이 포함되도록 지어집니다
(ex. Dictionary의 <Key
, Value
> 혹은 Array의 <Element
>)
하지만, 둘 간에 유의미한 관계가 없다면 전통적으로 U
, T
, V
를 사용합니다
type paramter의 이름은 upper camel case를 사용합니다
--
🐰 Generic 타입
Generic 함수에 더해 Generic 타입도 정의할 수 있습니다
예시로는 Array와 Dictionary가 있으며
어떤 타입과도 작업할 수 있는 custom class/struct/enum입니다
이번 section에서는 Stack을 예시로 Generic Collection
을 정의하는 방법을 다룹니다
...Stack과 Array의 차이..중략...
non-Generic 버전
Int외에 다른 타입을 element로 사용하려면 별도 정의가 필요하다
struct IntStack {
var items: [Int] = []
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
Generic 버전
struct Stack<Element> {
var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
✔️ Generic 타입은 인스턴스 생성구문이 다르다
Generic타입의 인스턴스를 생성할 때는, type paramter를 대신할 '실제 타입'을 명시해주어야 합니다
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// the stack now contains 4 strings
🦊 Generic 타입 Extension
Generic 타입을 extension할 때는
original 정의부에 명시된 type paramter를 그대로 가져오므로
type paramter를 별도로 적어주지 않습니다
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
✔️ 조건부 extension
extension 할 때도 다뤘던 내용인데
extension을 선택적으로 추가할 수 있습니다
아래는 Element가 Equatable 프로토콜을 준수하는 Stack인 경우에만 extension합니다
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
🐻 Type 제약걸기
위에서 다뤘던 예제의 swapTwoValues(_:_:)
함수와 Stack
은 Any 타입과도 작업할 수 있습니다
하지만, 특정 조건을 만족하는 타입들만 사용하고 싶을 수도 있습니다
'Type 제약'은 type paramter가 특정 class를 상속받거나, 특정 프로토콜을 준수해야 함을 명시할 수 있습니다
예로, Swift의 Dictionary
는 Key가 hashable해야 함을 명시하고 있습니다
즉, Key 간에 유일하게 구분할 방법을 제공해야한다는 뜻입니다
Dictionary는 어떤 key에 대해 이미 value를 가지고 있는 key인지 확인할 수 있도록 key가 hashable할 필요가 있습니다
✔️ Syntax
제약은 하나만 걸 수 있습니다
하나의 class 혹은 하나의 protocol을 걸 수 있습니다
다만, protocol은 composition을 이용하면 여러 개 걸 수도 있습니다
func someFunction<T: SomeClass, U: SomeProtocolA & SomeProtocolB>(someT: T, someU: U) {
// function body goes here
}
✔️ 예제
1. non-Generic으로 우선
String말고도 광범위하게 사용될 수 있으므로 Generic으로 만들자
func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
2. Generic으로 만들긴 했는데..
컴파일이 안된다
value == valueToFind
라는 '비교구문'이 있는데
이 비교 연산자는 아무 타입이나 갖고 있는 메서드가 아니므로
컴파일러 입장에서는 Swift의 모든 타입에 대해 허용할 수 없다
func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
3. 적절한 제약 추가
우리는 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
}
🐼 Associated Types (in 프로토콜)
프로토콜을 정의할 때, 때때로 associated type을 정의하는 것이 유용합니다
Associated Type
은 프로토콜에서 사용하는 type parameter입니다
기존에 다루던 Generic함수/타입에서처럼 구체적으로 '어떤 타입인지'는 정의하지 않습니다
반면, '이런저런 요구사항들은 서로 같은 타입을 사용해야 해'를 정의합니다
예제로 살펴봅시다
✔️ 예제
associatedtype
이라는 키워드로 정의됩니다
아래 예제는 associated type과 이를 사용하는 3가지 요구사항을 정의합니다
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
Container
프로토콜의 요구사항을 정리하면
Item
이 어떤 타입이든 상관없다- 단, 적어도 하나의 타입에 대해 append()/count/subscript를 전부 구현해야 한다
이 요구사항을 준수하는 것은, Generic/non-Generic 타입 모두 가능합니다
1. non-Generic 타입이 준수할 때
프로토콜에서 Item
이 어떤 타입인지는 상관없고
적어도 하나의 타입에서 요구사항을 전부 구현하기만 하면 됩니다
그러므로 아래의 예제는 Container
프로토콜을 준수합니다
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
typealias Item = Int
mutating func append(_ item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
❗️주의❗️
여기서typealias
구문은 정석적으로 명시해주는게 맞으나
'컴파일러가 추론 가능한 상황'이라면 생략할 수 있긴 합니다
위 예제처럼 Int에 대해서만 모든 요구사항을 맞췄다면
컴파일러는 Int가 Item임을 추론할 수 있습니다
2. Generic 타입이 준수할 때
아래는 Generic 타입 예시입니다
(여기서도 typealis Item = Element
를 써주는게 정석이나 생략할 수 있습니다)
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]
}
}
✔️ Extension
공식문서에서는 Array를 예시로 들었습니다
하지만, Array는 Container
의 요구사항을 이미 준수하고 있기에
빈 extension에 프로토콜만 추가채택시키면 됩니다
(typealias도 추론가능한 상태)
extension Array: Container {}
✔️ 제약걸기
associatedtype을 정의할 때, 제약을 걸어주면 됩니다
protocol Container {
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
Container 프로토콜을 준수하기 위해선,
Item이 Equatable해야 한다는 추가 조건이 생겼습니다
✔️ 자기자신 타입을 다루도록 하는 프로토콜
어떤 프로토콜은 자신의 요구사항의 일부로 나타날 수도 있습니다
예를 들면 아래와 같이,
SuffixableContainer
의 요구사항에 SuffixableContainer이 등장합니다
protocol SuffixableContainer: Container {
associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
func suffix(_ size: Int) -> Suffix
}
위 프로토콜의 요구사항을 해석해보면,
func suffix()
의 return타입인 Suffix
는 2가지 룰이 있습니다
1. SuffixableContainer 프로토콜을 준수하는 타입이어야 합니다
이걸 준수하기 위한 대표적인 방법으론,
이 프로토콜을 채택하려는 타입, 자기자신 타입을 return하도록 만드는 것입니다.
물론 아래 예제와 같이,
SuffixableContainer을 채택하는 다른 타입이 와도 되긴 합니다
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>.
}
2. Container 프로토콜을 준수시키느라 결정한 Item 타입과 같아야 합니다
우선, 프로토콜 상속을 사용하므로
SuffixableContainer를 준수하려면, Container도 준수해야 합니다
Container를 준수시키느라 append() 등에 사용한 타입이 있을텐데
이 타입과 Suffix의 Item이 같은 타입이어야 합니다
물론, 위의 1번에서 대표적인 방법이라고 소개한 '자기자신 타입을 Suffix로 지정'한다면 이 조건은 신경쓸 필요가 없습니다 (같을 수 밖에 없기 때문)
하지만, '자기자신이 아닌 다른 타입을 Suffix로 지정'하고자 할 때는 조건이 맞지 않을 수 있습니다
Suffix로 쓰려는 다른 타입 또한 SuffixableContainer를 준수하므로
Container도 준수하려고 Item으로 지정한 타입이 있을텐데
그 타입과 현재 타입의 Item은 서로 다른 타입일 수 있기 때문입니다
역시 말로만 치면 힘드므로 예제를 만들어보았습니다
extension IntStack: SuffixableContainer {
func suffix(_ size: Int) -> Stack<Double> { //Error!
var result = Stack<Double>()
for index in (count-size)..<count {
result.append(self[index])
}
return result
}
// Inferred that Suffix is Stack<Int>.
}
위에서 Stack<Double>
역시
SuffixableContainer를 준수하므로 1번 조건은 맞춰졌지만
Item 타입이 서로 다르므로 (Double / Int) 2번 조건이 맞춰지지 않아
에러를 유발합니다
🐻❄️ Generic Where 구문
Type 제약은 Generic과 관련한 type paramter에 요구사항을 정의할 수 있게 해줍니다
이는 associated type에 대한 요구사항을 정의할 때도 유용합니다
'Generic Where 구문'을 정의함으로써 그렇게 할 수 있습니다
(바로 위 section에서도 다룸)
Generic where 구문을 사용하여, associated type에 걸 수 있는 조건
- 특정 프로토콜을 준수할 것
- 특정 type parameter와 동일한 타입일 것
where
키워드로 시작해서 constraint 혹은 equality 관계를 명시하면 됩니다
✔️ 예제
아래는 Generic 함수에 where 구문을 추가한 예시입니다
where구문은 return 타입 뒤에 옵니다
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
}
보이는 바와 같이, allItemsMatch()
함수는 C1.Item과 C2.Item이 같으면서 Equatable해야 사용할 수 있습니다
🐨 Extension에서의 Generic Where 구문
Generic 타입/프로토콜에 어떤 기능을 추가하려는데
특정 조건에 부합하는 type parameter에 대해서만 추가하고 싶다면
extension에 where조건을 걸어 구현할 수 있습니다
extension에 Generic 조건을 걸 수 있습니다
(사실 위 section에서 이미 다뤘음)
아까봤던 예제를 다시 봅시다
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
만약 이 예제에서 where 조건을 걸 수 없었다면,
==
연산자를 코드에 넣는데 있어 문제를 겪었을 것입니다
Protocol의 extension에도 where 조건을 걸 수 있습니다
이것도 이미 본 것 같은데..
extension Container where Item: Equatable {
func startsWith(_ item: Item) -> Bool {
return count >= 1 && self[0] == item
}
}
Item에 비교연산자를 사용하기 위해 Equatable해야 한다는 조건을 설정
조건은 프로토콜이 아닌 특정 타입인지 여부를 확인하는 것일 수 있습니다
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"
조건을 여러 개 걸 수도 있습니다
콤마로 구분하면 됩니다
extension temptemp where Item: Equatable, Item == Int {
func printprint() {
print("good")
}
}
🐯 where 조건을 작은 단위로 걸 수도 있다 (contextual)
조건을 타입 통째로 걸지 않고
특정 subscript나 메서드에만 where 조건을 걸 수도 있습니다
(물론, 해당 type이 Generic type이어야 함)
이를 문서에선 contextual where clause
라고 표현합니다
아래 예제에서 Container 구조체는 Generic이며
extension으로 추가하는 메서드 별로 조건을 걸어주고 있습니다
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
}
}
average()와 endsWith()는 원본 정의부에 있는 Item
type paramter에 각각 type 제약을 걸고 있습니다
extension에 걸어서 분리시켜는 방식도 있다
위 예제에서 의도한 것처럼, 메서드별로 서로 다른 조건을 걸어주기 위한 방법으로
extension에 type 제약을 걸고 extension을 분리시켜버리는 방법도 있습니다
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
}
}
개인적으로 이게 가독성이 더 좋아보입니다
🦁 Associated Type에서의 Generic Where 구문
슬슬 익숙하니 바로 예제로 봅시다
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
}
위 예제를 해석해보면
Container 프로토콜을 준수하려면 Iterator의 타입을 정의해야 하는데
이 Itertor는 IteratorProtocol을 준수하면서 Element가 Item과 동일한 타입이어야 합니다
프로토콜 상속관계가 있는 경우
부모 프로토콜(아래 예제에선 Container)의 associated type(Item)으로 조건을 걸 수도 있습니다
protocol ComparableContainer: Container where Item: Comparable { }
🐮 Generic subscript
subscript 역시 generic으로 정의할 수 있고 where 조건을 걸 수 있습니다
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
}
}
위 예제를 해석하면
1. subscript의 index는 Sequence 프로토콜을 준수할 것
2. 또한, Sequence.Iterator.Element의 타입이 Int일 것
(self[index]
와 result
가 같은 index 타입을 사용하도록)
3. return 타입은 Container 원본 정의부에 정의한 Item과 같은 타입의 배열일 것
이 제약들을 종합해보면 indices로 전달되는 값은 integer의 sequence임을 의미합니다
Author And Source
이 문제에 관하여(Swift Language Guide: Generics), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@yohanblessyou/Generics저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)