[TIL] 스위프트의 제네릭

rbw·2022년 12월 19일
0

TIL

목록 보기
56/98

스위프트의 제네릭

참조

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을 사용했지만, 기본 연관 유형이라는 대안이 있기 때문에 불필요합니다. 하지만 확장을 할때는 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)

사용예시를 손에 익히면 유용하게 쓰는곳이 있지 않을까 합니다 !

profile
hi there 👋

0개의 댓글