참조
https://betterprogramming.pub/generics-in-swift-2cd549c53075
위 글을 보며 정리한 글 자세한 내용은 위 링크 참고 baram
다형성이란 여러 유형에서 작동하는 단일 인터페이스 또는 이름을 사용하는것을 의미한다.
주로 세 가지 유형이 존재한다.
ad hoc polymorphism
) 우리는 같은 이름을 가진 여러함수를 정의할 수 있지만 ad hoc polymorphism
이라고 하는 다른 유형이다.Subtype polymprphism
). 함수가 클래스 C
를 예상하는 경우 C
의 하위 클래스를 전달할 수도 있다. 이를 하위 유형 다형성이라고 한다.Parametric polymorphism
). 함수에 일반 매개변수가 있는 경우 다른 유형으로 전달할 수 있다.그 이유 중 하나는 코드의 양이 적어진다.
func square(value: Int) -> Int { ... }
func square(value: Double) -> Double { ... }
func square(value: Float) -> Float { ... }
// 대신 제네릭을 사용한다면 아래와 같다
func square<T>(value: T) -> T { ... }
위의 함수 처럼 다양한 매개변수 유형(함수 오버로딩)을 처리하기 위해 여러 함수를 작성하는 대신 단일 함수를 작성할 수 있다. 꼭 T
를 넣지 않아도 되며 대신 PutAnythingHere
같이 아무거나 넣어도 상관없다.
제네릭을 사용하여 클래스가 작동하는 유형의 유형을 표현하고 제한합니다. 아래 코드는 정수만 배열에 넣을 수 있게 제한됩니다.
var arr: Array<Int>
arr = [1, 2, 3, 4, 5]
// Array 에는 다음과 같이 정의된 일반 유형이 있기 때문에 위와 같이 작성이 가능하다.
public struct Array<Element> { ... }
만약 모든 항목을 담을 수 있는 배열을 갖고 싶다면 다음과 같이 정의가 가능합니다.
var arr: Array<Any>
arr = [1, "Two", 5.8, Car(), ...]
하지만 위와 같은 방식은 배열의 요소를 사용하려면 매번 언래핑을 해야합니다. 좋지 않은 코드의 사용 예시라고도 할 수 있습니다.
다시 위의 코드를 보겠습니다.
func square<T>(value: T) -> T { ... }
만약 위 코드에 문자열이 전달이 된다면 어떻게 될까요 ? 문자열에는 위 함수의 의미가 없습니다. 따라서 우리는 유형을 제한해야 합니다.
func square<T: Numeric>(value: T) -> T { ... }
위와 같이 작성하면, 숫자 프로토콜을 준수하는 유형의 값만 전달할 수 있습니다.
제네릭 형식으로 작동하는 스택 구조체를 만들어 보겠습니다.
struct Stack<Object> {
private var objects: [Object] = []
mutating func push(object: Object) {
objects.append(object)
}
mutating func pop() -> Object? {
return objects.popLast()
}
func peek() -> Object? {
return objects.last
}
}
위 스택을 바탕으로, 우리가 정의한 함수 스택을 적용해보겠습니다.
typealias Work = () -> Void
var stack: Stack<Work> = Stack()
stack.push { ... }
위 코드는 인수를 받지않고 아무것도 반환하지 않는 typealias work
만 푸쉬할 수 있습니다.
다른 예로 SwiftUI에서 다른 뷰를 하위 뷰로 받는 뷰를 만들어 봅시다. SwiftUI에서 뷰는 프로토콜이므로 우리도 그에 맞게 제한을 주면서 만들어 보겠습니다.
struct SomeView<Content: View>: View {
let subView: Content
var body: some View {
subView
}
}
// 이미지와 텍스트는 View이므로 아래와 같이 사용이 가능하다
let someView = SomeView(subview: Text("some text..."))
let anotherView = SomeView(subview: Image("some_imag "))
마지막 예로 스위프트의 옵셔널을 살펴보겠습니다.
enum Optional<Wrapped> {
case none
case some(Wrapped)
}
옵셔널은 제네릭 타입입니다. Wrapped
에 대한 값을 선택하면 구체적인 유형을 얻습니다. Optional<Int>, Optional<UIView>
모두 구체적인 유형입니다.
Associated Type
은 프로토콜에서 사용되는 타입의 플레이스홀더입니다. 이의 실제 유형은 프로토콜이 채택될 때까지 지정되지 않습니다. 프로토콜에서 지정되는 제네릭 타입입니다. 이것이 프로토콜에서 제네릭을 사용하는 방법입니다.
이제 프로토콜로 Stack
을 정의해보겠습니다.
protocol Stack {
associatedtype Object
mutating func push(object: Object)
mutating func pop() -> Object?
func peek() -> Object?
}
이제 이것을 채택하는 구조체를 만들어 보겠습니다.
// objects 관련 함수이므로 typealias Object = Work 생략가능 컴파일러가 유추해준다.
struct WorkStack: Stack {
private var objects: [Work] = []
mutating func push(object: @escaping Work) {
objects.append(object)
}
mutating func pop() -> Work? {
return objects.popLast()
}
func peek() -> Work? {
return objects.last
}
}
// 사용 예시
var workStack = WorkStack()
workStack.push {…}
while let work = workStack.pop() {
work()
}
특정 유형의 스택이 될 수 이쓴 프로토콜을 준수하는 제네릭한 스택을 만드려면 아래와 같이 작성하면 됩ㄴ디ㅏ.
struct MyStack<Item>: Stack {
private var objects: [Item] = []
mutating func push(object: Item) {
objects.append(object)
}
mutating func pop() -> Item? {
return objects.popLast()
}
func peek() -> Item? {
return objects.last
}
}
// 어떠한 타입도 받을 수 있다.
var carStack = MyStack<Car>()
carStack.push(Car())
이러한 프로토콜을 가지고 기존 클래스의 확장도 가능합니다.
extension Array: Stack {
mutating func push(object: Element) {
self.append(object)
}
mutating func pop() -> Element? {
return self.popLast()
}
func peek() -> Element? {
return self.last
}
}
위에서 Element
를 컴파일러는 스택 프로토콜의 관련 타입으로 유추합니다.
이제 우리가 만든 Stack
프로토콜을 전달하려면 어떻게 해야 할까요 ?
// 아래 코드는 에러가 발생합니다.
func executeStack(stack: inout Stack) { ... }
// some or any를 사용하면 에러가 발생하지 않습니다.
// any some은 다른 주제라 다루지 않겠습니당. Stack 프로토콜에 맞으면 통과한다고 생각하면 되겠습니다.
func executeStack(stack: inout some Stack) { ... }
// 아래 코드도 에러입니다. 스택 내부가 함수인지 모르기 때문에 당연하다고 볼 수 있습니다.
while let work = stack.pop() { work() }
// 좀 더 구체적으로 알려줄 필요가 있습니다.
func executeStack<T: Stack>(stack: inout T) where T.object == Work {
while let work = stack.pop() { work() }
}
하지만 바로 위 코드는 좀 더럽습니다.(little ugly 라네요 ㅌㅋ)
하나의 조건을 포함하지만 좀 깔끔하게 만들어 보겠습니다.
// 먼저 프로토콜을 아래와 같이 선언해줍니다 컴파일러에게 Object가
// 스택 프로토콜의 기본 연관 타입임을 알려줍니다.
protocol Stack<Object> {
associatedtype Object
...
}
// 아래와 같이 작성이 가능해집니다.
func executeStack(stack: inout some Stack<Work>){
while let work = stack.pop() { work() }
}
위 함수에서 타입을 명확하게 하기 위해 where
을 사용했지만, 기본 연관 유형이라는 대안이 있기 때문에 불필요합니다. 하지만 확장을 할때는 where
을 주로 사용합니다.
자체 인코딩 기능을 추가하기 위해 확장을 하려고 한다면 아래와 같이 정의합니다. 따라서 이 확장은 해당 항목을 인코딩 할 수 있는 인스턴스에만 적용이 됩니다. 다른 모든 경우에 이 확장이 존재하지 않습니다.
extension MyStck: Encodable where Item: Encodable {
func encode() -> Data? { try? JSONEncoder().encode(self) }
}
// 사용 예시, String은 이를 준수하기 때문에 사용이 가능하다.
var stringStack = MyStack<String>()
stringStack.push(object: "a string")
print(stringStack.encode())
// Optional(20 bytes)
또 비슷하게 프로토콜을 준수하지만 연관타입이 특정 타입을 준수하는 경우에만 확장을 할 필요도 생길 수 있습니다.
extension MyStack where Item == Work {
mutating func execute() {
while let work = self.pop() { work() }
}
}
Swift 5.7에서는 아래와 같이 작성해도 동일하게 기능합니다.
extension MyStack<Work> {
mutating func execute() {
while let work = self.pop() { work() }
}
}
당연히 여러 조건들도 작성이 가능합니다.
extension Foo where T: Sequence, T.Element == Character {
func specialCaseFoo() { … }
}
마지막으로 타입 별칭에도 사용하는 예시를 살펴보고 마무리하겠슴다 상당히 길었네요 ~
typealias StringDictionary<T> = Dictionary<String, T>
typealias DictionaryOfStrings<T : Hashable> = Dictionary<T, String>
typealias IntFunction<T> = (T) -> Int
typealias Vec3<T> = (T, T, T)
typealias BackwardTriple<T1,T2,T3> = (T3, T2, T1)
사용예시를 손에 익히면 유용하게 쓰는곳이 있지 않을까 합니다 !