제네릭은 Swift에서 가장 강력한 기능 중 하나이다.
실제로 Swift 표준 라이브러리 또한 수많은 제네릭으로 구성되어 있는데, 우리가 흔하게 사용하는 배열과 딕셔너리도 제네릭 타입이다.
제네릭이 무엇일까?
제네릭이란 모든 타입에서 동장할 수 있는 더 유연하고 재사용 가능한 함수와 타입을 작성할 수 있도록 하게 해주는 기능이다.
즉, 제네릭을 사용하면 중복을 피하고 명확하고 추상적인 방법으로 그 의도를 표현할 수 있는 코드를 작성할 수 있고 코드를 유연하게 작성할 수 있다.
제네릭 문법
제네릭을 사용하기 위해서는 <>를 사용하면 된다.
제네릭 함수
스위프트 공식문서에 있는 문제를 예를들면
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
print("변경 전 : \(a) \(b)")
let tempA = a
a = b
b = tempA
print("변경 후 : \(a) \(b)")
}
위와 같이 두 정수 값을 바꾸는 swapTwoInts 함수가 있다.
var intA: Int = 10
var intB: Int = 20
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
print("변경 전 : \(a) \(b)")
let tempA = a
a = b
b = tempA
print("변경 후 : \(a) \(b)")
}
swapTwoInts(&intA, &intB)
이 함수의 경우 파라미터 모두 정수형일 경우엔 잘 작동하지만 파라미터 타입이 Double이나 String형이면 사용할 수 없다.
따라서 Double, String 타입에 대해서 swapTwoInt 함수를 사용하고 싶다면
func swapTwoInts(_ a: inout Double, _ b: inout Double) {
print("변경 전 : \(a) \(b)")
let tempA = a
a = b
b = tempA
print("변경 후 : \(a) \(b)")
}
func swapTwoInts(_ a: inout String, _ b: inout String) {
print("변경 전 : \(a) \(b)")
let tempA = a
a = b
b = tempA
print("변경 후 : \(a) \(b)")
}
이런식으로 귀찮게 일일이 함수를 오버로딩해서 만들어야한다.
이럴 때 사용하는게 제네릭이다.
func swapTwoValues<T> (_ a: inout T, _ b: inout T) {
let tempA = a
a = b
b = tempA
}
위에서 설명한대로 <>를 사용하면 된다. 여기서 T를 Type Parameter라고 하는데,
T라는 새로운 형식이 생성되는 것이 아니라, 실제 함수가 호출될 때 해당 매개변수의 타입으로 대체되는 Placeholder이다.
✅ Placeholder인 T는 타입이 어떤 타입이어야 하는지 명시하지 않는다. 다만 두 인자의 타입이 같다는 것을 알려준다.
따라서 이렇게 swapTwoValues라는 함수를 제네릭으로 선언 해주면,
함수를 귀찮게 오버로딩 하지 않아도 한 개의 함수로 다른 타입을 받아올 수 있다.
✅ 여기서 주의해야할 것이 있는데, 서로 다른 타입을 파라미터로 전달 받으면 오류가 발생한다.
제네릭 타입
제네릭은 함수뿐만 아니라 구조체, 클래스, 열거형 타입에도 선언할 수 있다. 이것을 제네릭 타입이라고 한다.
struct Stack<T> {
var items = [T]()
mutating func push(_ item: T) {
items.append(item)
}
mutating func pop() -> T {
return items.removeLast()
}
}
Stack을 제네릭으로 만들고 싶다면 위와 같이 <>를 이용해서 제네릭으로 만들 수 있다.
var intStack: Stack<Int> = Stack<Int>()
제네릭 타입을 가지고 있는 Stack의 인스턴스를 생성할 땐 선언과 마찬가지로 <>를 통해 어떤 타입으로 사용할 것인지 명시해주면 된다.
타입 제한
제네릭을 이용할 때 특정 클래스를 상속하거나 특정 프로토콜을 따르거나 합성하도록 명시할 수 있다.
위에서 구현한 swapTwoValues 함수는 제네릭으로 구현되어 있기 때문에 모든 타입에서 동작한다.
하지만, 때로는 제네릭 함수가 처리해야 할 기능이 특정 타입에만 한정되어야만 처리할 수 있다던가,
제네릭 타입을 특정 프로토콜을 따르는 타입만 사용할 수 있도록 제약을 두어야할 경우가 있을 것이다.
이때 사용하는 것이 타입 제한이다.
func isSameValues<T>(_ a: T, _ b: T) -> Bool {
return a == b
}
파라미터로 두 개의 값을 받아서 두 값이 같으면 true, 다르면 false를 반환하는 함수를 제네릭으로 선언하려고 한다.
위와 같이 선언하면 에러가 발생한다. 왜냐하면 == 연산자는 a와 b의 타입이 Equatable이라는 프로토콜을 준수할 때만 사용할 수 있다.
func isSameValues<T: Equatable>(_ a: T, _ b: T) -> Bool {
return a == b
}
따라서 이렇게 타입 파라미터에 T: Equatable 처럼 제약을 줄 수 있다.
class Food { }
class Animal { }
class Dog: Animal { }
func printName<T: Animal>(_ a: T) { }
let food = Food.init()
let animal = Animal.init()
let dog = Dog.init()
printName(food) // err
printName(animal)
printName(dog)
위와 같이 T: Animal으로 제약을 주면 Animal 클래스 인스턴스인 animal과, Animal 클래스를 상속 받은 Dog는 printName이라는 제네릭 함수를 실행할 수 있지만, Animal 클래스의 서브 클래스가 아닌 food는 실행할 수 없다.
✅ 타입 제약에 자주 사용하는 프로토콜
Hashable, Equatable, Comparable, Indexable, IteratorProtocol, Error, Collection, CustomStringConvertible 등
제네릭 확장
제네릭 타입을 확장하기 위해선 extension을 사용하면 된다.
extension Stack {
var topElement: T? {
return self.items.last
}
}
위에서 구현한 Stack 구조체를 확장한 것이다. Stack은 제네릭 타입의 구조체이지만 extension의 정의에서는 따로 타입 파라미터 <T>를 명시해주지 않았다. 그 대신 기존의 제네릭 타입에 정의되어있는 T라는 타입을 사용할 수 있다.
where
where을 통해 확장 또는 제약을 줄 수 있다.
@frozen struct Dictionary<Key, Value> where Key : Hashable
이렇게, where을 통해 타입 파라미터 Key가 Hashable이라는 프로토콜을 준수해야 한다. 라는 제약을 줄 수 있다.
제네릭 함수와 오버로딩
제네릭은 타입에 관계없이 동일하게 실행 되지만, 만약 특정 타입일 경우 제네릭이 아닌 다른 함수로 구현하고 싶다면
제네릭 함수를 오버로딩 하면 된다.
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
print("변경 전 : \(a) \(b)")
let tempA = a
a = b
b = tempA
print("변경 후 : \(a) \(b)")
}
func swapTwoValues(_ a: inout Int, _ b: inout Int) {
print("오버로딩 함수")
print("변경 전 : \(a) \(b)")
let tempA = a
a = b
b = tempA
print("변경 후 : \(a) \(b)")
}
이렇게 오버로딩 할 경우 타입이 지정된 함수가 제네릭 함수보다 우선순위가 높기 때문에 Int 타입으로 swapTwoValues 함수를 실행할 경우 타입이 지정 된 함수가 실행되고, 다른 타입으로 swapTwoValues 함수를 실행할 경우 제네릭 함수가 실행 된다.
제네릭 사용 예시
1. Optional
enum Optional<Wrapped> {
case none
case some(Wrapped)
}
2. 배열, 딕셔너리
@frozen struct Array<Element>
@frozen struct Dictionary<Key, Value> where Key : Hashable
'Swift > Swift 기본기' 카테고리의 다른 글
12. 생성자 (Initialization) - 2 (0) | 2023.08.01 |
---|---|
11. 생성자 (Initialization) - 1 (0) | 2023.08.01 |
09. 프로퍼티 (Properties) (0) | 2023.03.18 |
08. 클래스과 구조체 (Classes and Structures) (0) | 2023.03.18 |
07. 열거형 (Enumerations) (0) | 2023.03.18 |