이 포스트는 야곰님의 Swift 프로그래밍 2판을 보고 스스로 공부한 내용을 정리한 포스트 입니다.



참고 자료



타입 제약(Type Constraints)

  • 제네릭 기능의 타입 매개변수는 실제 사용 시 타입의 제약 없이 사용할 수 있다.

  • 그러나 종종 제네릭 함수가 처리해야 할 기능이 특정 타입에 한정되어야만 처리할 수 있다던가, 제네릭 타입을 특정 프로토콜을 따르는 타입만 사용할 수 있도록 제약을 두어야 하는 상황이 발생할 수 있다.

    • 타입 제약은 타입 매개변수가 가져야 할 제약사항을 지정할 수 있는 방법이다.

      • 예를 들어 타입 매개변수 자리에 사용할 실제 타입이 특정 클래스를 상속 받는 타입이어야 한다든지, 특정 프로토콜을 준수하는 타입이어야 한다는 등의 제약을 줄 수 있다는 뜻이다.
    • 타입 제약(Type Constraints)은 클래스 타입 또는 프로토콜로만 줄 수 있다.

      • 즉, 열거형, 구조체 등의 타입은 타입 제약의 타입으로 사용할 수 없다.

      • 예를 들어 자주 사용하는 제네릭 타입인 Dictionary의 키는 Hashable 프로토콜을 준수하는 타입만 사용할 수 있다.

        // Dictionary Type 정의
              
        public struct Dictionary<Key: Hashable, Value> : Collection, 
            ExpressibleByDictionaryLiteral { /*...*?}
      
  • Dictionary Type 정의 코드를 살펴보면 Dictionary의 두 타입 매개변수는 Key 와 Value 이다.

    • 그런데 Key 뒤에 콜론(:)을 붙인 다음에 Hashable이라고 명시되어 있다.

      • 이는 타입 매개변수인 Key 타입은 Hashable 프로토콜을 준수해야 한다는 뜻이다.

      • 즉, Key로 사용할 수 있는 타입 Hashable 프로토콜을 준수하는 타입이어야 한다는 것이다.

      • Hashable은 스위프트 표준 라이브러리에 정의되어 있는 프로토콜이며 스위프트의 기본 타입(String, Int, Bool 등등)은 모두 Hashable 프로토콜을 준수한다.

  • 제네릭 타입에 제약을 주고 싶으면 타입 매개변수 뒤에 콜론을 붙인 후 원하는 클래스 타입 또는 프로토콜을 명시하면 된다.

// 제네릭 타입 제약

func swapTwoValuse<T: BinaryInteger>(_ a: inout T, _ b: inout T) {
    // 함수 구현
}

func Stack<Element: Hashable> {
    // 구조체 구현
}    
  • 위 코드는 기존(확장 시리즈 중 제네릭 관련 post 참고)에 타입 제약 없이 구현해보았던 swapTwoValues(::) 함수와 Stack 구조체에 타입 제약을 준 것이다.

    • 코드에서 보다시피 타입 매개변수 뒤에 콜론을 붙이고 제약조건으로 주어질 타입을 명시해주면 된다.

    • 여러 제약을 추가하고 싶다면 콤마로 구분해주는 것이 아니라 where 절을 사용할 수 있다.

    • Stack 구조체의 Element 타입 매개변수의 타입을 Hashable 프로토콜을 준수하는 타입으로 제약을 준다면, Any 타입은 Hashable 프로토콜을 준수하지 않기 때문에 기존에 사용했던(확장 시리즈 중 제네릭 관련 example code 참고) Any 타입은 사용할 수 없다.

// 제네릭 타입 제약 추가

func swapTwoValues<T: BinaryInteger>(_ a: inout T, _ b: inout T) where 
    T: FloatingPoint, T: Equatable {
    // 함수 구현
}    
  • 위의 코드는 현재 Post에서 설명하는 내용들의 의도와는 조금 다른 내용이다.

  • 상식적으로 생각한다면 T는 정수도, 실수도 들어올 수 있게 구현하고 싶지만, 그렇게 하려면 함수를 중복 정의해주어야 한다.

    • 다만 where를 사용하여 제약을 추가할 수는 있다.
  • 즉, 위 코드의 T는 BinaryInterger 프로토콜을 준수하고, FloatingPoint 프로토콜도 준수하며 Equatable 프로토콜도 준수하는 타입만 사용할 수 있다.

    • 개발자가 특별히 사용자정의 타입을 만들어 구현하지 않는 한, 저 조건에 맞는 기본 타입은 없다.
// substractTwoValue 함수의 잘못된 구현

func substractTwoValue<T>(_ a: T, _ b: T) -> T {
    return a - b
}
  • 위의 substractTwoValue(::) 함수는 T라는 타입 매개변수가 있는 간단한 제네릭 함수이다.

    • 그런데 이 함수에는 중대한 실수가 있는데, 뺄셈을 하려면 뺄셈 연산자를 사용할 수 있는 타입이여야 연산이 가능하다는 한계이다.

    • 즉, T가 실제로 받아들일 수 있는 타입은 뺄셈 연산자를 사용할 수 있는 타입이어야 한다.

// substractTwoValue 함수의 구현

func substractTwoValue<T: Binary Integer>(_ a: T, _ b: T) -> T {
    return a - b
}
  • 위의 코드에서 타입 매개변수인 T의 타입을 BinaryInteger 프로토콜을 준수하는 타입으로 한정해두니 뺄셈 연산이 가능하게 되었다.

    • 이처럼 타입 제약은 함수 내부에서 실행해야 할 연산에 따라 적절한 타입을 전달받을 수 있도록 제약을 둘 수 있다.

// 프로토콜과 제네릭을 이용한 전위 연산자 구현과 사용

prefix operator **

prefix func **<T: BinaryInteger> (value: T) -> T {
    return value * value
}
  • 위 코드의 ** 연산자를 보면 타입 매개변수(T)가 BinaryInteger라는 특정 프로토콜을 준수할 때 제네릭 함수인 연산자를 사용할 수 있도록 타입을 제약해 주었다.
// makeDictionaryWithTwoValue 함수의 구현

func makeDictionaryWithTwoValue<Key: Hashable, Value>(key: Key, value: 
    Value) -> Dictionary<Key, Value> {
    let dictionary: Dictionary<Key, Value> = [key:value]
    return dictionary
}
  • 위 코드의 makeDictionaryWithTwoValue(::) 함수는 Key와 Value라는 타입 매개변수가 있다.

    • 두 타입 매개변수의 제약조건이 다르다는 것을 알 수 있다.

    • 이처럼 타입 매개변수마다 제약조건을 달리해서 구현해줄 수도 있다.