ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Generics
    iOS/Swift 공식문서 2022. 2. 25. 23:41

    Generics

    generic은 개발자가 정의한 요구사항에 따라 어느 타입과도 사용할 수 있는 유연하고, 재사용 가능한 함수와 타입을 작성할 수 있게 한다. 이를 통하여 코드 작성 시 중폭을 피하고 개발자의 의도를 명확하고 추상적인 방식으로 전달할 수 있다.


    generic는 Swift의 가장 강력한 기능 중 하나이고, Swift 표준 라이브러리의 많은 코드가 generic으로 작성되어 있다. 예를 들어, ArrayDictionary 타입도 모두 generic이다. Int, String 뿐만 아니라 Swift에서 만들어진 다른 모든 타입의 컬렉션을 만들 수 있다.


    The Problem That Generics Solve

    swapTwoInts(_:_:) 함수를 정의해보자.

        let temporaryA = a
        a = b
        b = temporaryA
    }

    이 함수의 문제점은 Int 값에 대해서만 사용할 수 있다는 것이다. String이나 Double 값을 swap하기 위해서는 이를 위한 함수들을 따로 정의해야 한다. body가 모두 같은 중복 코드를 작성하게 되는 것이다.

    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
    }

    note

    위의 세 함수 모두 a, b의 타입이 같아야 한다. Swift는 type-safe language이기 때문에, 서로 다른 타입을 swap하려 하면 컴파일 에러가 발생한다.


    Generic Functions

    swapTwoValues(_:_:) 함수를 다음과 같이 정의하자. Int와 같은 실제 타입 대신 T라는 placeholder type을 사용한다. T를 통해 매개변수의 실제 타입은 정해지지 않지만, 두 매개변수의 타입이 같아야 한다는 것을 나타낼 수 있다. T에 사용될 실제 타입은 함수가 호출될 때 결정된다. 함수 이름 뒤에 bracket 안에 placeholder type name을 명시해야 하므로 주의하자.

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

    generic 함수를 정의하면 어떤 타입으로도 함수를 호출할 수 있다.

    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"

    note

    위 예시는 generic을 설명하기 위한 예시이다. 위와 같이 직접 구현하지 말고 Swift에서 제공하는 swap(_:_:) 함수를 사용하자.


    Type Parameters

    위의 swapTwoValues(_:_:) 함수에서 사용되는 placeholder type인 T를 type parameter라고 한다. type parameter는 placeholder type의 이름을 명시하고, 함수 이름 바로 뒤에 bracket(<>) 안에 적는다.


    type parameter를 명시하면, 함수 매개변수의 타입이나 리턴 타입을 정의할 때 사용할 수 있다. 함수 본문 안에서 type annotation으로 사용하는 것도 가능하다. generic 함수가 호출될 때마다 type parameter는 실제 타입으로 교체된다. 위의 swapTwoValues(_:_:) 함수를 호출한 코드에서 처음 호출할 때는 TInt로 교체되었고, 두번째로 호출할 때는 String으로 교체되었다.


    여러 type parameter를 사용하는 것도 가능한데, type parameter들을 <> 안에 콤마로 구분하여 명시하면 된다.


    Naming Type Parameters

    대부분의 경우 type parameter는 descriptive한 이름을 가지고 있다. Dictionary<Key, Value>에서의 KeyValue, Array<Element>에서의 Element를 예로 들 수 있다. 이러한 이름들은 type parameter와 generic type 혹은 function 사이의 관계를 알 수 있게 한다. 하지만 그 사이의 의미있는 관계가 없는 경우, T, U, V 등의 한 글자로 적는 것이 일반적이다.


    note

    type parameter는 type의 placeholder이기 때문에 upper camel case로 적도록 하자.


    Generic Types

    generic function 뿐만 아니라, generic type도 정의할 수 있다. 다음과 같이 Stack을 구현하기 위해 IntStack 구조체를 정의하자. 이러한 경우 Int 값 밖에 넣지 못한다.

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

    다음과 같이 generic으로 구현하면 모든 타입에 대하여 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("uno")
    stackOfStrings.push("dos")
    stackOfStrings.push("tres")
    stackOfStrings.push("cuatro")
    // the stack now contains 4 strings

    Extending a Generic Type

    generic type을 확장할 경우, type parameter들을 extension의 정의에 적지 않는다. 대신 타입을 처음 정의할 때 사용했던 type parameter를 extension에서 사용할 수 있다.


    다음과 같이 처음 정의 시 type parameter인 Element를 extension에서 사용할 수 있다.

    extension Stack {
        var topItem: Element? {
            return items.isEmpty ? nil : items[items.count - 1]
        }
    }
    
    if let topItem = stackOfStrings.topItem {
        print("The top item on the stack is \(topItem).")
    }
    // Prints "The top item on the stack is tres."

    Type Constraints

    type constraint는 type parameter가 특정 클래스나 상속받거나, 프로토콜, 혹은 프로토콜의 composition을 준수함을 명시한다. 예를 들어, Dictionary 타입의 경우 Key로 사용되는 타입은 Hashable 프로토콜을 준수해야 한다. 그래야 특정 key 값이 이미 포함되어 있는지 확인할 수 있기 때문이다. 커스텀으로 generic 타입을 만드는 경우에도 type constraint를 자체적으로 정의할 수 있다.


    Type Constraint Syntax

    각 type parameter 이름 위에 콜론(:)과 함께 하나의 클래스나 프로토콜을 명시하면 된다. 다음 예시에서 TSomeClass를 상속받아야 하고, USomeProtocol을 준수해야 함을 명시하고 있다.

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

    다음 예시를 살펴보자. 다음과 같이 배열에서 특정 element의 index를 반환하는 findIndex(of:in:) 함수를 정의하자.

    func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
        for (index, value) in array.enumerated() {
            if value == valueToFind {
                return index
            }
        }
        return nil
    }

    위 코드를 보면 if 문에서 == 연산자를 사용하였는데, 모든 타입이 이 연산자를 사용할 수 있는 것이 아니기 때문에 컴파일이 되지 않는다. 따라서 아래 코드처럼 연산자를 사용할 수 있도록 type parameter TEquatable 프로토콜을 준수하도록 강제해야 한다.

    func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
        for (index, value) in array.enumerated() {
            if value == valueToFind {
                return index
            }
        }
        return nil
    }

    이렇게 수정한 이후에는 findIndex 함수를 정상적으로 사용할 수 있다.

    let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
    // doubleIndex is an optional Int with no value, because 9.3 isn't in the array
    let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
    // stringIndex is an optional Int containing a value of 2

    Associated Types

    프로토콜을 정의할 때 정의의 일부로 associated type을 선언하는 경우가 있다. associated type은 프로토콜의 일부로서 사용되는 타입의 placeholder name을 제공한다. associated type으로 사용될 실제 타입은 프로토콜이 채택될 때 명시된다. associatedtype 키워드를 사용하여 정의한다.


    Associated Types in Action

    다음과 같이 Container라는 프로토콜을 정의하자. 이 안에는 Item이라는 associated type이 선언되어 있다.

    protocol Container {
        associatedtype Item
        mutating func append(_ item: Item)
        var count: Int { get }
        subscript(i: Int) -> Item { get }
    }

    다음과 같이 non-generic type인 IntStack을 정의할 수 있다. typealias 키워드를 사용하여 Item의 실제 타입이 Int임을 명시하고 요구사항을 구현하였다. 하지만 구현에서 Item에 실제 사용될 타입이 분명하기 때문에 typealias를 사용한 코드를 생략해도 된다.

    struct IntStack: Container {
        // original IntStack implementation
        var items: [Int] = []
        mutating func push(_ item: Int) {
            items.append(item)
        }
        mutating func pop() -> Int {
            return items.removeLast()
        }
        // conformance to the Container protocol
        typealias Item = Int
        mutating func append(_ item: Int) {
            self.push(item)
        }
        var count: Int {
            return items.count
        }
        subscript(i: Int) -> Int {
            return items[i]
        }
    }

    다음과 같이 generic type을 정의할 수 있다. Container 프로토콜에서 Item이 사용된 자리에 parameter type인 Element가 들어가면서 요구사항을 만족시킬 수 있다.

    struct Stack<Element>: Container {
        // original Stack<Element> implementation
        var items: [Element] = []
        mutating func push(_ item: Element) {
            items.append(item)
        }
        mutating func pop() -> Element {
            return items.removeLast()
        }
        // conformance to the Container protocol
        mutating func append(_ item: Element) {
            self.push(item)
        }
        var count: Int {
            return items.count
        }
        subscript(i: Int) -> Element {
            return items[i]
        }
    }

    Extending an Existing Type to Specify an Associated Type

    Array의 경우 프로퍼티 메서드가 이미 Container의 요구사항을 만족하기 때문에 다음과 같이 empty extension을 통해 프로토콜을 준수함을 명시할 수 있다.

    extension Array: Container {}

    Adding Constraints to an Associated Type

    다음과 같이 콜론 뒤에 타입을 명시하여 상속받거나 준수해야 하는 타입을 정할 수 있다.

    protocol Container {
        associatedtype Item: Equatable
        mutating func append(_ item: Item)
        var count: Int { get }
        subscript(i: Int) -> Item { get }
    }

    Using a Protocol in Its Associated Type's Constraints

    프로토콜은 요구사항의 일부로 나타날 수 있다. 아래 코드에서 SuffixableContainer 프로토콜은 Suffix 라는 associated type과 suffix(_ :) 메서드를 추가로 요구한다. Suffix 타입은 SuffixableContainer 프로토콜을 준수해야 하고 Item타입은 Container 프로토콜의 Item 타입과 동일해야 한다.

    protocol SuffixableContainer: Container {
        associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
        func suffix(_ size: Int) -> Suffix
    }

    다음과 같이 Stack를 확장하여 프로토콜의 요구사항을 만족할 수 있다.

    extension Stack: SuffixableContainer {
        func suffix(_ size: Int) -> Stack {
            var result = Stack()
            for index in (count-size)..<count {
                result.append(self[index])
            }
            return result
        }
        // Inferred that Suffix is Stack.
    }
    var stackOfInts = Stack<Int>()
    stackOfInts.append(10)
    stackOfInts.append(20)
    stackOfInts.append(30)
    let suffix = stackOfInts.suffix(2)
    // suffix contains 20 and 30

    Generic Where Clauses

    associated type에 대한 요구사항을 정의할 때는 generic where clause를 사용할 수 있다. where 뒤에 associated type에 대한 제약조건이나, 다른 타입과 associated type 사이의 관계를 적으면 된다. 이러한 generic where clause는 함수 body의 바로 앞에 작성한다.

    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
    }

    위 코드에서 두 type parameter에 대한 요구사항은 다음과 같다.


    a. C1, C2Container 프로토콜을 준수한다.
    b. C1, C2Item의 타입이 같아야 한다.
    c. C1.ItemEquatable 프로토콜을 준수해야 한다.


    a의 경우 type parameter list에 정의되어 있고, b와 c는 generic where clause에 정의되어 있다.


    결국 다음과 같이 allItemsMatch 함수를 사용할 수 있다.

    var stackOfStrings = Stack<String>()
    stackOfStrings.push("uno")
    stackOfStrings.push("dos")
    stackOfStrings.push("tres")
    
    var arrayOfStrings = ["uno", "dos", "tres"]
    
    if allItemsMatch(stackOfStrings, arrayOfStrings) {
        print("All items match.")
    } else {
        print("Not all items match.")
    }
    // Prints "All items match."

    Extensions with a Generic Where Clause

    extension의 일부로 generic where clause를 사용할 수 있다.

    extension Stack where Element: Equatable {
        func isTop(_ item: Element) -> Bool {
            guard let topItem = items.last else {
                return false
            }
            return topItem == item
        }
    }
    
    if stackOfStrings.isTop("tres") {
        print("Top element is tres.")
    } else {
        print("Top element is something else.")
    }
    // Prints "Top element is tres."

    Equatable 프로토콜을 준수하지 않는 element를 사용한 Stack에서 isTop() 메서드를 호출하면 컴파일 에러가 발생한다.

    struct NotEquatable { }
    var notEquatableStack = Stack<NotEquatable>()
    let notEquatableValue = NotEquatable()
    notEquatableStack.push(notEquatableValue)
    notEquatableStack.isTop(notEquatableValue)  // Error

    다음과 같이 parameter type과 다른 타입 사이의 관계로 제약 조건을 설정할 수 있다.

    extension Container where Item == Double {
        func average() -> Double {
            var sum = 0.0
            for index in 0..<count {
                sum += self[index]
            }
            return sum / Double(count)
        }
    }
    print([1260.0, 1200.0, 98.6, 37.0].average())
    // Prints "648.9"

    Contextual Where Clauses

    where를 사용하여 선언의 일부에서 서로 다른 제약조건을 추가할 수 있다. 다음 코드는 extension에서 두 함수에 서로 다른 제약 조건을 설정한 것이다.

    extension Container {
        func average() -> Double where Item == Int {
            var sum = 0.0
            for index in 0..<count {
                sum += Double(self[index])
            }
            return sum / Double(count)
        }
        func endsWith(_ item: Item) -> Bool where Item: Equatable {
            return count >= 1 && self[count-1] == item
        }
    }
    let numbers = [1260, 1200, 98, 37]
    print(numbers.average())
    // Prints "648.75"
    print(numbers.endsWith(37))
    // Prints "true"

    다음과 같이 두 개의 extension을 정의하는 것도 가능하다.

    extension Container where Item == Int {
        func average() -> Double {
            var sum = 0.0
            for index in 0..<count {
                sum += Double(self[index])
            }
            return sum / Double(count)
        }
    }
    extension Container where Item: Equatable {
        func endsWith(_ item: Item) -> Bool {
            return count >= 1 && self[count-1] == item
        }
    }

    Associated Types with a Generic Where Clause

    다음과 같이 associated type에 대하여 generic where clause로 제약조건을 설정할 수 있다. 다음 프로토콜에서는 두 개의 associated type을 선언했는데, 그 중 Iterator의 경우 제약조건이 설정되어 있다.

    protocol Container {
        associatedtype Item
        mutating func append(_ item: Item)
        var count: Int { get }
        subscript(i: Int) -> Item { get }
    
        associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
        func makeIterator() -> Iterator
    }

    다음과 같이 프로토콜을 상속하여 제약조건을 추가하는 것도 가능하다.

    protocol ComparableContainer: Container where Item: Comparable { }

    Generic Subscripts

    subscript도 generic으로 정의할 수 있다. 다음과 같이 subscript 키워드 뒤에 bracket 안에 type parameter를 명시하면 된다. 또한 함수 body 바로 앞에 generic where clause를 작성할 수 있다.

    extension Container {
        subscript<Indices: Sequence>(indices: Indices) -> [Item]
            where Indices.Iterator.Element == Int {
                var result: [Item] = []
                for index in indices {
                    result.append(self[index])
                }
                return result
        }
    }

    Reference

    https://docs.swift.org/swift-book/LanguageGuide/Generics.html

    'iOS > Swift 공식문서' 카테고리의 다른 글

    Automatic Reference Counting  (0) 2022.03.01
    Opaque Types  (0) 2022.02.27
    Protocols  (0) 2022.02.24
    Extensions  (0) 2022.02.23
    Nested Types  (0) 2022.02.23

    댓글

Designed by Tistory.