Generic과 associatedtype

Lily·2022년 4월 28일
0

안녕하세요 릴리입니다😊
오늘은 General한 함수와 타입을 만들 수 있는 방법인 Generic과 associatedtype에 대해 공부해보겠습니다.

📙Swift Language Guide - Generic을 토대로 작성되었습니다.

1. Generic


🧐 Generic이 무엇인가요?

함수나 타입에서 구체적인 타입 대신 플레이스 홀더 타입을 사용하여, 특정한 타입에 국한되지 않는 general한 함수와 타입을 만들 수 있는 문법입니다.

스위프트 스탠다드 라이브러리의 Dictionary, Array도 제네릭으로 만들어진 타입입니다.


💁🏻‍♀️ 어떻게 구현할까요?

구체적인 타입 대신 타입 파라미터라는 것을 사용합니다. 제네릭을 사용하려는 함수나 타입의 이름 뒤 꺽쇠(<>)안에 파라미터 타입을 작성해줍니다.

타입 파라미터는 함수가 호출될 때, 타입이 인스턴스화 될 때 구체적인 타입으로 결정됩니다.

타입 파라미터에 의미가 있는 경우 이름을 지어줄 수도 있습니다.
(Dictionary<Key,Value> 와 같이)
관계가 없는 경우엔 보통 T, U, V를 사용합니다.

Generic Function

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

Generic Type

struct Stack<Element> {
    var items: [Element] = []
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

🤷🏻‍♀️ Generic 왜 쓸까요?

동일한 기능의 함수나 타입이지만, 변수, 파라미터, 리턴값의 타입만 다르게 구현해야하는 경우에 제네릭을 사용하면 한가지 정의로 다양한 구체 타입을 커버할 수 있습니다.

파라미터로 전달 받는 a와 b를 swap하는 함수를 만들어본다고 합시다.
제네릭을 사용하지 않고 String, Double, Int 타입의 a, b를 swap하는 함수를 만들려고 하면 각 타입마다 함수를 만들어줘야합니다. 즉, 총 3개의 함수가 필요하게 됩니다.

func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA = a
    a = b
    b = temporaryA
}

func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
    let temporaryA = a
    a = b
    b = temporaryA
}

func swapTwoInt(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

하지만 제네릭을 사용하면 하나의 함수면 충분합니다.. 단지, 함수를 호출할 때 원하는 타입을 넣어주기만 하면 됩니다.

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

// Int를 swap하는 경우
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3

// String을 swap하는 경우
var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"

🙌 Type constraints

제네릭에서 타입 파라미터안에는 어떤 타입이든 들어올 수 있습니다.
하지만 Type constraints를 사용하면 특정한 프로토콜을 채택하거나 특정한 클래스를 상속하는 타입만 들어올 수 있도록 강제할 수 있습니다.

어떻게 제한하나요?

타입 파라미터의 콜론(:)뒤에 요구되는 프로토콜이나 타입을 작성합니다.

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // function body goes here
}

여러개의 프로토콜을 채택하도록 아래와 같이 제한할 수도 있습니다.

protocol SomeProtocol {
    
}

protocol AnotherProtocol {
    
}

func read<T: SomeProtocol & AnotherProtocol>(book: T) {
    
}

2. associatedtype


🧐 associatedtype이 무엇인가요?

프로토콜 내에서 사용되는 플레이스 홀더 타입을 associated type이라고 합니다.
프로토콜을 제네릭하게 사용할 수 있는 방법입니다.

protocol Container {
    associatedtype Item
    var element: Item { get }
    mutating func append(_ item: Item)
}

이 associatedtype은 프로토콜이 구현될 때 typealias 로 구체 타입으로 결정할 수 있습니다.

  • 프로토콜을 채택하는 타입에서 typealias 로 지정
struct Bag: Container {
    
    typealias Item = String
    
    var element: String
    
    mutating func append(_ item: String) {
        //
    }
    
}
  • 프로토콜을 채택하는 타입에서 associatedtype 이 사용된 자리에 타입 파라미터를 넣어줌
struct Box<T>: Container {
    
    var element: T
    
    mutating func append(_ item: T){
        //
    }
    
}

🤷🏻‍♀️ associatedtype에 제한 줄 수 있나요?

associatedtype도 제네릭과 동일하게 type constraints를 줄 수 있습니다.

protocol Container {
    associatedtype Item: Equatable
    var element: Item { get }
    mutating func append(_ item: Item)
}

🤲 좀 더 많은 제한을 주고 싶을 때, where 절

Type constraints, 타입 제약은 특정한 프로토콜이나 클래스에 대한 제약만 가능합니다.
하지만 where절은 특정한 프로토콜, 클래스 제약이외에도 다양한 조건을 추가할 수 있습니다.

먼저 특정한 프로토콜 채택 제약, 클래스 상속 제약에 대한 작동은 타입 제약과 where절이 동일합니다.

// 타입 제약
struct Box<T: Equatable>: Container {
    
    var element: T
    
    mutating func append(_ item: T){
        //
    }
    
}

// where절
struct Box<T>: Container where T: Equatable {
    
    var element: T
    
    mutating func append(_ item: T){
        //
    }
    
}

하지만 where절만 associatedtype과 관련된 제약등 더 다양한 제약을 추가할 수 있습니다.

func allItemsMatch<C1: Container, C2: Container>
    (_ someContainer: C1, _ anotherContainer: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {

        // Check that both containers contain the same number of items.
        if someContainer.count != anotherContainer.count {
            return false
        }

        // Check each pair of items to see if they're equivalent.
        for i in 0..<someContainer.count {
            if someContainer[i] != anotherContainer[i] {
                return false
            }
        }

        // All items match, so return true.
        return true
}

🙅‍♀️ 하지만 associatedtype을 사용한 프로토콜은 타입으로 사용이 불가합니다

associatedtype은 프로토콜을 제네릭하게 사용할 수 있는 방법이라 추상화에 유용하게 사용될 수 있는데요. 이런 프로토콜을 어떠한 변수에 대한 타입이나, 리턴 타입, 파라미터 타입으로서는 사용이 불가능합니다.😰

사용하려고 하면 아래와 같은 경고문을 만나게 됩니다.

제네릭 제약으로서만 사용이 가능하다고 합니다.

https://github.com/apple/swift-evolution/blob/main/proposals/0309-unlock-existential-types-for-all-protocols.md
Swift 5.7 부터는 associatedtype과 관련된 사항에 변동이 있는 것 같은데,
타입으로 사용이 가능한지 추후에 읽어보아야겠습니다.

profile
i🍎S 개발을 합니다

0개의 댓글