Warning: 이해한 부분을 최대한 남기고 정리하려 남긴 글 입니다. 틀린 부분이 있을 수 있습니다. 이점 유의하고 읽어주시면 감사할 것 같습니다. 그리고 틀린 부분 알려주시면 바로바로 고치도록 하겠습니다.
*애플 공식 문서에 따르면 Generics는 더 유연하고 재사용 가능한 함수와 타입의 코드를 작성하는 것을 가능하게 해준다고 합니다.
Generics은 Swift가 제공하는 가장 강력한 기능 중 하나이고 Swift 공부를 하면서 우리가 자주 접하던 Array 또는 Dictionary 또한 Generics code로 설계되었다고 해요. Int 타입 그리고 String타입과 같이 다양한 타입의 Array를 선언할 수 있는 이유 그리고 모든타입을 딕셔너리에 저장할 수 있는 이유는 언급했던 자료구조가 모두 Generics Code로 설계되었기에 가능한 것이었습니다.
어떠한 타입에 모두 대응할 수 있게 해 준다는 것이 Generics를 사용하는 것의 가장 큰 메리트입니다.
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"
swapTwoInts()
메서드를 활용하여 someInt
와 anotherInt
의 값을 바꿔줄 수 있습니다. 해당 메서드는 Int 타입의 값을 가진 변수만 인자값으로 받을 수 있게 제한 되어 있습니다.
그래서 만약에 String값을 가진 두 변수를 해당 메서드에 넣게 되면 타입이 다르기 때문에 작동이 되지 않습니다. 그렇기 때문에 String 타입에 맞는 메서드를 하나 더 생성해야 합니다.
이렇게 되면 중복되는 메서드를 한 번 더 작성해야 하고 그러면 코드길이도 늘어나기에 그렇게 좋지 않은 방법입니다. 이런 중복을 피하기 위해 Generic Function을 사용하면 됩니다.
Generics Function
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
함수명 뒤에 placeholder를 선언하고 파라미터를 T
로 선언합니다.
여기서 사용된 플레이스홀더는 swift에게 타입을 런타임 단계에서 정의할 것이라고 알려주는 것입니다. 그러면 사용자는 매개변수 정의나 반환타입 또는 함수 자체에 있는 타입 정의 대신에 플레이스홀더 타입을 사용할 수 있습니다.
한 가지 명심해야 할점은 placeholder를 어떠한 타입으로 한번 정의하고 나면 다른 모든 placeholder는 해당 타입으로 간주한다는 점입니다. 따라서 placeholder로 정의된 변수나 상수는 반드시 해당 타입의 인스턴스가 됩니다.
위 함수를 사용하면 다른타입에 필요한 메서드를 추가로 생성하지 않고 해당 메서드를 재사용할 수 있습니다.
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"
var a = 5
var c = "My String 1"
swapGeneric(a: &a, b:&c)
// cannot convert values of type String to expected argument type Int
여기서 함수가 Integer타입을 기대하는 이유는 함수에 전달한 첫 번째 매개변수가 Integer 타입의 인스턴스이기 때문입니다. 이 때문에 placeholder T로 정의된 함수 안의 모든 제네릭 타입은 Integer 타입이 되는 것입니다.
여러 개의 지네릭 타입을 사용할 수도 있습니다.
func testGeneric<T,E>(a:T, b:E) {
// 코드 구현
}
이렇게 하면 placeholder T
, E
에서 각각 다른 타입을 설정 할 수 있습니다. 해당 함수는 서로 다른 타입의 매개변수를 받기 때문에 값을 서로 교환할 수는 없습니다.
func genericEqual<T>(a: T, b: T) -> Bool {
return a == b
}
위 코드는 문제가 없어 보이지만 binary operator '==' cannot be applied to two 'T' operands
라는 오류가 발생하게 된다. 그 이유는 위에서 설명한 것 처럼 T
placeholder는 swift에서 런타임시 타입이 정의가 되고 이 때문에 코드가 컴파일 되는 시점에는 인자의 타입을 모르기 때문에 swift는 타입의 동등 연산자를 사용할 수 있는지를 알지 못하고 오류로 이어지게 됩니다. 스위프트에게 해당 타입이 어떠한 기능을 갖고 있을 것이라는 것을 알려주는 방법은 타입 제약 을 활용하는 것입니다.
타입 제약에서는 generic type은 반드시 구체적인 클래스를 상속하거나 특정 프로토콜을 채택해야 한다고 명시합니다. 타입 제약은 지네릭 타입에서 부모 클래스나 프로토콜에 정의된 메서드나 프로퍼티를 사용할 수 있게 해줍니다. 이를 토대로 위 코드를 수정 해 보자면
func genericEqual<T: Comparable>(a: T, b: T) -> Bool {
return a == b
}
위와 같이 T
placeholder 뒤에 타입 또는 프로토콜 제약을 위치 시키면 오류 없이 메서드를 구현할 수 있습니다.
func testGenericWithLimit<T: CertainClass, E: CertainProtocol>(a: T, b: E) {
}
struct List<T> { var items = [T]() mutating func add(item: T) { items.append(items) } func getItemsAtIndex(index: Int) -> T? { if items.count > index { return items[index] } else { return nil } }}
위와 같이 여러가지 타입으로 인스턴스화 될 수 있는 List 구조체를 만들 수 있다.
위에 구현된 구조체를 다양한 타입을 담는 List로서 활용할 수 있습니다. 아래와 같이 말이죠.
var stringList = List<String>()var intList = List<Int>()stringList.add(item: "I am String")intList.add(item: 1)
프로토콜에서 선언한 지네릭 타입을 연관 타입이라 부릅니다.
연관 타입은 프로토콜 내에서 타입 대신에 사용될 수 있는 placeholder명을 정의합니다.
실제로 사용되는 타입은 프로토콜에 채택되기 전까지는 명시되지 않습니다.
연관타입은 associatedtype
키워드를 사용해 명시합니다.
예를 들면 아래와 같이 연관타입을 사용할 수 있습니다.
protocol ListMakeable { associatedtype E var items: [E] { get set } mutating func add(item: E)}
해당 프로토콜을 따르는 타입은 아래와 같이 생성해 볼 수 있습니다.
struct MyIntList: ListMakeable { var items: Int = [] mutating func add(item: Int) { items.append(item) }}
참조:
지네릭 (Generics) - The Swift Language Guide (한국어) (gitbook.io)