여러 타입에 대한 동작을 작성하고 해당 타입의 요구사항을 지정합니다.
제너릭 코드(Generic code)는 정의한 요구사항에 따라 모든 타입에서 동작할 수 있는 유연하고 재사용 가능한 함수와 타입을 작성할 수 있습니다. 중복을 피하고 명확하고 추상적인 방식으로 의도를 표현하는 코드를 작성할 수 있습니다.
제너릭은 Swift의 강력한 특징 중 하나이고 Swift 표준 라이브러리 대부분은 제너릭 코드로 되어 있습니다. 예를 들어 Swift의 Array
와 Dictionary
타입은 둘다 제너릭 콜렉션 입니다. Int
값을 가진 배열, 또는 String
값을 가진 배열 또는 실제로 Swift에서 생성될 수 있는 다른 모든 타입에 대한 배열을 생성할 수 있습니다. 마찬가지로 모든 지정된 타입의 값을 저장하기 위한 딕셔너리를 생성할 수 있고 해당 타입에 대한 제한은 없습니다.
다음은 두 Int
값을 바꾸는 swapTwoInts(_:_:)
라는 제너릭이 아닌 함수를 나타냅니다.
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
이 함수는 In-Out 파라미터에서 설명한 대로 a
와 b
의 값을 바꾸기 위해 in-out 파라미터를 사용하여 만듭니다.
swapTwoInts(_:_:)
함수는 b
의 값을 a
로 그리고 a
의 값을 b
로 바꿉니다. 2개의 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
값만 사용이 가능 합니다. 2개의 String
값 또는 2개의 Double
값을 바꾸길 원하면 아래의 swapTwoStrings(_:_:)
와 swapTwoDoubles(_:_:)
함수와 같이 더 많은 함수를 작성해야 합니다.
func swapTwoString(_ 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(_:_:)
함수의 본문이 동일하다는 것을 알 수 있습니다. 차이점은 받아들이는 값의 타입만 (Int, String, 그리고 Double)
다릅니다.
모든 타입의 2개의 값을 바꾸는 단일 함수로 작성하면 더 유용하고 더 유연합니다. 제너릭 코드는 이러한 함수를 작성할 수 있습니다.(이 함수의 제너릭 버전은 아래에 정의됩니다.)
Note
이 3개의 함수는a
와b
의 타입이 모두 같아야 합니다.a
와b
가 같은 타입이 아니면 바꾸는 것은 불가능합니다. Swift는 타입 안전성 언어이고String
타입의 변수와Double
타입의 변수가 서로 값을 바꾸도록 허락하지 않습니다. 이러한 시도는 컴파일 에러가 발생합니다.
제너릭 함수(Generic functions)는 모든 타입과 함께 동작할 수 있습니다. 다음은 swapTwoValues(_:_:)
라는 위의 swapTwoInts(_:_:)
함수의 제너릭 버전입니다.
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 swapTwoValue<T>(_ a: inout T, _ b: inout T)
함수의 제너릭 버전은 Int
, String
또는 Double
와 같은 실제 타입 이름 대신에 이 경우 T
라는 임의의 타입 이름을 사용합니다. 이 임의의 타입 이름은 T
가 무엇이어야 하는지 아무 말도 하지 않지만 T
가 무엇을 나타내든 a
와 b
는 모두 같은 타입 T
여야 한다고 말합니다. T
의 실제 타입은 swapTwoValue(_:_:)
함수가 호출될 때마다 결정됩니다.
제너릭 함수와 제너릭이 아닌 함수 사이의 다른 차이점은 제너릭 함수의 이름(swapTwoValue(_:_:)
)에 바로 임의의 타입 이름 (T)
이 꺽쇠 괄호 내 (<T>
)에 위치한다는 것입니다. 이 괄호는 T
는 swapTwoValue(_:_:)
함수 정의 내에서 임의의 타입 이름이라고 Swift에게 말합니다. T
는 임의의 타입이므로 Swift는 T
라는 실제 타입을 찾지 않습니다.
swapTwoValue(_:_:)
함수는 이제 swapTwoInts
와 동일한 방식으로 호출될 수 있지만 두 값이 서로 동일한 타입이면 모든 타입의 두 값을 전달할 수 있다는 점이 다릅니다. swapTwoValue(_:_:)
가 호출될 때마다 T
로 사용한 타입은 함수에 전달된 값의 타입으로 부터 유추됩니다.
아래의 2개의 예제에서 T
는 각각 Int
와 String
으로 추론됩니다.
var someInt = 3
var anotherInt = 107
swapTwoValue(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3
var someString = "hello"
var anotherString = "world"
swapTwoValue(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"
대부분의 경우 타입 파라미터는 타입 파라미터와 제너릭 타입 간의 관계나 함수 간의 관계를 나타내기 위해 Dictionary<Key, Value>
에서 Key
와 Value
그리고 Array<Elementa>
에서 Element
와 같이 설명이 포함된 이름이 있습니다. 그러나 의미있는 관계가 없을 때는 위에서 swapTwoValue(_:_:)
함수에서 T
와 같이 T
, U
, 그리고 V
와 같은 단일 문자를 사용하여 이름을 지정하는 것이 일반적입니다.
제너릭 함수 외에도 Swift는 고유한 제너릭 타입을 정의할 수 있습니다. 이것은 Array
와 Dictionary
와 유사한 방식으로 모든 타입에서 동작할 수 있는 사용자 정의 클래스, 구조체, 그리고 열거형입니다.
이번 섹션은 Stack
이라는 제너릭 콜렉션 타입을 어떻게 작성하는지 보여줍니다. 스택은 배열과 유사하지만 Swift의 Array
타입보다 더 제한된 작업 집합을 가진 순서가 지정된 집합입니다. 배열은 모든 위치에서 새 항목을 삽입하고 제거할 수 있습니다. 그러나 스택은 새로운 항목이 콜렉션의 끝에 추가하는 것만 허락합니다.(스택에 새로운 값을 푸쉬 한다고 알려져 있음). 마찬가지로 스택은 콜렉션의 끝부분의 항목만 제거할 수 있습니다.(스택에서 값을 팝 한다고 알려져 있음).
다음은 스택의 제너릭이 아닌 버전을 어떻게 작성하는지 나타내며 이 경우 Int
값의 스택에 대한 것을 보여줍니다.
struct IntStack {
var items: [Int] = []
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
이 구조체는 스택에 값을 저장하기 위해 items
라는 Array
프로퍼티를 사용합니다. Stack
은 스택에 값을 푸쉬화고 팝하기 위해 push
와 pop
인 2개의 메서드를 제공합니다. 구조체의 items
배열을 수정하거나 변경이 필요하므로 이 메서드는 mutating
으로 표시되어 있습니다.
그러나 위에 IntStack
타입은 Int
값만 사용할 수 있습니다. 모든 타입의 값으로 스택을 관리할 수 있는 제너릭 Stack
구조체를 정의하는 것이 더 유용합니다.
다음은 같은 코드의 제너릭 버전입니다.
struct Stack<Element> {
var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
Stack
의 제너릭 버전은 기본적으로 제너릭이 아닌 버전과 동일하지만 Int
의 실제 타입 대신에 Element
라는 타입 파라미터를 사용합니다. 이 타입 파라미터는 구조체의 이름 바로뒤에 꺽쇠 괄호 내에 작성됩니다.
Element
는 나중에 제공할 타입에 대한 임의의 이름을 정의합니다.
이 미래의 타입은 구조체의 정의 내 어디서나 Element
로 참조될 수 있습니다. 이 경우에 Element
는 아래의 3군데에서 임의로 사용됩니다.
Element
타입의 값으로 빈 배열로 초기화 되는 item
라는 프로퍼티를 생성할 때Element
타입이어야 하는 item
이라는 단일 파라미터를 가지는 push(_:)
메서드를 지정할 때pop()
메서드에 의해 반환된 값이 Element
타입의 값으로 지정할 때제너릭 타입이므로 Stack
은 Array
와 Dictionary
와 유사하게 Swift에서 모든 유효한 타입의 스택을 생성하기 위해 사용될 수 있습니다.
꺽쇠 괄호 내에 스택에 저장될 타입을 작성하여 새로운 Stack
인스턴스를 생성합니다. 예를 들어 문자열에 새로운 스택을 생성하려면 Stack<String>()
이라 작성합니다.
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
제너릭 타입을 확장할 때 확장의 정의의 부분으로 타입 파라미터 리스트를 제공하지 않습니다. 대신에 기존 타입 정의에서 타입 파라미터 리스트는 확장의 본문 내에서 가능하고 기존 타입 파라미터 이름은 기존 정의에서 타입 파라미터를 참조하는데 사용됩니다.
다음의 예제는 스택에 팝 없이 스택의 가장 상단의 항목을 반환하는 topItem
이라는 읽기전용 계산된 프로퍼티를 추가하기 위해 제너릭 Stack
타입을 확장합니다.
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
topItem
프로퍼티는 Element
타입의 옵셔널 값을 반환합니다. 스택이 비어있다면 topItem
은 nil
을 반환하고 스택이 비어 있지 않다면 topItem
은 items
배열에서 마지막 항목을 반환합니다.
이 확장은 타입 파라미터 리스트를 정의하지 않습니다. 대신에 Element
라는 Stack
타입의 존재하는 타입 파라미터 이름은 topItem
계산된 프로퍼티의 옵셔널 타입임을 나타내기 위해 확장 내에서 사용됩니다.
topItem
계산된 프로퍼티는 최상단 항목의 삭제없이 접근하고 조회하기 위해 모든 Stack
인스턴스에서 사용될 수 있습니다.
if let topItem = stackOfStrings.topItem {
print("The top item on the stack is \(topItem).")
}