제네릭(Generic)
-제네릭은 스위프트의 강력한 기능 중 하나로, 제네릭을 이용해서 코드를 짜면
1. 어떤 타입에도 유연하게 대응이 가능,
2. 재사용이 용이,
3. 코드의 중복을 줄여 깔끔한 표현이 가능하다.
-스위프트의 표준 라이브러리는 대부분 제네릭을 사용한 코드이며, 우리는 제네릭을 쭉 사용해왔을 것이다.
예를 들어 Array, Dictionary, Set 등이 모두 제네릭 컬렉션이고, 우리가 Int, String 타입을 요소로 갖는 배열을 만드는 등의 동작들이 모두 제네릭의 기능이다.
제네릭(Generic)의 사용
-제네릭을 사용할 때는 제네릭을 사용하려는 타입 명이나 함수 명 뒤에 <타입 이름>을 적어서 사용한다.
-<타입 이름> 에서 타입 이름을 지어줄 때는 보통 T, V 등과 같은 단일 문자나 대문자 Camel Case를 사용한다.
제네릭을 이용한 함수
먼저 제네릭을 이용하지 않고 상황을 해결하는 함수를 예로 보면
상황: a, b라는 2개의 매개변수(타입은 Int형)를 받아서 서로의 값을 Swap 한다.
func swapTwoInts(_ a: inout Int, _ b: inout Int){
let tempA = a //새로운 상수를 만들어서 a의 값을 저장
a = b //a에 b의 값을 저장
b = tempA //b에 tempA의 값을 저장
}
위와 같이 함수를 구현하면 상황을 잘 해결할 수 있다.
상황 : Int형이 아닌 String형이나 Double형을 바꿔주는 함수를 구현하고 싶다면?
String 변환
func swapTwoStrings(_ a: inout String, _ b: inout String){
let tempA = a
a = b
b = tempA
}
Double 변환
func swapTwoDoubles(_ a: inout Double, _ b: inout Double){
let tempA = a
a = b
b = tempA
}
요렇게 매개변수로 받는 타입을 바꿔주면 쉽게 해결할 수는 있다
하지만 하는 일은 같은데 타입이 다르다고 3개의 함수를 따로 만들어주는 것이 너무 번거롭고, 코드의 양도 많아지는 불편함이 있다.
이 때 제네릭을 사용하면 이 불편함과 번거로움을 모두 해결할 수 있다.
제네릭을 사용한 함수의 예
func swapTwoValues<T>(_ a: inout T, _ b: inout T){
let tempA = a
a = b
b = tempA
}
var intA : Int = 1
var intB : Int = 2
var stringA : String = "a"
var stringB : String = "b"
var doubleA : Double = 1.1
var doubleB : Double = 2.2
swapTwoValues(&intA, &intB)
swapTwoValues(&stringA, &stringB)
swapTwoValues(&doubleA, &doubleB)
위에서 3개의 함수를 만들어서 해결해야 하는 작업들을 단 한개의 함수와 제네릭을 이용해서 해결했다.
이로써 처음에 말한 제네릭의 3가지 효과가 모두 맞아 떨어진다.
1. 어떤 타입에도 유연하게 대응이 가능,
2. 재사용이 용이,
3. 코드의 중복을 줄여 깔끔한 표현이 가능하다.
함수 명 뒤에 <타입이름 : T>를 쓰고, 매개변수의 데이터 타입 명시 자리에도 T를 적어 제네릭을 구현했는데 이것이 제네릭의 규칙? 이라고 할 수 있다.
1. 제네릭은 이렇게 실제 타입 이름을 적어주는 대신 T와 같은 Placeholder(타입이름)를 사용하고,
2. a, b 매개변수 모두 같은 T 로 지정했으니, 같은 타입이어야 한다.
그리고 T의 실제 타입은 함수가 호출되는 순간에 결정이 된다.(인자로 Int형이 오면, T는 Int형)
즉, 호출될 때 마다 인자로 들어온 타입으로 작동이 된다.
만약 T라는 하나의 타입말고 여러 개의 타입 파라미터를 사용하고 싶다면, <T, U, V> 같이 타입 파라미터를 쉼표로 분리해서 지정하면 된다.
제네릭 타입
-제네릭은 함수뿐만 아니라 제네릭 타입도 구현할 수 있다.
-사용자 정의 타입인 클래스, 구조체, 열거형에도 제네릭 타입을 사용하면 모든 타입과 연관되어 동작할 수 있다.
-맨 위에서 제네릭 개념을 설명할 때 예시로 Dictionary와 Array가 모두 제네릭 컬렉션이라고 언급했다.
Dictionary와 Array의 구성을 보면
1. Dictionary<Key, Value>
2. Array<Element>
이렇게 <>안에 Key, Value, Element라는 타입이름을 적었다.
제네릭을 사용하려면 <>안에 타입이름을 적어서 사용한다고 했는데, 2번 배열 예시를 보면 <>안에 Element라는 타입이름을 적었다. 이것으로 배열이 제네릭 타입으로 구성되어 있다는 것을 알 수 있다.
우리가 배열을 사용할 때 요소로 int형이나 String형 등 어떤 타입이든 가져올 수 있는 이유가 제네릭 덕분인 것이다.
타입 제약
-Dictionary도 배열과 마찬가지로 제네릭으로 구성되어있다.
하지만 제네릭은 분명 모든 타입에서 작동이 가능하다고 했는데 Dictionary의 Key 에는 Hashable 프로토콜을 준수하는 타입만이 올 수 있다. 이것이 바로 타입 제약이다.
ex)
만약 Dictionary의 key 타입에 Hashable 프로토콜을 준수하지 않는 타입을 넣었을 때 다음과 같은 오류가 발생한다.
" Type 'Any' does not conform to protocol 'Hashable' "
let dictionary : Dictionary<Any,String> //에러발생! : Type 'Any' does not conform to protocol 'Hashable'
-타입 제약은 타입 매개변수가 가져야 하는 제약사항을 지정하는 방법으로 예를 들어 제네릭 함수의 기능이 특정 타입에 한해서 처리되어야 하거나, 특정 프로토콜을 따르는 타입만 사용할 수 있도록 하는 등의 상황에서 타입 제약을 사용한다.
-타입제약의 사용 방법
func swapTwoValues<T : Hashable>(_ a: inout T, _ b: inout T){
let tempA = a
a = b
b = tempA
}
타입 매개변수 옆에 제약을 걸고 싶은 프로토콜이나, 클래스를 명시해주면 된다.
타입 제약은 클래스나 프로토콜 타입만 가능하고, 구조체나 열거형은 타입 제약의 타입으로 사용이 불 가능하다.
타입 제약을 여러 개 걸고 싶다면 Where 절을 이용한다.
func swapTwoValues<T : BinaryInteger>(_ a: inout T, _ b: inout T) Where T : FloatingPoint{
let tempA = a
a = b
b = tempA
}
제네릭 타입 확장
-제네릭 타입에 extension을 통해 기능을 추가하고 싶을 때는
1. extension을 통한 정의에서 타입 매개변수를 명시하지 않는다.
extension Array<T> {
//에러발생! 정의에서는 타입 매개변수를 입력하지 않는다.
}
2. 실제 제네릭 타입 구현부에서 명시한 타입 매개변수는 extension에서 사용이 가능하다.
extension Array {
func append(newElement : Element) { //예시로 만든 것 입니다.
self.append(newElement)
}
}
Tip)
1. Dictionary를 보면 타입 이름과 주체와의 특별한 관계를 표현해주고 싶어서 Key, Value 등으로 의미있는 타입이름을 명시해주었다. (딱히 특별한 관계를 표현할 필요가 없는 경우에는 보통 <T>, <U> 등으로 표기한다.)
2. 제네릭 함수의 예로 매개변수 두개의 값을 서로 Swap해주는 함수를 만들었는데, 이미 swift에서는 swap이라는 함수를 지원해준다.
참고:
야곰님의 스위프트 프로그래밍 3판
'Swift' 카테고리의 다른 글
서브스크립트에 관하여 (0) | 2023.01.28 |
---|---|
오버라이딩(Overriding)에 관하여 (0) | 2023.01.26 |
Protocol에 관하여 (0) | 2022.11.14 |
lazy variables에 관하여 (0) | 2022.10.07 |
순환참조에 관하여 (0) | 2022.09.28 |