본문 바로가기

Swift/꼬꼬무

Generic Where Clauses (제네릭 where 절)

Generic Where Clauses (제네릭 where 절)

타입제약을 통해서 제네릭 함수, 서브 스크립트 또는 타입과 연관된 타입 파라미터에 대한 요구사항을 정의할 수 있습니다.

이 중에서 연관된 타입에도 요구사항을 정의하는 것이 유용할 때가 있습니다.

 

연관된 타입에는 어떻게 요구사항을 정의할 수 있을까요??

바로! 제네릭 where 절을 이용하여 정의할 수 있습니다.

 

제네릭 where 절을 이용하면 연관된 타입이 특정 프로토콜을 준수해야 한다던지, 특정 타입 파라미터와 동일해야 한다고 요구할 수 있습니다.

 

제네릭 where 절은 where 키워드로 시작하고 이어서 제약이 작성됩니다.

타입 또는 함수의 본문에 중괄호 바로 전에 제네릭 where 절을 작성해야 합니다.

 

아래 예제에서는

allItemsMatch 라는 제네릭 함수 하나를 정의합니다. 이 함수는 2개의 컨테이너 인스턴스의 각 Item이 같은 순서인지 확인하여 같으면 true, 아니면 false를 반환합니다.

비교할 2개의 컨테이너 인스턴스는 같은 타입일 필요는 없지만, 같은 타입의 Item을 가지고 있어야 합니다.

 

이 요구사항을 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
}

 

이 함수는 someContainter와 anotherContainer라는 2개의 인자를 가집니다.

각각 C1, C2 라는 Container프로토콜을 준수하는 타입입니다. C1과 C2는 모두 함수가 호출될 때 결정되는 2개의 타입 파라미터 입니다.

 

함수의 2가지 타입 파라미터에 대한 요구사항

  • C1 은 Container 프로토콜을 준수해야 합니다. (C1: Container)
  • C2 는 Container 프로토콜을 준수해야 합니다. (C2: Container)
  • C1의 Item과 C2의 Item은 동일해야 합니다. (C1.Item == C2.Item)
  • C1의 Item은 Equatable을 준수해야 합니다.

 

첫번째와 두번째 요구사항은 타입 파리미터 리스트에 정의되었고, 세번째와 네번째 요구사항은 제네릭 where 절을 이용하여 정의되었습니다.

 

요구사항의 의미는

  • someContainer는 C1 타입의 컨테이너 입니다.
  • anotherContainer는 C2 타입의 컨테이너 입니다.
  • someContainer와 anotherContainer는 같은 타입의 Item을 가집니다.
  • someContainer 안의 항목은 Equtable을 준수하기 때문에 비동등 연산자(!=)를 사용할 수 있습니다.

 

추가로 세번째 네번째 요구사항을 통해서 anotherContainer도 someContainer와 같이 Equtable을 준수함을 의미합니다.

이 요구사항을 통해 allItemsMatch(:) 함수는 컨테이너의 타입이 다른 경우에도 비교할 수 있습니다.

 

 

이 함수의 동작 순서는

 

먼저 두 컨테이너의 갯수가 같은지 판단해서 다르면 false를 반환합니다.

갯수가 같다면 다음으로 넘어가서 for loop를 통해서 두 컨테이너의 count 만큼 반복합니다.

서브스크립트를 통해 반환되는 각 컨테이너의 Item이 같은지 판단해서 다르면 false를 반환하고 모두 같아서 통과하면 true를 반환합니다.

 

다음은 함수의 실제 동작 입니다.

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

 

위 예제에서 stackOfString 라는 String 타입을 저장하는 Stack 인스턴스를 생성합니다.

또 String 타입의 Array인 arrayOfStrings 인스턴스도 생성합니다.

 

두 인스턴스는 다른 타입이지만 모두 Container 프로토콜을 준수하고 같은 타입의 값을 가집니다.

 

따라서 이 2개의 컨테이너를 인수로 allItemsMatch(_:_:) 함수를 호출합니다.

위의 예제에서 allItemsMatch(_:_:) 함수는 두 컨테이너의 모든 항목이 일치하기 때문에 “All items match.”를 출력합니다.

 

Where 절이 있는 제네릭 확장 (Extensions with a Generic Where Clause)

 

확장의 부분으로 제네릭 where 절을 사용할 수도 있습니다.

아래의 예제는 이전 예제의 제네릭 Stack 구조체에 isTop(_:) 메서드를 추가하기 위해 확장합니다.

extension Stack where Element: Equatable {
    func isTop(_ item: Element) -> Bool {
        guard let topItem = items.last else {
            return false
        }
        return topItem == item
    }
}

 

새로운 isTop(_:) 메서드는 먼저 스택이 비어 있는지 확인하고 그 다음에 주어진 항목과 스택의 가장 상단의 항목을 비교합니다.

이때 제네릭 where 절이 없이 시도한다면 문제가 발생합니다.

 

isTop(_:) 에서 동등연산자 (==) 를 사용하지만 Stack 의 정의는 항목의 동등한지 요구하지 않으므로 == 연산자를 사용한 결과는 컴파일 에러가 발생합니다.

 

제네릭 where 절을 사용하여 확장에 새로운 요구사항을 추가해서 스택에 Item이 Equtable을 준수할 때만 isTop(_:) 메서드를 추가합니다.

 

 

다음은 isTop(_:) 메서드가 어떻게 동작하는지 보여줍니다:

if stackOfStrings.isTop("tres") {
    print("Top element is tres.")
} else {
    print("Top element is something else.")
}
// Prints "Top element is tres."

 

동등성이 가능하지 않은 요소를 가진 스택에서 isTop(_:) 메서드를 호출하면 컴파일 에러가 발생합니다.

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

 

Stack의 제네릭 타입으로 사용된 NotEquatable 구조체는 Equatable을 준수하지 않아서 에러가 발생합니다.

프로토콜 확장에도 제네릭 where 절을 사용할 수 있습니다.

 

 

아래의 예제는 이전 예제의 Container 프로토콜에 startsWith(_:) 메서드를 추가하기 위해 확장합니다.

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

 

startsWith(_:) 메서드는 먼저 컨테이너가 하나 이상의 항목을 가지고 있으면서 컨테이너의 첫번째 항목이 주어진 항목과 일치하는지 확인합니다.

 

 

새로운 startsWith(_:) 메서드는 컨테이너의 항목이 Equtable을 준수한다면 위에서 사용된 스택과 배열을 포함하여 Container 프로토콜을 준수하는 모든 타입에서 사용될 수 있습니다.

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

 

위의 예제에서 제네릭 where 절은 Item 은 프로토콜을 준수해야 하지만 Item 이 특정 타입을 요구하도록 제네릭 where 절을 작성할 수도 있습니다.

 

예를 들어

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 타입이 Double 인 컨테이너에 average() 메서드를 추가합니다.

컨테이너의 항목을 더하고 컨테이너의 수로 나누어 평균을 계산합니다.

부동 소수점 나누기가 가능하기 위해 카운트를 Int 에서 Double 로 명시적으로 변환합니다.

 

다른 곳에서 작성하는 제네릭 where 절과 마찬가지로 확장의 부분인 제네릭 where 절에 콤마로 구분하여 여러 요구사항을 포함할 수 있습니다.

 

Contextual Where Clauses (상황별 Where 절)

 

고유한 제네릭 타입 제약조건이 없는 제네릭 타입을 사용중인 경우에 상황에 맞게 제네릭 where 절을 사용할 수 있습니다.

예를 들어 제네릭 타입의 서브 스크립트나 제네릭 타입에 확장에 대한 확장 메서드에 제네릭 where 절을 작성할 수 있습니다.

 

Container 구조체는 제네릭 이고 아래의 예제에서 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"

 

이 예제는 항목이 정수이면 Container 에 average() 메서드를 추가하고 항목이 Equtable을 준수하면 endsWith(_:)메서드를 추가합니다.

 

두 함수 모두 Container 의 기존 선언에 제네릭 Item 타입 파라미터에 대해 타입 제약조건을 추가한 제네릭 where 절을 포함합니다.

만약 상황별 where 절 사용없이 코드를 작성하고 싶다면 2개의 확장을 작성하고 각각에 제네릭 where 절을 추가하면 됩니다.

 

위의 예제와 아래의 예제는 같은 동작을 가집니다.

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

위 예제에서 상황별 where절을 이용하면 각 메서드의 제네릭 where 절은 각각 메서드를 사용하기 위해 충족해야 할 요구사항을 명시하기 때문에 하나의 확장으로 사용할 수 있고

상황별 where절 없이 구현하기 위해서는 2개의 확장을 통해 각각의 요구사항을 작성해야 합니다.

 

Associated Types with a Generic Where Clause (제네릭 Where 절이 있는 연관된 타입)

 

연관된 타입에도 제네릭 where 절을 포함할 수 있습니다.

표준 라이브러리에서 Sequence 프로토콜의 Interator를 Container 프로토콜에 추가해 봅시다.

 

작성 방법은 아래와 같습니다

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
}

 

Iterator 의 제네릭 where 절은 Iterator의 타입에 상관없이 Iterator의 요소와 컨테이너의 항목과 동일한 타입이어야 합니다.

makeIterator() 함수는 컨테이너의 Iterator에 접근을 제공합니다.

 

다른 프로토콜에서 상속하는 프로토콜의 경우 프로토콜 선언에 제네릭 where 절을 포함하여 상속된 연관 타입에 제약조건을 추가합니다.

 

예를 들어 다음의 코드는 Item 이 Comparable 을 준수하도록 요구하는 ComparableContainer 프로토콜을 선언합니다:

protocol ComparableContainer: Container where Item: Comparable { }

'Swift > 꼬꼬무' 카테고리의 다른 글

Opaque and Boxed Types  (0) 2023.08.09
Generic Subscripts (제네릭 서브 스크립트)  (0) 2023.08.07
Associated Types (연관된 타입)  (0) 2023.08.07
Generics - Type Constraints (타입 제약)  (0) 2023.07.26
Generics - Generic Types  (0) 2023.07.26