-
GenericsiOS/Swift 공식문서 2022. 2. 25. 23:41
Generics
generic은 개발자가 정의한 요구사항에 따라 어느 타입과도 사용할 수 있는 유연하고, 재사용 가능한 함수와 타입을 작성할 수 있게 한다. 이를 통하여 코드 작성 시 중폭을 피하고 개발자의 의도를 명확하고 추상적인 방식으로 전달할 수 있다.
generic는 Swift의 가장 강력한 기능 중 하나이고, Swift 표준 라이브러리의 많은 코드가 generic으로 작성되어 있다. 예를 들어,
Array
와Dictionary
타입도 모두 generic이다.Int
,String
뿐만 아니라 Swift에서 만들어진 다른 모든 타입의 컬렉션을 만들 수 있다.
The Problem That Generics Solve
swapTwoInts(_:_:)
함수를 정의해보자.let temporaryA = a a = b b = temporaryA }
이 함수의 문제점은
Int
값에 대해서만 사용할 수 있다는 것이다.String
이나Double
값을 swap하기 위해서는 이를 위한 함수들을 따로 정의해야 한다. body가 모두 같은 중복 코드를 작성하게 되는 것이다.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 }
note
위의 세 함수 모두
a
,b
의 타입이 같아야 한다. Swift는 type-safe language이기 때문에, 서로 다른 타입을 swap하려 하면 컴파일 에러가 발생한다.
Generic Functions
swapTwoValues(_:_:)
함수를 다음과 같이 정의하자.Int
와 같은 실제 타입 대신T
라는 placeholder type을 사용한다.T
를 통해 매개변수의 실제 타입은 정해지지 않지만, 두 매개변수의 타입이 같아야 한다는 것을 나타낼 수 있다.T
에 사용될 실제 타입은 함수가 호출될 때 결정된다. 함수 이름 뒤에 bracket 안에 placeholder type name을 명시해야 하므로 주의하자.func swapTwoValues<T>(_ a: inout T, _ b: inout T) { let temporaryA = a a = b b = temporaryA }
generic 함수를 정의하면 어떤 타입으로도 함수를 호출할 수 있다.
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"
note
위 예시는 generic을 설명하기 위한 예시이다. 위와 같이 직접 구현하지 말고 Swift에서 제공하는
swap(_:_:)
함수를 사용하자.
Type Parameters
위의
swapTwoValues(_:_:)
함수에서 사용되는 placeholder type인T
를 type parameter라고 한다. type parameter는 placeholder type의 이름을 명시하고, 함수 이름 바로 뒤에 bracket(<>
) 안에 적는다.
type parameter를 명시하면, 함수 매개변수의 타입이나 리턴 타입을 정의할 때 사용할 수 있다. 함수 본문 안에서 type annotation으로 사용하는 것도 가능하다. generic 함수가 호출될 때마다 type parameter는 실제 타입으로 교체된다. 위의
swapTwoValues(_:_:)
함수를 호출한 코드에서 처음 호출할 때는T
가Int
로 교체되었고, 두번째로 호출할 때는String
으로 교체되었다.
여러 type parameter를 사용하는 것도 가능한데, type parameter들을
<>
안에 콤마로 구분하여 명시하면 된다.
Naming Type Parameters
대부분의 경우 type parameter는 descriptive한 이름을 가지고 있다.
Dictionary<Key, Value>
에서의Key
와Value
,Array<Element>
에서의Element
를 예로 들 수 있다. 이러한 이름들은 type parameter와 generic type 혹은 function 사이의 관계를 알 수 있게 한다. 하지만 그 사이의 의미있는 관계가 없는 경우,T
,U
,V
등의 한 글자로 적는 것이 일반적이다.
note
type parameter는 type의 placeholder이기 때문에 upper camel case로 적도록 하자.
Generic Types
generic function 뿐만 아니라, generic type도 정의할 수 있다. 다음과 같이 Stack을 구현하기 위해
IntStack
구조체를 정의하자. 이러한 경우Int
값 밖에 넣지 못한다.struct IntStack { var items: [Int] = [] mutating func push(_ item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() } }
다음과 같이 generic으로 구현하면 모든 타입에 대하여 Stack을 사용할 수 있다.
struct Stack<Element> { var items: [Element] = [] mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() } } var stackOfStrings = Stack<String>() stackOfStrings.push("uno") stackOfStrings.push("dos") stackOfStrings.push("tres") stackOfStrings.push("cuatro") // the stack now contains 4 strings
Extending a Generic Type
generic type을 확장할 경우, type parameter들을 extension의 정의에 적지 않는다. 대신 타입을 처음 정의할 때 사용했던 type parameter를 extension에서 사용할 수 있다.
다음과 같이 처음 정의 시 type parameter인
Element
를 extension에서 사용할 수 있다.extension Stack { var topItem: Element? { return items.isEmpty ? nil : items[items.count - 1] } } if let topItem = stackOfStrings.topItem { print("The top item on the stack is \(topItem).") } // Prints "The top item on the stack is tres."
Type Constraints
type constraint는 type parameter가 특정 클래스나 상속받거나, 프로토콜, 혹은 프로토콜의 composition을 준수함을 명시한다. 예를 들어,
Dictionary
타입의 경우Key
로 사용되는 타입은Hashable
프로토콜을 준수해야 한다. 그래야 특정 key 값이 이미 포함되어 있는지 확인할 수 있기 때문이다. 커스텀으로 generic 타입을 만드는 경우에도 type constraint를 자체적으로 정의할 수 있다.
Type Constraint Syntax
각 type parameter 이름 위에 콜론(:)과 함께 하나의 클래스나 프로토콜을 명시하면 된다. 다음 예시에서
T
는SomeClass
를 상속받아야 하고,U
는SomeProtocol
을 준수해야 함을 명시하고 있다.func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) { // function body goes here }
다음 예시를 살펴보자. 다음과 같이 배열에서 특정 element의 index를 반환하는
findIndex(of:in:)
함수를 정의하자.func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { return index } } return nil }
위 코드를 보면
if
문에서==
연산자를 사용하였는데, 모든 타입이 이 연산자를 사용할 수 있는 것이 아니기 때문에 컴파일이 되지 않는다. 따라서 아래 코드처럼 연산자를 사용할 수 있도록 type parameterT
가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 }
이렇게 수정한 이후에는
findIndex
함수를 정상적으로 사용할 수 있다.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
Associated Types
프로토콜을 정의할 때 정의의 일부로 associated type을 선언하는 경우가 있다. associated type은 프로토콜의 일부로서 사용되는 타입의 placeholder name을 제공한다. associated type으로 사용될 실제 타입은 프로토콜이 채택될 때 명시된다.
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 } }
다음과 같이 non-generic type인
IntStack
을 정의할 수 있다.typealias
키워드를 사용하여Item
의 실제 타입이Int
임을 명시하고 요구사항을 구현하였다. 하지만 구현에서Item
에 실제 사용될 타입이 분명하기 때문에typealias
를 사용한 코드를 생략해도 된다.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] } }
다음과 같이 generic type을 정의할 수 있다.
Container
프로토콜에서Item
이 사용된 자리에 parameter type인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] } }
Extending an Existing Type to Specify an Associated Type
Array
의 경우 프로퍼티 메서드가 이미Container
의 요구사항을 만족하기 때문에 다음과 같이 empty extension을 통해 프로토콜을 준수함을 명시할 수 있다.extension Array: Container {}
Adding Constraints to an Associated Type
다음과 같이 콜론 뒤에 타입을 명시하여 상속받거나 준수해야 하는 타입을 정할 수 있다.
protocol Container { associatedtype Item: Equatable mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } }
Using a Protocol in Its Associated Type's Constraints
프로토콜은 요구사항의 일부로 나타날 수 있다. 아래 코드에서
SuffixableContainer
프로토콜은Suffix
라는 associated type과suffix(_ :)
메서드를 추가로 요구한다.Suffix
타입은SuffixableContainer
프로토콜을 준수해야 하고Item
타입은Container
프로토콜의Item
타입과 동일해야 한다.protocol SuffixableContainer: Container { associatedtype Suffix: SuffixableContainer where Suffix.Item == Item func suffix(_ size: Int) -> Suffix }
다음과 같이
Stack
를 확장하여 프로토콜의 요구사항을 만족할 수 있다.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
associated type에 대한 요구사항을 정의할 때는 generic where clause를 사용할 수 있다.
where
뒤에 associated type에 대한 제약조건이나, 다른 타입과 associated type 사이의 관계를 적으면 된다. 이러한 generic where clause는 함수 body의 바로 앞에 작성한다.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 }
위 코드에서 두 type parameter에 대한 요구사항은 다음과 같다.
a.
C1
,C2
는Container
프로토콜을 준수한다.
b.C1
,C2
의Item
의 타입이 같아야 한다.
c.C1.Item
은Equatable
프로토콜을 준수해야 한다.
a의 경우 type parameter list에 정의되어 있고, b와 c는 generic where clause에 정의되어 있다.
결국 다음과 같이
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.") } // Prints "All items match."
Extensions with a Generic Where Clause
extension의 일부로 generic where clause를 사용할 수 있다.
extension Stack where Element: Equatable { func isTop(_ item: Element) -> Bool { guard let topItem = items.last else { return false } return topItem == item } } if stackOfStrings.isTop("tres") { print("Top element is tres.") } else { print("Top element is something else.") } // Prints "Top element is tres."
Equatable
프로토콜을 준수하지 않는 element를 사용한 Stack에서isTop()
메서드를 호출하면 컴파일 에러가 발생한다.struct NotEquatable { } var notEquatableStack = Stack<NotEquatable>() let notEquatableValue = NotEquatable() notEquatableStack.push(notEquatableValue) notEquatableStack.isTop(notEquatableValue) // Error
다음과 같이 parameter 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"
Contextual Where Clauses
where
를 사용하여 선언의 일부에서 서로 다른 제약조건을 추가할 수 있다. 다음 코드는 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 } } let numbers = [1260, 1200, 98, 37] print(numbers.average()) // Prints "648.75" print(numbers.endsWith(37)) // Prints "true"
다음과 같이 두 개의 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 Types with a Generic Where Clause
다음과 같이 associated type에 대하여 generic where clause로 제약조건을 설정할 수 있다. 다음 프로토콜에서는 두 개의 associated type을 선언했는데, 그 중
Iterator
의 경우 제약조건이 설정되어 있다.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 { }
Generic Subscripts
subscript도 generic으로 정의할 수 있다. 다음과 같이
subscript
키워드 뒤에 bracket 안에 type parameter를 명시하면 된다. 또한 함수 body 바로 앞에 generic where clause를 작성할 수 있다.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 } }
Reference
https://docs.swift.org/swift-book/LanguageGuide/Generics.html
'iOS > Swift 공식문서' 카테고리의 다른 글
Automatic Reference Counting (0) 2022.03.01 Opaque Types (0) 2022.02.27 Protocols (0) 2022.02.24 Extensions (0) 2022.02.23 Nested Types (0) 2022.02.23