GGURUPiOS

Swift 공식 문서 정리 - (23) Generics 본문

Swift/공식문서 정리 ( 문법 )

Swift 공식 문서 정리 - (23) Generics

꾸럽 2023. 5. 14. 15:43

Generics (제네릭)

제네릭 코드는 모든 유형에서 작동할 수 있는 유연하고 재사용 가능한 함수 및 타입을 작성할 수 있음

중복을 피하고 의도를 명확하고 추상화된 방식으로 표현하는 코드를 작성 가능

제네릭은 스위프트의 가장 강력한 기능 중 하나임

제네릭이 해결하는 문제

다음은 두 값을 교환하는 제네릭이 아닌 표준 함수임.

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"

이 함수는 유용하지만 인트 값에만 사용이 가능함.

String과 String을 바꾸려면 String, String 을 인자로 받는 새로운 함수를 하나 더 짜야함.

제네릭 코드를 사용하면 하나의 함수로 작성할 수 있음

밑에 예제로 확인하자

제네릭 함수

제네릭 함수는 모든 타입에서 작동할 수 있음

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

전의 예제와 위의 예제는 코드가 동일함. 그러나 첫줄은 약간 다름

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

실제 실행하는 타입 T가 어떤 타입인지 보지 않음

함수가 실행되면 T에 해당하는 값을 함수에 넘김

제네릭으로 선언한 함수를 실행하면 기대했던 대로 동작함

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"

타입 파라미터

위에서 사용한 플레이스홀더 T는 타입 파라미터의 예시임

타입 파라미터는 플레이스 홀더 타입의 이름을 명시하고 함수명 바로뒤에 적어줌

그리고 꺾쇄로 묶어준다

타입 파라미터를 한번 선언하면 이 것을 함수의 타입으로 사용할 수 있음

복수의 타입 파라미터를 사용할때는 컴마로 구분함

파라미터 이름짓기

딕셔너러의 Key, Value 같이 엘리먼트 간의 서로 상관관계가 있는 경우 의미가 있는 이름을 파라미터 이름으로 붙이고, 그렇지 않은 경우 T, U, V 와 같은 단일 문자로 이름을 짓는다

타입을 의미하기 때문에 → 첫 글자는 대문자로 작성

제네릭 타입

제네릭 함수에 추가로 제네릭 타입을 정의할 수 있음

이후 섹션에서는 Stack 이라는 제네릭 콜렉션 타입을 어떻게 구현하는지 보여줌

네비게이션 컨트롤러로 예를 들어보자

그림 순서대로

1,2,3,4,5 로 가정

푸쉬(push)의 동작 후 → 팝(pop)의 동작 방식을 순서대로 나타낸 그림임

  1. 현재 스택에는 세 개의 값이 있음
  2. 네 번째 값이 스택 맨 위로 푸쉬됨
  3. 이제 스택에는 가장 최근 값이 맨 위에 있는 4개의 값이 있음
  4. 스택의 맨 위 항목이 팝 됨
  5. 값을 팝한 후 스택은 다시 한 번 세개의 값을 보유 함

IntStack을 구현

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

위의 구조는 스택에 값을 저장하기 위해 호출되는 Array 프로퍼티를 사용함

푸쉬 및 팝하는 두 가지 메소드를 제공

이러한 메소드는 구조체의 배열을 수정 해야하므로 mutating 으로 표시

그러나 위의 표시된 타입은 Int 값에만 사용할 수 있음

제네릭 코드가 훨씬 더 유용할 것임

밑의 예제는 제네릭 코드

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

제네릭 타입 확장 ( Extending a Generic Type )

익스텐션을 이용해 제네릭 타입을 확장할 수 있음

이때 원래 선언한 파라미터 이름을 사용함

여기서는 Element

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

이제 익스텐션으로 추가한 topItem 프로퍼티에 접근할 수 있음

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

타입 제한

스위프트의 딕셔너리 타입은 key 값을 사용함. 이때 key는 유일한 값이어야 하기 때문에 hashable 프로토콜을 준수해야함.

이와 같이 특정 타입이 반드시 프로토콜을 따라야 하는 경우가 있음

제네릭에서도 이런 경우가 필요할 수 있음

제네릭에서는 특정 클래스를 상속하거나 특정 프로토콜을 따르거나 합성하도록 명시할 수 있음

타입 제한 문법

제네릭 함수를 선언할 때, 파라미터 뒤에 상속 받아야 하는 클래스를 선언하거나, 반드시 따라야 하는 프로토콜을 명시할 수 있음

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

// T는 SomeClass의 하위 클래스여야 하고, U는 SomeProtocol을 준수해야 함

타입 제한의 실 사용

다음과 같이 한 배열에서 특정 문자를 검색하는 findIndex 함수를 선언

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

위 함수는 문자열 배열에서 문자열 값을 찾는 데 사용할 수 있음

let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findIndex(ofString: "llama", in: strings) {
    print("The index of llama is \\(foundIndex)")
}
// Prints "The index of llama is 2"

그러나 배열에서 값의 인덱스를 찾는 원칙은 문자열에만 유용한 것은 아님.

제네릭 함수로 구현 해보자

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

/* 
위의 함수는 컴파일 되지 않음
문제는 value == valueToFind 의 경우, 등호 메소드를 사용하기 위해서 두 값 혹은 객체가 
반드시 Equatable 프로토콜을 따라야 하기 때문
이 문제를 해결하기 위해 아래와 같이 수정해야함
*/

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

연관 타입 (Associated Types)

연관 타입은 프로토콜의 일부분으로 타입에 플레이스홀더 이름을 부여함

특정 타입을 동적으로 지정해 사용할 수 있음

연관 타입의 실 사용

아래와 같이 Item에 associatedtype 키워드를 사용해 선언한다.

이렇게 지정하면 Item은 어떤 타입도 될 수 있음

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

아래의 코드는 Item을 Int형으로 선언해 사용함

프로토콜에서 associatedtype Item은 프로토콜에선 추상적인 타입을

채택하는 곳에서 typealias Item = Int 이런식으로 구체적인 타입으로 선언

내가 쓸 Item은 무슨 타입이야! 라고 말해주는 것 같음.

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

아래 예제에서는 Item을 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]
    }
}

존재하는 타입에 연관 타입을 확장

아래와 같이 기존의 타입 Array 에 특정 연관 타입을 추가할 수 있음

extension Array: Container {}

연관 타입에 제약조건 추가

연관 타입이 해당 제약 조건을 충족하도록 요구하기 위해 프로토콜의 관련 타입에 유형 제약조건을 추가할 수 있음

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

// Item: Equatable

프로토콜의 연관 타입의 제한 사용

연관 타입을 적용할 수 있는 타입에 조건을 걸어 제한을 둘 수 있음

조건을 붙일 때는 where구문을 사용한다.

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

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

/*
위의 예제에서 Stack 의 연관 타입인 Suffix 또한 Stack임 
그래서 Stack의 suffix의 실행으로 또 다른 Stack을 반환하게 됨
*/

아래와 같이 IntStack에 Stack을 사용해

SuffixableContainer를 따르는 익스텐션 선언도 가능

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

제네릭의 Where절

제네릭에서도 where절을 사용할 수 있음.

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
}
// Container C1,C2를 비교하여 모든 값이 같을 때 true를 반환하는 함수 구현 

// Container 종류 자체는 상관없으며, 단지 각 Container의 같은 인덱스의 모든 값이 
// 같기만 하면 결과로 true를 얻게됨 

다른 타입의 Container (하나는 스택, 다른 하나는 배열)의 내용을 함수로 비교한 결과

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."

Where절을 포함하는 제네릭의 익스텐션

제네릭의 익스텐션을 선언할 때 where절을 포함시킬 수 있음

다음은 isTop함수를 익스텐션으로 추가하면서 이 함수가 추가되는 Stack은 반드시

Equatable 프로토콜을 준수해야 하는 제한을 부여한 코드

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."

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

where 절을 프로토콜 확장과 함께 사용 가능 함.

extension Container where Item: Equatable {
    func startsWith(_ item: Item) -> Bool {
        return count >= 1 && self[0] == item
    }
}

위의 예제는 Container의 Item이 Equatable 프로토콜을 준수해야 하는 제약을 추가한 예제임

startWith함수의 인자인 Item은 Container의 특정 아이템이 입력한 Item 으로 시작하는지 비교하기 위해서는

Container의 첫 아이템이 입력한 Item과 같은지 비교해야 하므로 프로토콜을 준수해야 함

if [9, 9, 9].startsWith(42) {
    print("Starts with 42.")
} else {
    print("Starts with something else.")
}
// Prints "Starts with something else."

제네릭의 연관 타입에 where절 적용

연관 타입에도 where절을 적용해 제한을 둘 수 있음

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
}

다른 프로토콜을 상속하는 프로토콜에도 where절로 조건을 부여할 수 있음

protocol ComparableContainer: Container where Item: Comparable { }

제네릭 서브스크립트

제네릭의 서브스크립트에도 조건을 걸 수 있음

아래 예제

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.Iterator.Element == Int