티스토리 뷰
• The Problem That Generics Solve
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
받은 두 변수를 스왑시켜주는 간단한 함수가 있다.
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
}
간단한 기능을 가진 함수지만, 타입마다 함수를 새로 만들어 줘야 한다.
• Generic Functions
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
제네릭을 이용하면 타입을 실제 타입의 이름 대신 T, 즉 타입 파라미터(원하는 이름으로 지을 수 있다. 일반적으로 T를 쓴다.) 로 표시할 수 있다.
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"
각 타입에 맞게 값을 넣어주면 T는 내부에서 알맞은 타입으로 바꾸어 코드를 진행한다.
• Naming Type Parameters
타입 파라미터의 이름은 설명할 수 있는 이름을 가진다. 예로 Dictionary<Key, Value>의 Key와 Value 처럼. 이를 통해 타입 파라미터, 제네릭 타입과 함수 사이의 알려줄 수 있다. 그러나 별 의미있는 관계가 없다면, T나 U 같은 싱글 문자로 표기한다.
• Generic Types
generic function에 추가적으로 스위프트는 자신만의 제네릭 타입을 만들 수 있다. 문서는 스택(stack)을 구현하는 것을 예시로 들었다.
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()
}
}
제네릭 타입을 이용해 여러 타입에 부합한 형태를 만들었다.
• Extending a Generic Type
제네릭 타입을 익스테션할 때 타입 파라미터를 만들 순 없지만 원본 타입에 정의한 타입 파라미터는 익스텐션 내부에서 활용할 수 있다. 아래 예제는 위 스택 예시를 익스텐션해 스택 가장 위에 있는 아이템을 가져오는 연산 프로퍼티를 만들었다.
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
• Type Constraints
앞선 변수를 스왑하는 코드는 어떤 타입이 오건 작동하는 코드였다. 그러나 가끔은 제네릭 함수나 제네릭 타입에 들어갈 수 있는 타입에 제약을 걸어줘야할 필요가 있다. 타입 제약은 타입 파라미터가 특정 클래스를 상속하거나 또는 특정 프로토콜을 준수해야만 하게 만든다. 예를 들어 스위프트의 Dictionary의 key는 Dictionary의 key값이 고유함을 가지기 위해서 hashable 프로토콜을 준수해야만 한다.
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// function body goes here
}
타입 파라미터 뒤에 제약조건인 클래스와 프로토콜을 집어넣어 줄 수 있다. 아래는 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
프로토콜을 정의할 때 associated type을 정의해줄 수 있다. associated type은 프로토콜이 채택되기 전까지는 실제 타입이 정해지지 않는다.
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
위 코드를 보면 Item 이라는 associated type을 선언하고 이를 타입으로 사용해 메소드의 파라미터나 서브스크립트의 리턴타입으로서 활용하고 있지만, Item 이 어떤 타입인지는 실제 구현부에서 정해진다.
만약 한 타입이 프로토콜의 준수사항을 이미 준수하고 있다면, 바디없이 선언부만으로도 프로토콜 채택을 위한 익스텐션이 가능함을 전 챕터에서 확인했다 .마찬가지로 예를 들어 Array 타입은 Container 프로토콜의 준수사항인 서브스크립트, 카운트 변수, append 메소드를 이미 구현해놓았음으로 익스텐션해 프로토콜 채택이 가능하다. 이를 통해 associated type도 같이 활용가능하다.
extension Array: Container {}
마찬가지로 associatedtype에도 제약을 줄 수 있다.
protocol Container {
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
}
두가지 제약이 있는데 첫째로. Suffix 타입은 SuffixableContainer 프로토콜을 준수해야하며 둘째로, Suffix 타입의 Item은 Container의 Item과 타입이 일치해야 한다,
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
• Generic Where Clauses
where는 associated type이 특정 프로토콜을 준수하거나 특정 타입 파라미터와 associated type이 일치하도록 만들 수 있다.
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
}
• Extensions with a Generic Where Clause
익스텐션을 사용할 때도 where을 사용가능하다.
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
• Associated Types with a Generic Where Clause
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
}
protocol ComparableContainer: Container where Item: Comparable { }
associated type에게도 where절을 쓸 수 있다.
• Generic Subscripts
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
}
}
제네릭의 서브스크립트에도 where절을 쓸 수 있다.
'Swift Language Guide' 카테고리의 다른 글
Protocols(프로토콜) (0) | 2020.11.26 |
---|---|
Extensions (0) | 2020.11.24 |
타입 캐스팅 + 중첩 타입 (0) | 2020.11.24 |
Error Handling(에러 핸들링) (0) | 2020.11.19 |
Optional Chaining(옵셔널 체이닝) (0) | 2020.11.18 |