[Swift] Generic
Generic
제네릭
- 제네릭이 없다면 단순히 타입만 다르고 구현 내용이 같은 함수(클래스, 구조체, 열거형)마다 모든 경우를 다 정의해야하기 때문에 개발자의 할 일이 늘어남
- 제네릭을 통해 타입(형식)에 관계없이 한번의 구현으로 모든 타입을 처리하여 유연함(유지보수 쉽고, 재사용성 높은) 함수 / 구조체 / 클래스 / 열거형 등을 일반화 가능한 코드로 작성 가능
// Double을 스왑하는 함수의 정의
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를 사용하지만 다른 이름을 사용하는 것도 문제가 없음, 형식이름이기 때문에 UpperCamelcase로 선언
- 2개이상을 선언하는 것도 가능
(ex. <T, U>, <A, B>) - 제네릭은 타입에 관계없이, 하나의 정의(구현)로 모든 타입(자료형)을 처리할 수 있는 문법
- 일반 함수와 비교해보면, 작성해야하는 코드의 양이 비약적으로 감소
- 타입 파라미터는 실제 자료형으로 대체되는 플레이스 홀더 같은것
(새로운 형식이 생성되는 것이 아님)- 코드가 실행될때 문맥에 따라서 실제 형식으로 대체되는 “플레이스 홀더”일뿐
- 관습적으로 Type의 의미인 대문자 T를 사용하지만, 다른 문자를 사용해도 됨
제네릭 문법
함수
func printArray<T>(array:[T]) {
for number in array {
print(element)
}
}
타입 파라미터의 지정
- 함수의 이름 마지막에 꺽쇠 괄호(
<T>
)를 쓰고, 안에 파라미터 작성- 타입 파라미터는 대문자로 시작하면 상관 없음
- 함수 내부에서 파라미터 형식이나 리턴형, 내부 변수 타입으로 사용
- 실제 타입 대신에 사용하는 플레이스 홀더 역할
(어떤 타입이 입력되어야 한다는 것을 제시)
타입 파라미터의 사용
- 본래 타입의 사용하는 위치(파라미터, 바디, 리턴형)에서 타입이 필요한 곳에 타입 파라미터 사용
- 실제 함수 호출시에 실제 타입으로 치환
클래스, 구조체, 열거형
// 클래스
class SomeClass<T> {
var x:T
var y:T
init(x: T, y: T){
self.x = x
self.y = y
}
}
// 구조체
struct SomeStruct<T> {
var members: [T] = []
}
// 열거형
enum SomeEnumeration<T> {
case x
case y
case z(T)
}
타입 파라미터의 지정
- 타입 이름 마지막에 꺽쇠 괄호(
<T>
)를 쓰고 안에 타입 파라미터 작성- 타입 파라미터는 대문자로 시작하면 상관 없음
- 타입 내부 속성의 타입, 메서드의 파라미터, 리턴형으로 사용
- 실제 타입 대신에 사용하는 플레이스 홀더 역할
(어떤 타입이 입력되어야 한다는 것을 제시)
타입 파라미터의 사용
- 본래 타입의 사용하는 위치(속성, 메서드)에서 타입이 필요한 곳에 타입 파라미터 사용
- 실제 함수 호출시에 실제 타입으로 치환
열거형에서 제네릭 사용 시
- 열거형에서 연관값을 가질때만 제네릭으로 정의가능
(어짜피 케이스는 자체가 선택항목 중에 하나일뿐(특별타입)이고, 그것을 타입으로 정의할 일은 없음)
확장
extension SomeColor {
func getColor() -> T {
// ...
}
}
// where절을 통한 타입에 대한 제네릭 제약
// Int 타입에만 적용되는 확장과 getIntArray() 메서드
extension Coordinates where T == Int {
// 튜플로 리턴하는 메서드
func getIntArray() -> [T] {
return [x, y]
}
}
제네릭 타입 확장 시
- 타입 파라미터 명사없이 확장 (
SomeColor<T>
-> X) - 본체의 제네릭에서 정의한 타입 파라미터 사용 가능
제네릭의 타입 제약
제네릭에서 모든 타입이 다 가능하지는 않도록 타입을 제약 가능
func doSomething<T:제약조건>(a:T) {
// ...
}
프로토콜 제약 (<T: Equatable>
)
- 특정 프로토콜을 따르는 타입만 가능하도록 제약
클래스 타입 제약 (<T: SomeClass>
)
- 특정 클래스와 상속관계 내에 속하는 클래스 타입만 가능하도록 제약
프로토콜에서 제네릭 문법의 사용
프로토콜에서는 연관 타입이라는 것을 사용해 제네릭과 동일한 타입 파라미터를 지정
- 연관 타입 (Assiciated Types)으로 선언
- 프로토콜은 타입들이 채택할 수 있는 한차원 높은 단계에서 요구사항만을 선언하는 개념이기 때문에 제네릭 타입과 조금 다른 개념(연관 타입)을 추가적으로 도입한 것 뿐
protocol RemoteControl { // <T>의 방식이 아님
associatedtype Element
// 연관형식은 대문자로 시작해야함 (UpperCamelcase)
// 관습적으로 Element를 많이 사용
func changeChannel(to: Element)
func alert() -> Element?
}
// 연관형식이 선언된 프로토콜을 채용한 타입은, typealias로 실제 형식을 표시해야함 (생략도 가능)
struct TV: RemoteControl {
typealias Element = Int // 생략 가능
func changeChannel(to: Int) {
print("TV 채널바꿈: \(to)")
}
func alert() -> Int? {
return 1
}
}
class Aircon: RemoteControl {
// typealias를 생략해도 연관 형식이 추론됨
func changeChannel(to: String) {
print("Aircon 온도바꿈: \(to)")
}
func alert() -> String? {
return "1"
}
}
// 프로토콜에서 타입을 제약하는 경우
protocol RemoteControl2 {
associatedtype Element: Equatable
// <T: Equatable> 방식으로 제약조건 추가
func changeChannel(to: Element)
func alert() -> Element?
}