제네릭(Generic)은 스위프트의 강력한 기능 중 하나이다.
제네릭을 이용해 코드를 구현하면 어떤 타입에도 유연하게 대응할 수 있습니다.
또한, 제네릭으로 구현한 기능과 타입은 재사용하기도 쉽고, 코드의 중복을 줄일 수 있기에 깔끔하고 추상적인 표현이 가능합니다.
Swift에서 흔히 사용하는 Array, Dictionary, Set 등의 타입도 모두 제네릭 컬렉션 입니다.
제네릭을 사용하고자 할 때는 제네릭이 피요한 타입 또는 메서드의 이름 뒤의 기호 <> 사이에 제네릭을 위한 타입 매개변수를 써주어 제네릭을 사용할 것임을 표현한다.
제네릭을 사용하고 하는 타입 이름 <타입 매개변수>
제네릭을 사용하고자 하는 함수 이름 <타입 매개변수> { 함수의 매개변수 )
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
var num1 = 5
var num2 = 10
swapTwoInts(&num1, &num2)
print("\(num1), \(num2)")
//10, 5
두 개의 Int 를 받아서 값을 서로 바꿔주는 간단한 함수를 만들었다.
swapTwoInts함수의 Int타입인 num1과 num2를 전달한다면 위의 코드는 에러 없이
num1과 num2의 값이 바뀐것을 확인할 수 있습니다.
하지만 Double 타입을 전달한다면?
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
var num1 = 5.1
var num2 = 10.1
// Err 발생
swapTwoInts(&num1, &num2)
swapTwoInts 함수의 파라미터는 Int 타입이기 때문에,
위와 같이 Double타입을 넘겨주게 된다면 에러가 발생하게 된다.
원하는 타입이 Int가 아닌 String이나 Double일 경수 swapTwoInts 함수의 개수가 늘어날 것입니다.
하지만,
제네릭을 사용하게 된다면 함수를 따로 만들지 않아도 함수를 재사용 할 수 있습니다.
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var num1 = 5
var num2 = 10
var double1 = 5.1
var double2 = 10.1
var str1 = "hello"
var str2 = "world"
swapTwoValues(&num1, &num2)
print("\(num1), \(num2)")
swapTwoValues(&double1, &double2)
print("\(double1), \(double2)")
swapTwoValues(&str1, &str2)
print("\(str1), \(str2)")
위와 같이 제네릭을 사용한 코드를 사용한다면, Int, Double, String 타입을 받는 함수를 추가하지 않아도
하나의 함수로 원하는 결과를 얻을 수 있습니다.
위 코드에서 < T > 라고 적혀 있는 부분이 바로 제네릭인데
T 를 Placeholder 타입 '이름'인 타입 파라미터 라고 부른다.
이 부분이 제네릭 함수와 일반 함수의 가장 큰 차이점이라고 볼 수 있는데, 제네릭 함수는 함수 이름 옆에 < T > Placeholder 타입 이름이 온다.
func swapTwoValues<SeoPRO>(_ a: inout SeoPRO, _ b: inout SeoPRO) {
let temp = a
a = b
b = temp
}
T 부분에 다른 어떠한 타입이름을 적어도 파라미터에 정확히 같은 타입 이름만 정의해준다면 문제가 생기지 않는다.
그리고 이 T의 실제 타입은 함수가 호출되는 그 순간에 결정된다.
num1과 같이 Int타입이 전달 된다면 T는 Int 타입이 되는 것이고, str1과 같이 String타입이 전달 된다면 T String 타입이 되는것이다.
그래서 T가 Placeholder라고 불린다고 한다.
제네릭 함수에 이어 제네릭 타입을 구현할 수도 있다.
제네릭 타입을 구현하면 사용자 정의 타입인 구조체, 클래스, 열거형 등이 어떤 타입과도 연관되어 동작할 수 있다.
struct IntStack {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() {
items.removeLast()
}
}
var intStack = IntStack()
intStack.push(1)
intStack.push(2)
print(intStack.items)
intStack.pop()
intStack.pop()
print(intStack.items)
위 코드의 IntStack 타입은 Int 타입을 요소로 가질 수 있는 간단한 스택 기능을 구현한 구조체 타입입니다.
items라는 이름의 Int 타입 배열을 가짐으로써, Int 타입의 요소들을 pop()하고 push()할 수있는 스택의 기능을 구현했다.
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() {
items.removeLast()
}
}
var stack = Stack<Double>()
stack.push(double1)
stack.push(double2)
print(stack.items)
stack.pop()
stack.pop()
print(stack.items)
위 코드에서 Stack 이라는 구조체는 Int라는 타입 대신 Element라는 타입 매개변수를 사용했습니다.
Stack의 인스턴스를 생성할 때 Stack< Double >() 과 같이 생성하고 싶은 Type을 생성하여 사용할 수있습니다.
이를 통해 제네릭 함수와 같이 제네릭 타입을 만들어 코드의 재사용을 높일 수 있습니다.
제네릭 타입은 익스텐션을 사용하여 확장할 수도 있습니다.
익스텐션을 사용하여 타입을 확장하고 싶을 때에는 익스텐션 정의에 타입 매개변수를 명시하지 않아야 합니다.
extension Stack {
var topElement: Element? {
return self.items.last
}
}
print(stack.topElement)
위 처럼 익스텐션의 정의에는 따로 타입 매개변수를 정의하지 않고, 기존의 제니릭 타입에 정의 되어있는 Element라는 타입을 사용할 수 있습니다.