Generic(제네릭) - ehrldyd15/Swift_Skills GitHub Wiki
타입에 의존하지 않는 코드를 작성할떄 사용한다. (코드의 중복을 피하기 위함)
애플에 의하면 Swift에서 가장 강력한 기능 중 하나이며 표준 라이브러리의 대부분은 제네렉으로 선언이 되어 있다고 한다.
참고로 Array와 Dictionary도 제네릭 타입이다.
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let tempA = a
a = b
b = tempA
}
위의 함수는 두 Int 타입의 값을 swap하는 함수이다.
여기에서 파라미터가 모두 Int라면 문제가 없지만 Double이나 String일 경우에는 사용이 불가능하다.
이 두가지 타입이 모두 가능하게 하려면
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
let tempA = a
a = b
b = tempA
}
func swapTwoStrings(_ a: inout String, _ b: inout String) {
let tempA = a
a = b
b = tempA
}
위처럼 함수를 따로 만들어서 오버로딩 할 수 있지만 비효율 적이다.
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let tempA = a
a = b
b = tempA
}
위처럼 <>내부에 T를 선언해주면 그 뒤로 해당 이름을 타입처럼 사용할 수 있다.
T는 Type Parameter라고 부른다.
T라는 새로운 형식이 생성되는 것이 아니라 실제 함수가 호출될 때 해당 매개변수의 타입으로 대체되는 Placeholder라고 보면 되겠다.
var someInt = 1
var aotherInt = 2
swapTwoValues(&someInt, &aotherInt) ✅ // 함수 호출 시 T는 Int 타입으로 결정됨
var someString = "Hi"
var aotherString = "Bye"
swapTwoValues(&someString, &aotherString) ✅ // 함수 호출 시 T는 String 타입으로 결정됨
위 처럼 실제 함수를 호출하면 T의 타입이 결정된다.
swapTwoValues(&someInt, &aotherString)
❌ // Cannot convert value of type 'String' to expected argument type 'Int'
만약 서로 다른 타입을 전달하면 첫번째 파라메터를 통하여 T가 Int로 결정이 되었는데 두번째 파라메터인 aotherString은 Int가 아니라고 에러가 발생한다.
타입 파라메터는 T말고 원하는 이름으로 해도 된다. 또한, 여러개를 comma(,)를 이용해서 선언할 수 있다.
func swapTwoValues<One, Two> { ... }
하지만 가독성을 위하여 T나 V같은 단일 문자, 혹은 Upper Camel Case를 사용하자
제네릭은 함수 뿐만 아니라 구조체, 클래스, 열거형 타입에도 선언할 수 있는데 이를 제네릭 타입이라고 한다.
struct Stack<T> {
let items: [T] = []
mutating func push(_ item: T) { ... }
mutating func pop() -> T { ... }
}
이렇게 구조체를 제네릭 타입으로 선언한다.
let stack1: Stack<Int> = .init()
let stack2 = Stack<Int>.init()
제너릭 타입의 인스턴스를 생성할 땐 위처럼 <>를 통하여 사용할 타입을 명시해 주어야 한다.
let array1: Array<Int> = .init()
let array2 = Array<Int>.init()
우리가 평소에 보던 Array도 바로 제네릭 타입이다.
제네릭 함수와 타입을 사용할 때 특정 클래스의 하위클래스나, 특정 프로토콜을 준수하는 타입만 받도록 제약할 수 있다.
func isSameValues<T>(_ a: T, _ b: T) -> Bool {
return a == b ❌ // Binary operator '==' cannot be applied to two 'T' operands
}
== 연산자는 a와 b의 타입이 Equatable이란 프로토콜을 준수할 때만 사용 가능하다.
타입파라메터인 T는 a와 b가 Equatable을 준수할수도 아닐수도 있기 때문에 에러가 발생함
func isSameValues<T: Equatable>(_ a: T, _ b: T) -> Bool {
return a == b
}
따라서 위처럼 T: Equatable 이런식으로 제약을 줄 수 있다.
이렇게 하면 함수에 들어오는 파라메터는 Equatable 프로토콜을 준수하는 파라메터만 받을 수 있다.
프로토콜 제약과 똑같지만 해당 자리에 클래스 이름이 오는 것이다.
class Bird { }
class Human { }
class Teacher: Human { }
func printName<T: Human>(_ a: T) { }
위 처럼 T: Human 이런 식으로 써주면
let bird = Bird.init()
let human = Human.init()
let teacher = Teacher.init()
printName(bird) ❌ // Global function 'printName' requires that 'Bird' inherit from 'Human'
printName(human)
printName(teacher)
Human 클래스를 상속받은 Teature클래스는 제네렉 함수를 실행시킬 수 있지만
아무 연관없는 Bird는 실행이 불가능하다.
만약 제네릭 타입인 Array를 확장하고 싶다면?
extension Array {
mutating func pop() -> Element {
return self.removeLast()
}
}
제네릭 타입을 확장하면서 타입 파라메터를 사용할 경우
실제 Array 구현부에서 타입 파라메터가 Element이기 때문에 Element로 사용해야 한다.
만약 확장에서 새로운 제네릭을 선언하거나, 다른 타입 파라메터를 사용하면 에러가 발생한다.

where를 통해 확장 또한 제약이 가능하다.
extension Array where Element: FixedWidthInteger {
mutating func pop() -> Element { return self.removeLast() }
}
FixedWidthInteger라는 프로토콜을 준수해야 한다 라는 제약을 주면
let nums = [1, 2, 3]
let strs = ["a", "b", "c"]
nums.pop() // ✅
strs.pop() // ❌
위와 같은 결과를 얻게 된다.
제네릭은 타입에 상관없이 실행되지만 특정 타입을 사용하는 함수를 구현하고 싶다면 오버로딩을 하면 된다.
func swapValues<T>(_ a: inout T, _ b: inout T) {
print("generic func")
let tempA = a
a = b
b = tempA
}
func swapValues(_ a: inout Int, _ b: inout Int) {
print("specialized func")
let tempA = a
a = b
b = tempA
}
위 처럼 구현할 경우 타입이 지정된 함수가 제네릭 함수보다 우선순위가 높아서
var a = 1
var b = 2
swapValues(&a, &b) // "specialized func"
var c = "Hi"
var d = "There!"
swapValues(&c, &d) // "generic func"
Int타입으로 실행한 함수가 먼저 실행되고 String타입으로 실행한 함수는 나중에 실행된다.