Generic 코드를 사용하면 정의한 요구 사항에 따라 모든 type에서 작동할 수 있는 유연하고 재사용 가능한 함수 및 type을 작성할 수 있습니다.
중복을 피하고 명확하고 추상적인 방식으로 의도를 표현하는 코드를 작성할 수 있습니다.
다음은 두 개의 Int 값을 교환하는 swapTwoInts(::)라는 nongeneric인 표준 함수입니다.
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
이 함수는 In-Out 매개변수에 설명된 대로 in-out 매개변수를 사용하여 및 b의 값을 교환합니다.
swapTwoInts(::) 함수는 b의 원래 값을 b로 바꾸고 a의 원래 값을 b로 바꿉니다. 이 함수를 호출하여 두 Int 변수의 값을 바꿀 수 있습니다.
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"
swapTwoInts(::) 함수는 유용하지만 Int 값에만 사용할 수 있습니다.
두 개의 String 값 또는 두 개의 Double 값을 교환하려면 아래 표시된 swapTwoStrings(::) 및 swapTwoDoubles(::) 함수와 같은 더 많은 함수를 작성해야 합니다.
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
}
swapTwoInts(::), swapTwoStrings(::) 및 swapTwoDoubles(::) 함수의 본문이 동일하다는 것을 눈치채셨을 것입니다. 유일한 차이점은 허용하는 값의 type(Int, String 및 Double)입니다.
모든 type의 두 값을 교환하는 단일 함수를 작성하는 것이 더 유용하고 훨씬 더 유연합니다. Generic 코드를 사용하면 이러한 기능을 작성할 수 있습니다. (이러한 함수의 Generic 버전은 아래에 정의되어 있습니다.)
세 함수 모두에서 a 및 b type은 동일해야 합니다.
a 와 b가 같은 type이 아니면 값을 바꿀 수 없습니다.
Swift는 type이 안전한 언어이며 (예를 들어) String type의 변수와 Double type의 변수가 서로 값을 교환하는 것을 허용하지 않습니다. 그렇게 하려고 하면 컴파일 시간 오류가 발생합니다.
Generic 함수는 모든 type에서 작동할 수 있습니다. 다음은 위의 swapTwoInts(::) 함수의 Generic 버전으로, swapTwoValues(::)라고 합니다.
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
swapTwoValues(::) 함수의 본문은 swapTwoInts(::) 함수의 본문과 동일합니다, 그러나 swapTwoValues(::)의 첫 번째 줄은 swapTwoInts(::)와 약간 다릅니다.
첫 번째 줄을 비교하는 방법은 다음과 같습니다.
func swapTwoInts(_ a: inout Int, _ b: inout Int)
func swapTwoValues<T>(_ a: inout T, _ b: inout T)
함수의 Generic 버전은 실제 type 이름(예: Int, String 또는 Double) 대신 자리 표시자 type 이름(이 경우 T라고 함)을 사용합니다.
자리 표시자 type 이름은 T가 무엇인지에 대해 아무 것도 말하지 않지만, T가 나타내는 것이 무엇이든 간에 와 b는 모두 동일한 type T여야 한다고 말합니다.
T 대신 사용할 실제 type은 swapTwoValues(_:_:) 함수가 호출될 때마다 결정됩니다.
Generic 함수와 Generic이 아닌 함수의 다른 차이점은 Generic 함수의 이름(swapTwoValues(::)) 다음에 꺾쇠 괄호(\) 안에 자리 표시자 type 이름(T)이 온다는 것입니다.
괄호는 T가 swapTwoValues(::) 함수 정의 내의 자리 표시자 type 이름임을 Swift에 알려줍니다.
T는 자리 표시자이기 때문에 Swift는 T라는 실제 type을 찾지 않습니다.
swapTwoValues(::) 함수는 이제 swapTwoInts와 같은 방식으로 호출될 수 있습니다. 단, 두 값이 서로 같은 type인 한 모든 type의 두 값을 전달할 수 있다는 점만 다릅니다.
swapTwoValues(::)가 호출될 때마다 T에 사용할 type은 함수에 전달된 값 type에서 유추됩니다.
아래 두 가지 예에서 T는 각각 Int 및 String으로 유추됩니다.
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"
위에 정의된 swapTwoValues(::) 함수는 Swift 표준 라이브러리의 일부인 스왑이라는 Generic 함수에서 영감을 얻었으며 앱에서 자동으로 사용할 수 있습니다.
자체 코드에서 swapTwoValues(::) 함수의 동작이 필요한 경우 자체 구현을 제공하는 대신 Swift의 기존 swap(::) 함수를 사용할 수 있습니다.
항상 타입 매개변수에 대문자 대소문자 이름(예: T 및 MyTypeParameter)을 지정하여 값이 아닌 type의 자리 표시자임을 나타냅니다.
Stack의 개념은 UINavigationController class에서 탐색 계층 Struct의 뷰 컨트롤러를 모델링하는 데 사용됩니다.
UINavigationController class의 pushViewController(:animated:) 메서드를 호출하여 뷰 컨트롤러를 탐색 Stack에 추가(또는 push)하고 popViewControllerAnimated(:) 메서드를 호출하여 탐색 Stack에서 뷰 컨트롤러를 제거(또는 pop)합니다.
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()
}
}
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
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."
그러나 Generic 함수 및 Generic 형식과 함께 사용할 수 있는 형식에 특정 형식 제약 조건을 적용하는 것이 때때로 유용합니다.
형식 제약 조건은 형식 매개 변수가 특정 class에서 상속되거나 특정 Protocol 또는 Protocol 구성을 준수해야 함을 지정합니다.
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
}
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>(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
Swift의 모든 표준 type은 Equatable Protocol을 자동으로 지원합니다.
func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
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 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]
}
}
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으로 Protocol 적합성 추가에 설명된 대로 기존 type을 extension하여 Protocol에 적합성을 추가할 수 있습니다.
extension Array: Container {}
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
}
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
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 함수, 아래 첨자 또는 형식과 연결된 형식 매개 변수에 대한 요구 사항을 정의할 수 있습니다.
연결된 type에 대한 요구 사항을 정의하는 것도 유용할 수 있습니다.
Generic where 절을 정의하여 이를 수행합니다.
Generic where 절을 사용하면 연결된 형식이 특정 Protocol을 준수해야 하거나 특정 형식 매개변수와 연결된 형식이 같아야 한다고 요구할 수 있습니다.
Generic where 절은 where 키워드로 시작하고 그 뒤에 연결된 형식에 대한 제약 조건 또는 형식과 연결된 형식 간의 동등 관계가 옵니다.
형식이나 함수 본문의 여는 중괄호 바로 앞에 Generic where 절을 작성합니다.
아래 예는 두 개의 Container 인스턴스가 같은 순서로 같은 items을 포함하는지 확인하는 allItemsMatch라는 Generic 함수를 정의합니다.
이 함수는 모든 items이 일치하면 부울 값 true를 반환하고 일치하지 않으면 false 값을 반환합니다.
검사할 두 개의 Container가 동일한 type의 Container일 필요는 없지만(일 수도 있지만) 동일한 type의 items을 보유해야 합니다.
이 요구 사항은 형식 제약 조건과 Generic 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
}
이 함수는 someContainer와 anotherContainer라는 두 개의 인수를 취합니다.
someContainer 인수는 C1 type이고 anotherContainer 인수는 C2 type입니다.
C1과 C2는 둘 다 함수가 호출될 때 결정되는 두 가지 Container type에 대한 type 매개변수입니다.
함수의 두 type 매개변수에 다음 요구사항이 적용됩니다.
첫 번째 및 두 번째 요구 사항은 함수의 type 매개 변수 목록에 정의되어 있고 세 번째 및 네 번째 요구 사항은 함수의 Generic where 절에 정의되어 있습니다.
이러한 요구 사항은 다음을 의미합니다.
- someContainer는 C1 type의 Container입니다.
- anotherContainer는 C2 type의 Container입니다.
- someContainer 및 anotherContainer는 동일한 type의 item을 포함합니다.
- someContainer의 items은 같지 않음 연산자(!=)를 사용하여 서로 다른지 확인할 수 있습니다.
세 번째와 네 번째 요구 사항이 결합되어 otherContainer의 item도 someContainer의 item과 정확히 같은 type이기 때문에 != 연산자로 확인할 수 있음을 의미합니다.
이러한 요구 사항을 통해 allItemsMatch(::) 함수는 Container type이 다른 경우에도 두 Container를 비교할 수 있습니다.
allItemsMatch(::) 함수는 두 Container에 동일한 수의 item이 포함되어 있는지 확인하는 것으로 시작합니다.
item 수가 다른 경우 일치시킬 방법이 없으며 함수는 false를 반환합니다.
이 검사를 수행한 후 함수는 for-in 루프와 반개방 범위 연산자(..<)를 사용하여 someContainer의 모든 items을 반복합니다.
각 item에 대해 함수는 someContainer의 items이 anotherContainer의 해당 items과 같지 않은지 확인합니다.
두 item이 같지 않으면 두 Container가 일치하지 않고 함수는 false를 반환합니다.
불일치를 찾지 않고 루프가 완료되면 두 Container가 일치하고 함수는 true를 반환합니다.
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."
위의 예에서는 String 값을 저장할 Stack 인스턴스를 만들고 3개의 문자열을 Stack에 push합니다.
이 예제에서는 또한 Stack과 동일한 세 개의 문자열을 포함하는 배열 리터럴로 초기화된 Array 인스턴스를 만듭니다.
Stack과 배열은 다른 type이지만 둘 다 Container Protocol을 준수하며 둘 다 동일한 type의 값을 포함합니다.
따라서 이 두 Container를 인수로 사용하여 allItemsMatch(::) 함수를 호출할 수 있습니다.
위의 예에서 allItemsMatch(::) 함수는 두 Container의 모든 item이 일치한다고 올바르게 보고합니다.
extension의 일부로 Generic where 절을 사용할 수도 있습니다.
아래 예제는 이전 예제의 Generic Stack Struct를 extension하여 isTop(_:) 메서드를 추가합니다.
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
이 새로운 isTop(_:) 메서드는 먼저 Stack이 비어 있지 않은지 확인한 다음 지정된 items을 Stack의 최상위 item과 비교합니다.
Generic where 절 없이 이 작업을 수행하려고 하면 문제가 발생합니다.
isTop(_:)의 구현은 == 연산자를 사용하지만 Stack의 정의는 item이 동일할 필요가 없으므로 == 연산자를 사용하면 컴파일 타임 오류가 발생합니다.
Generic where 절을 사용하면 Stack의 item이 동일할 때만 extension이 isTop(_:) 메서드를 추가하도록 extension에 새 요구 사항을 추가할 수 있습니다.
isTop(_:) 메서드가 작동하는 모습은 다음과 같습니다.
if StackOfStrings.isTop("tres") {
print("Top element is tres.")
} else {
print("Top element is something else.")
}
// Prints "Top element is tres."
요소가 동일하지 않은 Stack에서 isTop(_:) 메서드를 호출하려고 하면 컴파일 시간 오류가 발생합니다.
```swift
struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue) // Error
```
Protocol extension과 함께 Generic where 절을 사용할 수 있습니다. 아래 예제는 이전 예제의 Container Protocol을 extension하여 startsWith(_:) 메서드를 추가합니다.
extension Container where Item: Equatable {
func startsWith(_ item: Item) -> Bool {
return count >= 1 && self[0] == item
}
}
startsWith(_:) 메서드는 먼저 Container에 하나 이상의 item이 있는지 확인한 다음 Container의 첫 번째 item이 지정된 item과 일치하는지 확인합니다.
이 새로운 startsWith(_:) 메서드는 Container의 item이 동일하다면 위에서 사용된 Stack 및 배열을 포함하여 Container Protocol을 준수하는 모든 type과 함께 사용할 수 있습니다.
if [9, 9, 9].startsWith(42) {
print("Starts with 42.")
} else {
print("Starts with something else.")
}
// Prints "Starts with something else."
위의 예에서 Generic where 절은 Item이 Protocol을 준수하도록 요구하지만 Item이 특정 type이 되도록 요구하는 Generic where 절을 작성할 수도 있습니다.
예를 들어
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"
이 예에서는 Item type이 Double인 Container에 average() 메서드를 추가합니다.
Container의 item을 반복하여 합산하고 Container 수로 나누어 평균을 계산합니다.
부동 소수점 나누기를 수행할 수 있도록 카운트를 Int에서 Double로 명시적으로 변환합니다.
extension의 일부인 Generic where 절에 여러 요구 사항을 포함할 수 있습니다, 다른 곳에서 작성하는 Generic where 절에 대해 수행할 수 있는 것과 같습니다.
목록의 각 요구 사항을 쉼표로 구분합니다.
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 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
}
}
연결된 type에 Generic where 절을 포함할 수 있습니다.
예를 들어, Sequence Protocol이 표준 라이브러리에서 사용하는 것과 같은 iterator를 포함하는 Container 버전을 만들고 싶다고 가정합니다.
작성 방법은 다음과 같습니다.
```swift
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 절은 iterator의 type에 관계없이 iterator가 Container의 item과 동일한 item type의 요소를 순회해야 한다고 요구합니다. makeIterator() 함수는 Container의 iterator에 대한 액세스를 제공합니다.
다른 Protocol에서 상속하는 Protocol의 경우 Protocol 선언에 Generic where 절을 포함하여 상속된 Associated type에 제약 조건을 추가합니다.
예를 들어 다음 코드는 Item이 Comparable을 준수해야 하는 ComparableContainer Protocol을 선언합니다.
protocol ComparableContainer: Container where Item: Comparable { }
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
}
}