본문 바로가기
iOS/Swift

Opaque Type

by 바등쪼 2023. 2. 11.
Opaque Types - The Swift Programming Language (Swift 5.7)
A function or method with an opaque return type hides its return value's type information. Instead of providing a concrete type as the function's return type, the return value is described in terms of the protocols it supports.
https://docs.swift.org/swift-book/LanguageGuide/OpaqueTypes.html
Understanding the "some" and "any" keywords in Swift 5.7 - Swift Senpai
The some and any keywords are not new in Swift. The some keyword was introduced in Swift 5.1 whereas the any keyword was introduced in Swift 5.6. In Swift 5.7, Apple makes another great improvement on both of these keywords. We can now use both of these keywords in the function's parameter position!
https://swiftsenpai.com/swift/understanding-some-and-any/
Embrace Swift generics - WWDC22 - Videos - Apple Developer
Generics are a fundamental tool for writing abstract code in Swift. Learn how you can identify opportunities for abstraction as your code...
https://developer.apple.com/videos/play/wwdc2022/110352/

Opaque Type (불투명한 타입)

  • 불투명한 반환 타입이 있는 함수 또는 메서드는 반환값의 타입 정보를 가린다.
  • 함수의 반환 타입으로 구체적인 타입을 제공하는 대신에 반환값은 지원되는 프로토콜 측면에서 설명된다.
  • 반환값의 기본 타입이 비공개로 유지될 수 있으므로 모듈과 모듈을 호출하는 코드 사이의 경계에서 타입 정보를 숨기는 것이 유용하다.
  • 타입이 프로토콜 타입인 값을 반환하는 것과 달리 불투명한 타입은 타입 정체성을 보존합니다—컴파일러는 타입 정보에 접근할 수 있지만 모듈의 클라이언트는 그럴 수 없다.
  • 제네릭과는 반대

왜 사용하는가?

  • 공식 문서를 읽으면서 느꼈던 점은 Opaque Type과 Protocol이 상당히 유사하다는 점이었다. 공식 문서에서 제시하는 둘의 차이점은 타입이 프로토콜 타입인 값을 반환하는 것과 달리 불투명한 타입은 타입 정체성을 보존한다는 점이었지만 솔직히 와닿지 않았다.
  • 다른 자료들을 찾아보면서 다음과 같은 정리를 할 수 있었다.

Opaque Type을 사용하면 다음과 같은 에러 문구를 더 이상 안 볼 수 있다.

protocol can only be used as a generic constraint because it has Self or associated type requirement

protocol Vehicle {

    var name: String { get }

    associatedtype FuelType
    func fillGasTank(with fuel: FuelType)
}
  • associatedtype (연료)를 사용하는 protocol을 생성
struct Car: Vehicle {

    let name = "car"

    func fillGasTank(with fuel: Gasoline) {
        print("Fill \(name) with \(fuel.name)")
    }
}

struct Bus: Vehicle {

    let name = "bus"

    func fillGasTank(with fuel: Diesel) {
        print("Fill \(name) with \(fuel.name)")
    }
}

struct Gasoline {
    let name = "gasoline"
}

struct Diesel {
    let name = "diesel"
}
  • Car와 Bus 구조체가 Vehicle을 채택하도록 하고 각각 Gasoline과 Diesel을 사용하도록 한다. (associatedType을 사용한 이유)

다음과 같이 Wash 함수를 다양하게 작성 가능

// The following 3 function signatures are identical.

func wash<T: Vehicle>(_ vehicle: T) {
    // Wash the given vehicle
}

func wash<T>(_ vehicle: T) where T: Vehicle {
    // Wash the given vehicle
}

func wash(_ vehicle: some Vehicle)  {
    // Wash the given vehicle
}

내가 이해한 Some

  • some
    • 로토콜과 함께 사용되어 특정 프로토콜을 준수하는 것을 나타내는 불투명한 유형을 만듭니다. 함수의 매개 변수 위치에 사용되는 경우 함수가 특정 프로토콜을 준수하는 특정 유형을 수용한다는 것을 의미합니다.
    • 즉, 하나의 프로토콜을 준수하는 특정한 하나의 타입으로만 한정한다.
    • 예시
      var myCar: some Vehicle = Car()
      myCar = Bus() // 🔴 Compile error: Cannot assign value of type 'Bus' to type 'some Vehicle'
      
      var myCar: some Vehicle = Car()
      myCar = Car() // 🔴 Compile error: Cannot assign value of type 'Car' to type 'some Vehicle'
      
      
      var myCar1: some Vehicle = Car()
      var myCar2: some Vehicle = Car()
      myCar2 = myCar1 // 🔴 Compile error: Cannot assign value of type 'some Vehicle' (type of 'myCar1') to type 'some Vehicle' (type of 'myCar2')
      // ✅ No compile error
      let vehicles: [some Vehicle] = [
          Car(),
          Car(),
          Car(),
      ]
      
      // 🔴 Compile error: Cannot convert value of type 'Bus' to expected element type 'Car'
      let vehicles: [some Vehicle] = [
          Car(),
          Car(),
          Bus(),
      ]
      // 여기서 some Vehicle로 제한을 하고 Car라는 특정한 하나의 타입으로 컴파일 시에 확정이 되었기 때문에
      // Bus는 어레이에 들어갈 수 없다.
      // ✅ No compile error
      func createSomeVehicle() -> some Vehicle {
          return Car()
      }
      
      
      // 🔴 Compile error: Function declares an opaque return type 'some Vehicle', but the return statements in its body do not have matching underlying types
      func createSomeVehicle(isPublicTransport: Bool) -> some Vehicle {
          if isPublicTransport {
              return Bus()
          } else {
              return Car()
          }
      }
      
      // 같은 원리로 함수의 리턴 타입으로 some을 사용했을 때 하나의 구체 타입만 리턴 할 수 있다!!

내가 이해한 any

  • Swift 5.7 부터는 다음과 같은 코드에서 에러 발생
    let myCar: Vehicle = Car() // 🔴 Compile error in Swift 5.7: Use of protocol 'Vehicle' as a type must be written 'any Vehicle'
    
    // 🔴 Compile error in Swift 5.7: Use of protocol 'Vehicle' as a type must be written 'any Vehicle' 
    func wash(_ vehicle: Vehicle)  {
        // Wash the given vehicle
    }
  • any를 사용하여 에러 해결 가능
    let myCar: any Vehicle = Car() // ✅ No compile error in Swift 5.7
    
    // ✅ No compile error in Swift 5.7
    func wash(_ vehicle: any Vehicle)  {
        // Wash the given vehicle
    }
  • some 과 any의 차이점
    • 앞서 some이 특정한 구체적 타입으로 한정하도록 하는 불투명 타입이었다면, any는 위의 그림과 같이 상자로 감싸 놓아서 특정 1개의 구체 타입이 아니라 Vehicle을 만족하는 다양한 타입들로 범위를 확장할 수 있다.
      // ✅ No compile error when changing the underlying data type
      var myCar: any Vehicle = Car()
      myCar = Bus()
      myCar = Car()
      
      // ✅ No compile error when returning different kind of concrete type 
      func createAnyVehicle(isPublicTransport: Bool) -> any Vehicle {
          if isPublicTransport {
              return Bus()
          } else {
              return Car()
          }
      }
      
      // 🔴 Compile error in Swift 5.6: protocol 'Vehicle' can only be used as a generic constraint because it has Self or associated type requirements
      // ✅ No compile error in Swift 5.7
      let vehicles: [any Vehicle] = [
          Car(),
          Car(),
          Bus(),
      ]
  • any의 한계
    • == operator 사용 불가
      // Conform `Vehicle` protocol to `Equatable`
      protocol Vehicle: Equatable {
      
          var name: String { get }
      
          associatedtype FuelType
          func fillGasTank(with fuel: FuelType)
      }
      
      
      let myCar1 = createAnyVehicle(isPublicTransport: false)
      let myCar2 = createAnyVehicle(isPublicTransport: false)
      let isSameVehicle = myCar1 == myCar2 // 🔴 Compile error: Binary operator '==' cannot be applied to two 'any Vehicle' operands
      
      
      let myCar1 = createSomeVehicle()
      let myCar2 = createSomeVehicle()
      let isSameVehicle = myCar1 == myCar2 // ✅ No compile error
      • 이는 any를 사용했을 때(existential type) 컴파일러는 box 안에 저장된 구체적인 타입(underlying type)을 알 수 없기 때문이다. 즉, 컴파일러는 box만 알 수 있다.

정리

  • Opaque Type은 일종의 추상화
    • 제네릭과 Protocol도 추상화이지만 함수를 만들 때 필요한 세부 정보를 노출하는 측면(제네릭)에서 Opaque Type이 더 깊은 추상화라고 생각된다.
    • associatedType이 있을 때 Opaque Type의 필요성이 크게 느껴진다.
      • Protocol 자체로서의 사용은 return value의 연산이 불가능하다는 단점과 해당 프로토콜을 채택하는 모든 구현체를 허용한다는 점에서 Type 안정성이 떨어진다.
      • Protocol이 associatedType또는 Self를 가진다면 함수의 return Type으로서 Protocol 단독으로 활용이 불가능 하지만 Opaque Type은 가능하다.
      • Protocol를 파라미터 타입으로 단독 사용하면 argument의 associatedType은 함수 내부에서 사용하지(알지) 못하지만 some을 사용한다면 타입을 특정하고 type relationship을 보장하기 때문에 해당 Protocol의 associatedType 또한 컴파일러가 파악하여 사용할 수 있다.
  • 웬만하면 any보다는 some을 사용하자 (더 엄격한 타입) → 필요 시 some을 any로 바꾼다.

궁금한 점

  • generic으로 작성된 코드를 전부 opaque type으로 대체가 가능한가?


Uploaded by N2T

'iOS > Swift' 카테고리의 다른 글

Understanding Swift Performance  (2) 2023.03.13
Explore structured concurrency in Swift  (0) 2023.02.11
Meet AsyncSequence  (0) 2023.02.11
Use async/await with URLSession  (0) 2023.02.11
Meet async/await in Swift  (0) 2023.02.11

댓글