타입에 의존하지 않는 범용 코드 작성시 사용.
중복을 피하고 유연한 코드 작성이 가능.
Swift는 강타입 언어로 변수 및 파라미터에 대한 타입을 지정해야 한다. 하지만 제네릭을 사용하면 하나의 함수로도 원하는 타입을 지정해 사용할 수 있다.
Swift의 표준 라이브러리 대다수는 제네릭으로 선언되어 있다. 흔히 사용하는 Array, Dictionary 역시 제네릭 타입이다.
struct GenericsIntro: View {
@State private var useInt = false
@State private var ageText = ""
// T : 타입 플레이스홀더.
func getAgeText<T>(value1: T) -> String {
return String("Age is \(value1)")
}
var body: some View {
VStack(spacing: 20) {
Group {
Toggle("Use Int", isOn: $useInt)
// 파라미터 타입이 제네릭이므로 모든 타입 전달가능
Button {
if useInt {
ageText = getAgeText(value1: 28)
} else {
ageText = getAgeText(value1: "30")
}
} label: {
Text("Show Age")
}
Text(ageText)
}
.padding(.horizontal)
}
.font(.title)
}
}
위 코드의 getAgeText옆에 T가 보인다.
이는 제네릭이 사용되고 있으며, 즉 원하는 모든 타입으로 대체가 가능함을 나타낸다.
조금 더 살펴보면 getAgeText라는 하나의 함수에 Int와 String이 각각 전달된 것을 볼 수 있다. 일반적인 함수였다면 불가능한 부분이다.
이렇게 인풋타입이 다르고 구현 내용은 동일할 때, 코드를 반복할 필요 없이 한 번의 구현만으로 모든 타입을 지정할 수 있는 것이 제네릭이다.
func getAgeText<T>(array: [T]) {
...
}
class Animal<T> {
var cat: T
var dog: T
var bird: T
}
struct GenericsObjects: View {
class MyGenericClass<T> {
var myProperty: T
init(myProperty: T) {
self.myProperty = myProperty
}
}
var body: some View {
// 다른 타입을 사용해 클래스를 초기화할 수 있음.
let myGenericWithString = MyGenericClass(myProperty: "MARK")
let myGenericWithBool = MyGenericClass(myProperty: true)
VStack(spacing: 20) {
Text(myGenericWithString.myProperty)
Text(myGenericWithBool.myProperty.description)
}
.font(.title)
}
}
extension GetAnimal {
func getAnimal() -> T { ... }
}
// 주의할 점 : 확장에서는 GetAnimal<T> 이런 식으로 타입 파라미터를 명시하지 않는다!
struct MultipleGenerics: View {
class MyGenericClass<T, U> {
var property1: T
var property2: U
init(property1: T, property2: U) {
self.property1 = property1
self.property2 = property2
}
}
var body: some View {
let myGenericWithString = MyGenericClass(property1: "Yang", property2: "JeongWon")
let myGenericWithIntAndBool = MyGenericClass(property1: 100, property2: true)
VStack(spacing: 20) {
Text("\(myGenericWithString.property1) \(myGenericWithString.property2)")
Text("\(myGenericWithIntAndBool.property1) \(myGenericWithIntAndBool.property2.description)")
}
.font(.title)
}
}
T는 타입 플레이스 홀더이다.
실제 Int나 String등의 타입을 사용하는 대신에, '타입이 입력되어야 한다'는 것을 제시하는 역할이다.
T의 자리에는 대문자로 시작하는 A, B, Element등 다른 문자 사용도 가능하다. 일반적으로는 T를 사용한다.
제네릭에서는 타입 제약이 가능하다.
모든 타입이 다 가능하지 않도록 제약을 걸 수 있다.
struct GenericsConstraints: View {
private var age1 = 25
private var age2 = 45
func getOldest<T: SignedInteger>(age1: T, age2: T)
-> String {
if age1 > age2 {
return "The first is older"
} else if age1 == age2 {
return "The ages are equal"
}
return "The second is older"
}
var body: some View {
VStack(spacing: 20) {
HStack(spacing: 40) {
Text("Age One: \(age1)")
Text("Age Two: \(age2)")
}
Text(getOldest(age1: age1, age2: age2))
}
.font(.title)
}
}
위의 getOldest함수를 보면 SignedInteger타입을 제약조건으로 설정했다.
이렇게 파라미터 타입을 지정하는 것과 동일한 방식으로 제약조건 설정이 가능하다.
이 뿐만 아니라 제네릭 선언을 추가할 수 있는 모든 곳에서 제약조건을 사용할 수 있다.
💡 SignedInteger : Int, Int8, Int16, Int64에서 채택한 프로토콜로, T는 이러한 유형 중 하나일 수 있음