Combine을 공부하는 중에 Combine을 이해하는데에 있어 저도 정확히 한번 더 공부해서 정리하고 가보면 좋을 것 같아서 Generic에 대한 글을 써보려고 합니다.
개념
애플 문서에서 Generic의 정의를 보면 다음과 같습니다.

Generic의 정의를 보면 multiple types에 대해 동작하는 code를 작성하고, 그 types에 대한 특정한 요구사항을 지정한다.
사용하는 이유
Generic은 왜 사용하는 걸까요?
이유는 다음과 같이 애플 문서에 잘 나와있습니다.

눈에 띄는 문구들은 다음과 같네요.
- to write flexible
- reusable functions and types that can work with any type
- avoids duplication
- the most powerful features of Swift
- Swift standard library is built with generic code
- you've been using generics throughout the Language Guide(ex. Array, Dictionary)
문서의 내용은 한번 살펴보시길 추천드리고, 저는 위 문구들 중 사실 2번의 내용을 통해 1번과 3번이 가능하다(= 어떤 타입과도 동작할 수 있는 함수와 타입들의 재사용이 가능하게 해줌으로써 유연하며, 중복을 피하는 코드를 작성하게 해줄 수 있다.)가 핵심이라고 생각했습니다.
이 내용은 문서에 있는 문제 상황에 대한 예시 코드와 Generic을 사용한 코드 예시를 보면 좀 더 명확하게 이해가 됩니다.
문제 상황에 대한 예시 코드
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
이 함수는 a와 b파라미터를 받아서 함수 내부에서 값을 바꾸는(swap) 함수입니다.
이 함수가 현재는 한 개의 타입에서만 사용되고, 앞으로 다른 타입에 대해서 사용할 계획이 없다는 보장이 되어 있다면 사실 문제가 되지 않겠지만 예를들어, String 타입과 Double 타입에 대해서도 사용할 계획이 있다면? 그런데 만약 generic이 없다면?
Swift안에서 방법이야 찾아보면 또 있겠지만(예를들면 프로토콜과 타입 캐스팅) 대응해야하는 타입이 더 많아진다면?
각 각의 타입을 파라미터로 받는 메서드를 다음과 같이 오버로딩해서 만들어야 되겠죠?! 🙉
func swapTwoInts(_ a: inout String, _ b: inout String) {
let temporaryA = a
a = b
b = temporaryA
}
func swapTwoInts(_ a: inout Double, _ b: inout Double) {
let temporaryA = a
a = b
b = temporaryA
}
이런 상황들에 대한 해결 방법 중 하나로 generic이라는 것이 생겨나지 않았을까 싶습니다.
다음으로 Generic을 사용한 예시 코드를 봐볼까요?
Generic을 사용한 예시 코드
func swapTwoData<DataType>(_ a: inout DataType, _ b: inout DataType) {
let temporaryA = a
a = b
b = temporaryA
}
//!!!: - Int Type Data Swap
var num1: Int = 1
var num2: Int = 3
print(num1, num2) //1, 3
swapTwoData(&num1, &num2)
print(num1, num2) //3, 1
//!!!: - String Type Data Swap
var string1: String = "가"
var string2: String = "나"
print(string1, string2) //가, 나
swapTwoData(&string1, &string2)
print(string1, string2) //나, 가
//!!!: - Double Type Data Swap
var double1 = 1.0
var double2 = 10.0
print(double1, double2) //1.0, 10.0
swapTwoData(&double1, &double2)
print(double1, double2) //10.0, 1.0
swap을 하기 위한 메서드는 하나지만 DataType을 받는 파라미터 하나로, Int, String, Double 타입을 파라미터로 받아서 swap이 가능해졌습니다. 위에서 적었던 애플 문서 내용 중 아래의 내용들.. 명확하게 이해가 되지 않으시나요?!
- to write flexible
- reusable functions and types that can work with any type
- avoids duplication
사실, 2번의 내용 중 앞의 내용 + 1,3번의 내용인 것 같고, 2번의 뒷 내용은 애플 문서에 Stack자료구조를 가지고 설명을 하고 있습니다. Stack이라는 Custom Type에 어떻게 Generic을 적용하는가의 내용이 담겨있으니 해당 문서를 보지 않으신 분 중에 Stack에 대해 잘 모르시는 분은 한번 참고해보셔도 좋을 것 같습니다.🙂
그럼 이제 정의에서 나왔었던 'multiple types에 대해 동작하는 코드', 'generic types에 대한 특정한 요구사항을 지정'이라는 것이 무엇인지 알아보겠습니다.
multiple types에 대해 동작하는 코드
multiple types에 대해 동작하는 코드는 앞에서 Generic을 사용하는 이유에 대해 알아보면서 두 가지로 사용 가능함을 확인해 본 것 같습니다.
- Generic을 적용한 메서드
- Generic을 적용한 Custom Type
애플 문서에서는 여기에 더해 protocol에서도 Associatedtype이라는 프로토콜 요구사항을 작성하고, 다음과 같이 해당 protocol을 채택한 타입에서 Generic과 같이 사용할 수 있음을 설명하고 있는데요.
- protocol의 associatetype을 이용한 Custom Type의 Generic화
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
mutating func pop() -> Item
var count { get }
var items { get set }
subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
typealias Item = Int
var items: [Int] = []
mutating func append(_ item: Int) {
self.items.append(item)
}
...
}
이 부분은 Protocol에 대한 부분도 정리 예정이니 해당 글을 정리하면서 그 곳에 작성을 하는게 더 적절할 것 같아서 그 글에서 추가적으로 정리를 해보도록 하겠습니다.
Generic types에 대한 특정한 요구사항을 지정
먼저 Generic types에 대한 특정 요구사항을 지정하기 위해서는 문법이 필요합니다.
문법은 다음과 같습니다.
func genericFunction<T: SomeClass, U: SomeProtocol>(param: T, param2: U) {
//body
}
먼저, 저는 위에서 Generic을 사용한 예시 코드를 DataType이라는 단어로 표시를 했는데, 일반적으로 Generic을 적용할 때는 T 다음 U를 사용하는 형태로 많이 사용합니다.
저는 개인적으로 T나 U도 좋지만, 좀 더 명확한 표현을 선호해서 특별한 룰이 없는 이상 DataType과 같이 알아볼 수 있는 단어로 표현을 하고자 합니다.🙂
애플 문서에 적힌 위와 같은 코드도 T와 U를 사용해서 Generic을 적용했는데, 보면 파라미터의 타입 표시처럼 ':'과 타입과 같은 것(SomeClass, SomeProtocol)을 적어준 것을 확인할 수 있습니다.
문법은 '파라미터가 특정 타입어야 한다'처럼 'generic한 타입이 특정 타입이어야한다.' 또는 'generic한 타입이 특정 프로토콜을 채택한 타입이어야한다.'로 해석을 하면 적절하다고 생각합니다.
다음의 예시를 봐볼까요?
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
//!!!: - Find Specify Int Type Value Index
let intTypeIndex = findIndex(of: 5, in: [1,3,5])
print(intTypeIndex) //Optional(2)
//!!!: - Find Specify Double Type Value Index
let doubleTypeIndex = findIndex(of: 3.15, in: [3.14, 3.15, 3.16])
print(doubleTypeIndex) //Optional(1)
파라미터로 주어지는 array를 순회하며 또 다른 파라미터로 주어진 값과 array의 요소가 일치하면 해당 index를 Optional타입으로 리턴하거나 없다면 nil을 리턴하는 generic한 메서드입니다.
사용 예를 보면 다음과 같습니다.
- 1이라는 값을 [1, 3, 5] 배열에서 찾고 해당 인덱스를 리턴
- 3.15라는 값을 [3.14, 3.15, 3.16] 배열에서 찾고 해당 인덱스를 리턴
이 예시만 보고는 사실 'generic한 타입이 특정 타입이어야한다'와 무슨 관계가 있나 싶을 수도 있는데 Swift에서는 기본 타입들(Int, Double, String 등)이 Equatable이라는 프로토콜을 채택하고 있기 때문에 해당 메서드에서는 컴파일 에러가 없었지만 generic 타입이 특정 타입이거나 특정 프로토콜을 채택했음을 설정하는 Constraint(: Type)을 다음과 같이 없애면
func findIndex<T>(of valueToFind: T, in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
//!!!: - Find Specify Int Type Value Index
let intTypeIndex = findIndex(of: 5, in: [1,3,5])
print(intTypeIndex)
//!!!: - Find Specify Double Type Value Index
let doubleTypeIndex = findIndex(of: 3.15, in: [3.14, 3.15, 3.16])
print(doubleTypeIndex)
다음과 같은 Compile Error를 확인할 수 있습니다.

'generic type에 대한 특정한 요구사항을 지정'한다는 의미가 어떤 의미인지 이해가 되셨을까요? 저는 충분히 이해가 된 것 같습니다.🙂
참고
애플 공식 문서
'Swift' 카테고리의 다른 글
| 배열 요소에 안전하게 접근하는 방법(How to access array's element safely) (0) | 2024.03.15 |
|---|---|
| Protocol(1) (0) | 2023.08.15 |