제네릭
- 제네릭을 이용해 코드를 구현하면 어떤 타입에도 유연하게 대응할 수 있다.
- 제네릭으로 구현한 기능과 타입은 재사용하기 쉽고, 코드의 중복을 줄일 수 있다.
- 타입 파라미터<T>는 함수 내부에서 타입의 이나 리턴형으로 사용된다.
- 관습적으로 T를 사용하지만 사용하지만 다른문자를 사용해도 되면 Upper camel case를 사용한다.
- <T, A>처럼 2개이상을 선언해도 된다.
제네릭이 필요한 이유
var numA = 10
var numB = 20
print(numA, numB) // 10 20
// 두 숫자를 서로 교환하는 함수
func swapInt(_ a: inout Int, _ b: inout Int){
let tmp = a
a = b
b = tmp
}
swapInt(&numA, &numB)
print(numA, numB) // 20 10
두 숫자를 교환하는 함수를 이렇게 정의할 수 있다.
근데 만약 String이나 Double을 교환하려면 모든 타입에 해당하는 함수를 다시 작성해야한다.
// 두 문자를 서로 교환하는 함수
func swapInt(_ a: inout String, _ b: inout String){
let tmp = a
a = b
b = tmp
}
// 두 실수를 서로 교환하는 함수
func swapInt(_ a: inout Double, _ b: inout Double){
let tmp = a
a = b
b = tmp
}
이런 번거로움과 코드 재사용성을 높이기 위해 제네릭이란 문법이 필요하다.
func printArray<T>(array: [T]){
for element in array{
print(element)
}
}
func swapInt<T>(_ a: inout T, _ b: inout T){
let tmp = a
a = b
b = tmp
}
실제로 Array와 Dictionary는 제네릭으로 생성되어있고, 다음과같이 작성되어있다.
struct Array<Element>
struct Dictionary<Key, Value> where Key : Hashable
제네릭의 활용
struct Stack<T> {
var items = [T]()
mutating func push(_ item: T) {
items.append(item)
}
mutating func pop() -> T {
return items.removeLast()
}
}
var intStack: Stack<Int> = Stack<Int>()
var doubleStack: Stack<Double> = Stack<Double>()
intStack.push(1)
intStack.push(2)
intStack.push(3)
print(intStack) //Stack<Int>(items: [1, 2, 3])
doubleStack.push(1.0)
doubleStack.push(2.0)
doubleStack.push(3.0)
print(doubleStack) //Stack<Double>(items: [1.0, 2.0, 3.0])
FILO(First-In-Last-Out)방식으로 동작하는 스택을 제네릭을 활용해 구현할 수 있다.
여기서 Element는 타입임을 나타내는 표시일 뿐이고, 함수의 인풋, 아웃풋으로 사용할 수 있으며 타입을 생성할수도 있다.
제네릭의 확장
제네릭을 extension(확장)에도 적용할 수 있다.
확장을 할때에는 <T>를 적지 않고 본체에서만 Placeholder를 정의할 수 있다.
extension Stack{ // Stack<Element> (X)
// 랜덤한 1개를 리턴하는 메서드
func returnRandom() -> T{
return items.randomElement()!
}
}
타입제약
- 타입 제약은 타입이 가져야할 제약사항을 지정할 수 있는 방법이다.
- 타입 제약은 클래스 타입 또는 프로토콜로만 줄 수 있다.
- 제네릭에 타입에 제약을 주고싶으면 타입 매개변수 뒤에 콜론을 붙히고, 원하는 클래스 타입 또는 프로토콜을 명시하면 된다.
- 여러 제약을 추가하고싶다면 콤마로 구분하지 않고 where절을 이용한다.
func swapInt<T: BinaryInteger>(_ a: inout T, _ b: inout T){
let tmp = a
a = b
b = tmp
}
func swapFloatingpoint<T: BinaryInteger>(_ a: inout T, _ b: inout T) where T: FloatingPoint{
let tmp = a
a = b
b = tmp
}
// Int타입에만 적용되는 확장과, returnRandom()메서드
extension Stack where T == Int{ // Stack<Element> (X)
// 랜덤한 1개를 리턴하는 메서드
func returnRandom() -> T{
return items.randomElement()!
}
}
// Int가 아닌 타입(Double, String...) 에는 이 확장이 적용되지 않아 returnRandom()메서드가 존재하지 않는다.
// T(타입)은 Equatable프로토콜을 채택한 타입만 함수에서 사용가능하다는 제약⭐️
func findIndex<T: Equatable>(item: T, array: [T]) -> Int?{
for (index, value) in array.enumerated(){
if item == value{
return index
}
}
return nil
}
// 구체화된 함수도 구현 가능하다. 항상 제네릭을 적용시킨 함수만 사용하게되면 불편함이 있을 수 있다.
// 제네릭이 존재하더라도 동일한 함수이름에 구체적인 타입을 명시하면 해당 구체적인 타입의 함수가 실행된다.⭐️
// 문자열의 경우 대소문자를 무시하고 비교하고 싶다면 아래처럼 구현 가능
func findIndex(item: String, array: [String]) -> Int?{
for (index, value) in array.enumerated(){
if item.caseInsensitiveCompare(value) == .orderedSame{
return index
}
}
return nil
}
열거형에서의 제네릭
- 열거형에서 연관값을 가질 때 제네릭으로 정의할 수 있다.
- 어차피 케이스는 자체가 선택항목중에 하나일 뿐이고, 타입으로 정의할 일은 없다.
enum Pet<T>{
case dog
case cat
case etc(T)
}
let snake = Pet.etc("뱀")
let 무거운동물 = Pet.etc(130)