https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/
애플에서 제공하는 공식문서를 따라가면서 제네릭에 대해 알아보도록 할께요.
여러 유형에 작동하는 코드를 작성하고 해당 유형에 대한 요구사항을 지정함
struct Heap<T: Comparable> {
private var elements: [T] = []
...
/// 힙을 구성하는 코드에서나
}
provider = MoyaProvider<GitHub>()
...
// 네트워크를 구성하는 코드에서나.. (꺽쇠)
제네릭을 사용하면 사용자가 정의한 요구 사항에 따라서 모든 유형에서 작동하는 유연하고 재사용 가능한 코드를 작성할 수 있습니다. 중복을 피하고 명확하고도 추상적인 방식으로 표현하는 코드를 만들 수 있어요.
제네릭은 다음과 같은 문제해결에 매우매우 효과가 있습니다.
func swapTwoInt(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
두개의 인자를 받아 서로의 위치를 스위칭해주는 간단한 함수인데요, 이 함수에는 가장 큰 문제점이 있습니다.
확장이 불가능합니다.
이말인 즉슨 입력을 받을 때 타입이 Int로 고정되어있기 때문에 Float, Double, String ...등등의 타입들을 받고 싶다면 함수를 다시 만들어야 합니다.
func swapTwoDouble(_ a: inout Double, _ b: inout Double) {
let temp = a
a = b
b = temp
}
func swapTwoString(_ a: inout String, _ b: inout String) {
let temp = a
a = b
b = temp
}
...
이는 명확하게 비효율적이겠죠? 이를 해결해주는 담당일진이 제네릭입니다.
func swapTwoValue<T>(_ a: inout T, _ b: input T) {
let temp = a
a = b
b = temp
}
제네릭함수는 예상되는 타입을 함수이름 우측에 꺽쇠로 ' <> ' (병아리같네욤) 표현해줍니다.
💡 T는 어디서 나온 이름이에요?
보통 이름을 선언할 때 단일 문자나 Upper Camel Case로 많이 사용합니다.
예) T, V, U, Element
여기서 T는 Type의 약자입니다.
사용하게 될 실제 유형은 함수가 호출될 때마다 결정이 됩니다.
a와 b의 타입이 같기 때문에 서로 다른 타입을 넣는 것은 허용되지 않습니다.
let firstValue: Int = 10
let secondValue: Double = 5.41
swapTwoValue(&firstValue, &secondValue)
// Cannot convert value of type 'Double' to
// expected argument type 'Int'
firstValue를 넣을 시점에 이미 T는 Int로 결정이 났기 때문에 Double이 들어올 수 없는 것입니다.
앞에서 살펴본 제네릭은 함수였습니다. 클래스, 구조체, 열거형 타입에서도 선언할 수가 있어요. 이를 제네릭 타입이라고 합니다.
제네리 함수와 마찬가지로 작성할 수 있는데, Stack을 한번 만들어보도록 하겠습니다.
struct Stack<Element> {
var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
// 사용
var stackOfStrings = Stack<String>()
stackOfStrings.push("하나")
stackOfStrings.push("둘")
stackOfStrings.push("셋")
stackOfStrings.push("넷")
stackOfStrings.pop()
Stack에 저장할 유형을 <>안에 작성하여 새 인스턴스를 만들어 사용할 수 있습니다.
extension을 통해 확장을 할 수 있습니다. 다만 정의할 때 매개변수 목록을 제공하지 않기 때문에 본문의 매개변수를 가져다가 사용을 해야 합니다.
extension Stack {
/// 본문에서 사용한 제네릭 타입 Element 사용
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
확장의 일부로 where절을 활용 할 수도 있어요!
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
...
}
}
제네릭으로 어떤 타입이든 받던 Stack에서 Equatable Protocol을 채택한 타입만이 사용할 수 있습니다. 이렇게 제약을 주어 타입마다의 사용가능한 함수를 제한할 수 있게 됩니다.
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
...
}
제약은 클래스 제약과 프로토콜 제약이 있습니다.
위 코드에서 클래스제약은 T 자리에 들어오는 어떠한 타입은 T의 하위 클래스여야 하는 조건입니다.
프로토콜 제약은 해당 프로토콜을 준수해야 하는 제약 조건이 되겠습니다.
class Move {}
class Animal: Move {}
class TV {}
let move = Move()
let animal = Animal()
let tv = TV()
func walk<T: Move>(input: T) -> String {
}
walk(move) // ✅
walk(animal) // ✅
walk(tv) // ❌
위 코드에서 Move라는 하나의 클래스를 만들었습니다.
Animal은 움직일 수 있으니 Move를 상속해줍니다.
인스턴스를 생성하고 walk함수의 인자로 넣어주면 move와 animal은 동작을 하게 됩니다. Move타입을 준수했기 때문입니다.
반면 tv는 타입을 준수하지 못했기 때문에 실행할 수 없습니다.
protocol Hash {
associatedtype Element
var element: [Element] { get set }
mutating func pop() -> Element?
}
프로토콜에서는 associatedtype를 통해 연관타입을 지정해줄 수 있습니다. 채택하는 타입에 의해서 실제 타입으로 대체되는 연관 타입을 정의하게 되며 추상화된 형태로 만들 수 있어요.
이제 Hash protocol을 한번 채택해보겠습니다.
struct Store: Hash {
var element: [Int] = []
mutating func pop() -> Int? {
if element.isEmpty { return nil }
return element.popLast()
}
}
Hash에 의해 필수적으로 구현해야하는 프로퍼티와 메서드가 있습니다. 이들의 타입은 associatedtype을 통해 정해졌었는데요, swift는 타입추론을 통해 Store에서 사용된 Int라는 타입이 associatedtype의 Int임을 알 수 있습니다.
💡 다음과 같이 사용하지 않아도 자동으로 추론해요!
typealias Element = Int
associatedtype에도 제약을 걸 수 있나요? 넵 가능합니다.
protocol Heap {
associatedtype Element: Equatable
}
Element가 Equatable 프로토콜을 준수하게 만들 수 있습니다.
또한 이런것도 가능한데요, 자기 자신을 준수해야 할 사항에 포함시킬 수 있습니다.
protocol Hash {
associatedtype Element
var element: [Element] { get set }
mutating func pop() -> Element?
}
protocol HashImprove: Hash {
associatedtype Improve: HashImprove where Improve.Element == Element
mutating func pop() -> Improve
}
Hash 프로토콜을 준수하는 HashImprove 프로토콜을 만들었습니다.
HashImprove의 연관타입에는 Improve가 있습니다.
이 Improve는 HashImprove 프로토콜을 준수해요. 그리고 이 Improve의 Element 타입이 Element랑 같아야 합니다.
간단하게 그림으로 그려보았어요.
Improve에서 Element에 접근하는 방법과 바로 Element에 접근하는 방법을 정리한 그림입니다.
여기까지 제네릭을 알아보았는데요! 오픈소스를 찾아보거나 여러 사람들의 코드들을 볼 때면 항상 제네릭으로 되어있어 읽기에 어려움을 많이 느꼈었습니다. 이해하고 코드를 읽고 직접 만들어보며 어느정도 두려움을 사라진 것 같습니다. 😁
Generic 덕에 Collection 같은 자료구조에도 우리가 직접 만든 enum, class, struct, protocol 등을 담아서 활용할 수가 있죠
애플이 미래를 예측해서 우리가 만들 클래스에서 사용할 수 있는 Array를 만들어 놓을 수는 없으니까요
자기 자신을 제약사항으로 거는 Generic을 설명하시면서 HashImprove라는 protocol을 제시하셨는데
이 프로토콜을 채택해서 구현하려면 어떻게 코드를 작성해야 할까요?