안녕하세요, iOS 개발하는 루피입니다!
오늘은 Swift 공식문서를 바탕으로 제네릭에 대해 공부해 보겠습니다.
바로 시작합니다.
제네릭이란?
제네릭은 쉽게 말해 모든 타입에 동작할 수 있는 유연하고 재사용 가능한 함수와 타입을 작성할 수 있도록 돕는 Swift의 강력한 도구입니다. Swift 표준 라이브러리 대부분은 제네릭 코드로 되어 있다고 합니다.
예를 들어, 우리가 자주 사용하는 Array, Dictionary, Set 등의 타입은 모두 제네릭 컬렉션입니다. 우리가 Int 나 String 타입의 요소를 갖는 배열을 갖는다고 해서 그에 맞게 컬렉션을 새롭게 만드는 게 아니라 우리는 타입만 정해주면 되지 않나요??
이게 제네릭이 가져다 주는 강력함입니다!
코드를 통해 보겠습니다.
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Prints "someInt is now 107, and anotherInt is now 3"
코드를 보고 바로 함수의 기능이 이해 가시나요??... 네 우리가 흔히 볼 수 있는 swap 함수를 따라 만들어 본 코드입니다.
하지만 이 코드에는 아쉬운 점이 있습니다. 바로 Int 타입 값만 서로 바꿀 수 있다는 것입니다.
만약에 제가 Int 가 아닌 Double이나 String 등 다른 타입의 변수에 적용하고 싶다면 Double, String에서 이용 가능한 함수를 더 만들어야 할 겁니다.. 그렇다면 우리는 어떻게 이러한 상황을 좀 더 유연하게 대처할 수 있을까요?
이때 우리는 제네릭을 이용해 모든 타입에 대응 가능한 함수로 변경할 수 있습니다.
제네릭 함수
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3
var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"
이전 코드를 제네릭 함수로 변경해 봤습니다. 어떤가요?? 상당히 간단하지 않나요???
우리는 단지 함수 뒤에 <T>를 추가해 주고 이에 맞게 파라미터 값들의 타입을 T로 만들어주면 됩니다.
이때, T는 임의의 타입이므로 Swift는 T라는 실제 타입을 찾지 않습니다.
T를 우리는 타입 파라미터라고 합니다. 타입 파라미터의 경우 관계가 있을 때는 설명이 포함된 이름으로 사용하지만, 관계가 없을 경우는 T, U, V와 같은 단일 문자로 사용하여 이름을 지정하는 것이 일반적입니다!
// 관계가 있는 경우
Dictionary<Key,Value>
Array<Element>
// 관계가 없는 경우
Swap<T,U,V>
타입 제약
물론 제네릭 함수는 모든 타입에 대응할 수 있도록 유연함을 목적으로 하지만, 특정 타입만 사용할 수 있도록 제약을 줄 수 있습니다. 그리고 이때 타입 제약은 클래스 타입 또는 프로토콜로만 줄 수 있습니다.
즉, 열거형, 구조체 등의 타입은 타입 제약의 타입으로 사용할 수 없습니다.
1. 클래스 제약
func someFunction<T: SomeClass>(someT: T) {
// function body goes here
}
다음과 같이 타입 파라미터의 이름 뒤에 단일 클래스 또는 프로토콜 제약을 위치시킨다면 제네릭 함수에서 타입 제약에 대한 구문을 작성할 수 있습니다. 이렇게 작성하게 된다면 T는 SomeClass의 하위 구문이어야 합니다.
2. 프로토콜 제약
func isSame<T>(_a: T,_b: T) -> Bool {
return a == b
}
위 함수는 제네릭 타입으로 선언되어 있으며, 매개변수로 a와 b가 존재합니다. 하지만, "==" 연산자를 사용할 경우 에러가 발생하게 됩니다. 왜 그럴까요???
바로 "=="는 Equatable 프로토콜을 준수할 경우만 사용될 수 있기 때문입니다. 그러면 의문이 생길 수 있는데요..? 그러면 다른 타입들도 모두 Eqautable을 다 적어줘야 하는 거 아닌가요?? 난 그동안 안 적었는데?
네 맞습니다. 하지만, Swift 표준 라이브러리 내 기본 데이터 타입은 이미 Equatable을 따르고 있기 때문에 우리가 따로 제약을 주지 않아도 괜찮았던 것입니다!!
func isSame<T:Equatable>(_a: T,_b: T) -> Bool {
return a == b
}
따라서 이렇게 Equatable을 채택해 준다면.... 우리는 값을 비교하는 함수를 만들 수 있습니다.
프로토콜의 연관 타입 (Associated Type)
프로토콜을 정의할 때 연관 타입(Associated Type)을 함께 정의하면 유용할 때가 많은데요, 연관 타입은 프로토콜에서 사용할 수 있는 타입 매개변수라고 생각하시면 쉬울 거 같습니다.
제네릭에서는 어떤 타입이 들어올지 모를 때, 타입 매개변수를 통해 "종류는 알 수 없지만, 어떤 타입이 여기에 쓰일 것이다" 라고 표현해 주었다면, 연관 타입은 타입 매개변수의 그 역할을 프로토콜에서 수행할 수 있도록 만들어진 기능입니다!
코드를 통해 보겠습니다.
protocol Container {
associatedtype ItemType // 연관 타입 선언
var count: Int { get } // 아이템 개수를 반환하는 프로퍼티
mutating func append(_ item: ItemType) // 아이템 추가 메서드
subscript(i: Int) -> ItemType { get } // 인덱스로 아이템 접근
}
class StringContainer: Container {
// 연관 타입을 String으로 지정
typealias ItemType = String
private var items: [String] = [] // 내부 저장소
var count: Int {
return items.count
}
func append(_ item: String) {
items.append(item)
}
subscript(i: Int) -> String {
return items[i]
}
}
StringContainer는 Container 프로토콜을 준수하기 위해서 필요한 것을 모두 갖추었습니다.
연관 타입인 Itemtype 대신에 실제 타입인 String 타입으로 구현해주었고, 이는 프로토콜의 요구사항을 모두 충족하므로 큰 문제가 없습니다.
왜냐하면, 프로토콜에서 ItemType이라는 연간 타입만 정의했을 뿐, 특정 타입을 지정하지 않았기 때문입니다. 실제 프로토콜 정의를 준수하기 위해 구현할 때는 ItemType을 하나의 타입으로 일관성 있게 구현하면 됩니다.
이러한 특징으로 우리는 프로토콜을 정의할 때 특정 타입에 의존하지 않고 추상적인 타입으로 설계할 수 있게 되고 이는 곳 유연한 코드 작성으로 이어지게 됩니다!!
오늘도 화이팅입니다!
'iOS > Swift' 카테고리의 다른 글
| [Swift] 프로토콜 (Protocol) (0) | 2025.02.06 |
|---|---|
| [Swift] Where절 (0) | 2025.02.05 |
| [Swift] ARC (0) | 2025.01.28 |
| [Swift] Delegate 패턴을 구현해 사용해 보자 (0) | 2025.01.17 |
| [Swift] Delegate를 사용해 객체의 동작을 커스텀하기 (0) | 2025.01.16 |