[Swift] 22. Generics

도윤·2021년 8월 19일
0

Swift

목록 보기
19/21

Generic 코드는 내가 정의한 요구사항에 충족하는 어떠한 타입으로 작업하는 융통성있고 재사용가능한 함수나 타입으로 쓰도록 해준다. 중복을 방지하고 명확하게 쓸 수 있다.

Generic은 스위프트에서 가장 강력하고 스위프트 표본 라이브러리의 대다수도 generic code로 정의되어 있다. 예를 들어 Array,Dictionary타입 또한 generic 타입이다.


The Problem That Generics Solve

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

var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Prints "someInt is now 107, and anotherInt is now 3

두 Int형의 값을 바꾸는 것이다. 매우 유용하지만 Int 타입에서만 사용가능하다.
Double,String 타입의 변수를 스왑할때도 위처럼 또 선언해야하는 번거로움이 발생한다.
이러한 불편함을 더 방지하기 위해 any타입의 변수를 스왑하는 제너릭 코드로 작성하게 되면 더 융통성있고 유용하다.


Generic Functions

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

Generic 함수는 any타입으로 쓰인다. 함수의 코드 내용은 똑같지만 위에 함수 선언할 때의 작성 방법이 위와는 다르다.

func swapTwoInts(_ a: inout Int, _ b: inout Int)
func swapTwoValues<T>(_ a: inout T, _ b: inout T)

Generic 버전의 함수는 Int,String처럼 실제 타입이 아닌 T라고 불리는 placeholder를 사용한다.
T의 타입을 특정시키는 것이 아닌 같은 타입의 T의 변수를 뒤바꾸는 것이 가능하다.

generic함수와 아닌것의 차이는 generic함수는 <>안에 T라고 placeholder를 작성하는 것이다.

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

Type Parameters

위의 코드에서 placeholder 타입 T는 type parameter의 한 예이다. 타입 파라미터는 는 자리 표시자 유형을 지정하고 이름을 지정하며 함수 이름 바로 뒤에 일치하는 <>사이에 지정해야 한다.

파라미터 타입을 지정하면 함수의 파라미터 타입이나 함수의 반환값이나 함수의 코드 내에서 type annotaion으로써 정의할 수 있다. 각각의 경우 타입 파라미터는 함수가 호출될 때 언제든지 실제 타입으로 대체된다.


Naming Type Parameters

타입 파라미터는 Dictionary<Key,value>에서 key,value 또는 배열의 element처럼 descriptive이름을 가진다. 그러나 그것들 사이에 특별한 의미가 없다면, T,U같이 single letter로 작성하면 된다. 항상 대문자로 작성해야 한다.


Generic Types

Array나 Dictionary처럼 any타입으로 열거형이나 클래스,구조체를 작성할 수 있다.

이 부분은 Stack이라고 불리는 Generic Collection 타입을 정의하는 방법을 보여준다.
스택은 순서가 있는 value 집합이지만 array타입보다는 더 엄격하게 동작한다. Array는 삽입과 삭제가 자유롭지만 스택은 collection의 마지막에만 제거를 하거나 삽입을 할 수 있다.

  1. 스택에 3가지 값이 존재
  2. 4번째 값은 stack의 top에 push
  3. 4가지 값을 가지고 있고, 가장 최근에 추가된 값은 꼭대기에 존재
  4. 스택의 꼭대기 값이 pop
  5. 값이 빠져나간 후 3가지의 값이 존재
struct IntStack {
    var items = [Int]()
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
}

Int타입 Stack이다. 이 구조체는 item이라고 불리는 Array 프로퍼티를 사용한다. Stack은 push와 pop메서드를 제공하고 있다. 이 메서드는 구조체의 item 배열을 수정해야 하므로 mutating을 작성해야 한다.

IntStack은 Int형 값만 사용할 수 있지만 Generic타입을 이용하면 더 다양하게 사용가능하다.

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

코드의 내용은 같지만 Element라고 불리는 타입 파라미터가 Int형 대신 쓰인다. 타입 파라미터는 구조체 이름 뒤에 <>안에 쓰여진다.

Element는 후에 제공될 타입을 위한 placeholder name을 정의이다.

  • Element의 타입을 갖는 빈 array로 생성되는 item이라고 불리는 프로퍼티를 만드는 것
  • push 메서드를 정의하는 것은 매개변수가 item 배열의 타입인 element가 필요
  • pop메서드의 반환값을 정의하기 위해 반드시 Element타입을 써야 한다.

Generic 타입이기 때문에 스택은 any타입으로 만들어진다.

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// the stack now contains 4 strings

let fromTheTop = stackOfStrings.pop()
// fromTheTop is equal to "cuatro", and the stack now contains 3 strings

위 처럼 사용하면 된다.


Extending a Generic Type

generic type을 연장할 때, parameter 타입 리스트를 제공할 필요가 없다. 대신 원래의 타입 정의로부터 type parameter는 extension 내에서 이용가능하고 원래 형식 매개 변수 이름은 원래 정의에서 형식 매개 변수를 참조하는 데 사용된다.

extension Stack {
    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
}

topItem은 read-only 계산 프로퍼티를 추가하는 코드이다.element 타입의 옵셔널 값을 반환하는데 만약 비어있따면 nil, 아니면 top값을 반환한다.

extension은 파라미터 타입을 정의하지 않았지만 대신 stack의 타입인 Element가 계산 프로퍼티에서 사용되는 것을 볼 수 있다.

topItem 계산 프로퍼티는 제거하는 것 없이 top값을 반환하는데 사용된다.

if let topItem = stackOfStrings.topItem {
    print("The top item on the stack is \(topItem).")
}
// Prints "The top item on the stack is tres.

Type Constrains

위에서 본 swapTwoValues(::) 함수와 Stack은 any타입으로 작성된다. 그러나 generic 타입과 함수에서 사용될 수 있는 타입을 특정하는 것이 때때로 유용하다. Type constrains 타입 매개변수가 특정 클래스에서 상속되거나 특정 프로토콜 또는 프로토콜 구성을 준수해야 함을 지정합니다

예를 들어 Dictionary타입은 Dictionary key로 사용될 타입들이 제한이 있는데, key는 반드시 hashable해야 된다. 즉 반드시 그자체로 uniquely하게 표현될 수 있어야 한다. 따라서 dictionary의 특정 key의 value가 포함되어 있는지 아닌지 확인할 수 있어야 한다.

제네릭 타입을 만들 때 type에 제약을 설정할 수 있으며 이 제약들은 프로그래밍에 많은 기능을 제공한다.

Type Constraint Syntax

클래스나 프로토콜을 타입 파라미터 이름 뒤에 놓으면서 설정할 수 있다.

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

타입 제한의 가장 기본적인 문법이다.
someFuntion은 2가지 매개변수를 가진다. 첫번째 매개변수의 타입 T는 someClass의 subclass여야하고 두번째 매개변수 U는 someProtocol을 따라야 한다.

Type Constraints in Action

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

nongeneric함수인 findIndex는 array에서 주어진 String을 찾고 index를 반환하는 함수이다.

제네릭 타입으로 바꿔 보자

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

제네릭 타입으로 만듦으로써 모든 타입의 배열에서도 값을 찾을 수 있다.
하지만 "if value == valueToFind"에서 문제가 발생할 수 있다.
모든 타입에서 == 연산자를 사용하지 않기 때문에 제약을 설정해야 한다.

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

T가 Eqautable 프로토콜을 따르는 타입만 사용할 수 있다.


Associated Types

프로토콜을 정의할 때 하나 이상의 associated 타입을 선언하는 것이 좋을 때가 있다. associated type은 프로토콜의 일부분으로써 사용된 타입에 placeholder 이름을 준다.

Associated Types in Action

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

Container 프로토콜은 append,count,subscript의 요구사항들을 정의했다. Container로써 여겨지기 위해선 반드시 append,count,subscipt를 구체화해야 된다.

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]
    }
}

IntStack은 컨테이너 프로토콜의 모든 요구 사항을 준수하기 때문에 Swift는 단순히 adpend(_:) 메서드의 항목 매개 변수 유형과 첨자의 반환 유형만 보고 사용할 적절한 항목을 추론할 수 있다. 실제로 위의 코드에서 typealias를 제거해도 항목에 사용할 유형이 명확하기 때문에 모든 항목이 계속 작동한다.

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

Associated를 사용한 프로토콜도 extension을 사용할 수 있다.

extension Array: Contrainer{}

위와 같이 Array타입에 Container 프로토콜을 추가로 채택할 수 있다. Array 타입에는 이미 append(_ :)메서드, count 프로퍼티, Int인덱스를 사용하는 서브 스크립트가 존재하기 때문에 Container가 요구하는 것을 모두 준수한다.

Adding Constraint to an Associated Type

프로토콜에 정의된 associated type도 추가할 수 있다.

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

Container 프로토콜을 채택하기 위해 container의 Item 또한 Equatable해야 한다.

Using a Protocol in Its Associated Type's Constraints

프로토콜은 요구사항의 일부로써 나타날 수 있다.

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

suffix타입은 suffixableContainer을 따라야 하고 Item타입은 Contrainer 프로토콜의 Item 타입과 동일해야 한다.

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

이 코드에서 Suffix의 associated 타입도 Stack이므로 suffix(_ :)메서드는 Stack타입을 반환한다.


extension IntStack: SuffixableContainer {
    func suffix(_ size: Int) -> Stack<Int> {
        var result = Stack<Int>()
        for index in (count-size)..<count {
            result.append(self[index])
        }
        return result
    }
    // Inferred that Suffix is Stack<Int>.
}

위의 코드는 제네릭을 사용하지 않은 IntStack 구조체에 익스텐션을 사용하여 SuffixableContainer 프로토콜을 추가로 채택하는 코드이다. Suffix의 타입으로 IntStack대신 Stack<Int>를 사용한 것을 볼 수 있다.


Generic Where Clauses

Type Constraints는 제네릭 함수과 관련된 타입 매개변수에 대한 요구사항을 정의 할 수 있다.

Associated 타입에 대해 요구사항을 정의하는 것도 유용할 수 있다. 이러한 정의는 제네릭 where 절을 정의하여 할 수있다. 제네릭 where절을 사용하면 associated 타입이 특정 프로토콜을 준수해야 하거나 특정 타입 매개변수와 동일해야 한다고 요구할 수 있다. 제네릭 where 절은 where키워드로 시작하고 그 뒤에 associated 타입에 대한 제약조건, 동등관계를 써주면 된다.

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
}

where절에서 associated type의 제약조건을 설정하는 것을 볼 수 있다.

  • C1은 Container 프로토콜을 준수해야 한다.
  • C2는 Container 프로토콜을 준수해야 한다.
  • C1의 항목과 C2의 항목은 동일해야 한다.
  • C1의 항목들은 Equatable 프로토콜을 준수해야 한다.

첫번째와 두번째 제약조건은 함수 선언 시 매개변수를 설정할 때 정의 할 수 있고 세번째와 네번째는 함수의 where절에서 정의할 수 있다.

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.

Extension with a Generic Where Clause

where절을 extension의 일부로써 사용할 수 있다.
익스텐션으로 추가한 isTop(_ :) 메서드는 매개변수로 받은 값이 현재 인스턴스의 제일 위에 존재하는 값인지를 확인해주는 메서드이다.

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.

여기서 타입 매개변수로 사용할 Element는 Equatable 프로토콜을 준수하지 않는다면 컴파일 에러를 발생한다.

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

프로토콜 extension에 where절을 사용할 수 있다.

extension Container where Item: Equatable {
    func startsWith(_ item: Item) -> Bool {
        return count >= 1 && self[0] == item
    }
}
if [9, 9, 9].startsWith(42) {
    print("Starts with 42.")
} else {
    print("Starts with something else.")
}
// Prints "Starts with something else.

Item이 Equatable을 따르도록 요구하는 것이다.

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"

Item의 특정 타입을 요구하는 where절도 설정할 수 있다.


Contextual Where Clause

제네릭 타입을 정의할 때 where절의 일부에만 제약조건을 사용할 수 있다.

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"

위와 같이 average()메서드와 endsWith(_ :)메서드에 서로 다른 제약조건을 줄 수 있다.

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
    }
}

위와 동일한 코드이지만 각각의 extension의 where절을 따로 정의하여 사용할 수도 있다.


Associated Types with a Generic Where Clause

Where절을 Associated type에도 사용할 수 있다.

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
}

위의 코드처럼 Associated 타입에 제네릭 where절을 사용할 수 있다. Iterator 타입에 where절로 제약조건을 준 것을 볼 수 있다.

protocol ComparableContrainer: Contrainer where Item:Comparable{}

만약 어떤 프로토콜을 상속하려는 경우 where절을 사용하여 상속된 associated 타입에 제약조건을 추가할 수 있다.


Generic Subscipts

서브 스크립트에도 제네릭을 사용할 수 있고 where절을 사용하여 제약조건을 줄 수도 있다.

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
    }
}

위와 같이 서브 스크립트에 제네릭과 where절을 사용할 수 있다. 위의 코드에서 서브 스크립트는 다음과 같은 제약조건이 있다.

<>로 묶인 매개변수 Indices는 Sequence프로토콜을 준수하는 타입 이어야 한다.
서브 스크립트는 매개변수로 Indices타입의 인스턴스를 사용한다.
매개변수 indices에 전달된 값은 Int 타입이다..

0개의 댓글