iOS/Swift

[Swift] Opaque Types 찍어먹기

Joo-Topia 2022. 2. 16. 23:35

doc: https://docs.swift.org/swift-book/LanguageGuide/OpaqueTypes.html

Opaque Types

  • Opaque Types을 사용한 fuction이나 method는 리턴되는 값의 타입에 대한 정보를 숨기는 타입으로 반환된다.
  • 함수의 반환 타입으로 구체적인 타입을 제공하지 않고, 반환 값이 채택하는 프로토콜로 묘사된다.
  • 프로토콜 타입을 반환하는 것과 다르게 Opaque Types은 타입 정보를 보존한다.
    => 컴파일러는 타입 정보에 접근할 수 있지만, 모듈의 클라이언트는 그렇지 않다.

Opaque Type을 사용하지 않을 시

protocol Weapon {
    func attack()
}

struct NoTypeWeapon: Weapon {
    func attack() {
        // Attck With No Type
    }
}

let noTypeWeapon = NoTypeWeapon()

스크린샷 2022-02-16 오후 10 08 26

Weapon 프로토콜을 정의하고 테스트에 사용할 무타입 무기를 만든다.
아직까지 이상한 점은 없다
이어서 Weapon을 컨펌하는 속성 타입 무기를 만들었다.

struct IceTypeWeapon<T: Weapon>: Weapon {
    var weapon: T
    func attack() {
        // Attck With Ice Type
    }
}

struct FireTypeWeapon<T: Weapon>: Weapon {
    var weapon: T
    func attack() {
        // Attck With Fire Type
    }
}

let iceTypeWeapon = IceTypeWeapon(weapon: noTypeWeapon)
let fireTypeWeapon = FireTypeWeapon(weapon: noTypeWeapon)

스크린샷 2022-02-16 오후 10 16 02스크린샷 2022-02-16 오후 10 16 22

타입이...아주 보기 싫게 나온다..
(IceTypeWeapon이면 좋겠지만 IceTypeWeapon이라고 나옴)
흠...

마지막으로 Elementweapon을 두개 합친 듀얼 타입 무기를 만들었다.

struct DualTypeWeapon<T: Weapon, U: Weapon>: Weapon {
    var firstWeapon: T
    var secondWeapon: U
    func attack() {
        // Attck With Dual Type
    }
}

let dualTypeWeapon = DualTypeWeapon(
    firstWeapon: iceTypeWeapon,
    secondWeapon: fireTypeWeapon
)

스크린샷 2022-02-16 오후 10 18 12

오마이갓... 이게 무슨 타입인지.. 실제 개발할때 저런 타입을 마주한다면 상당히 화가날 것 같다...

Opaque Type을 사용 시

어떤 Weapon이든 사용처에서는 attack 기능이 중요하지, 어떤 타입인지는 저렇게 자세하게 알 필요가 없을테니
Opaque Type을 사용하여 디테일한 정보를 숨기고 Weapon으로 접근할 수 있게 해보자.

func iceType<T: Weapon>(_ weapon: T) -> some Weapon {
    return IceTypeWeapon(weapon: weapon)
}
func fireType<T: Weapon>(_ weapon: T) -> some Weapon {
    return FireTypeWeapon(weapon: weapon)
}

func makeDualTypeWeapon<T: Weapon, U: Weapon>(first: T, second: U) -> some Weapon {
    return DualTypeWeapon(firstWeapon: first, secondWeapon: second)
}

let iceWeapon = iceType(noTypeWeapon)
let fireWeapon = fireType(noTypeWeapon)
let opaqueTypeWeapon = makeDualTypeWeapon(
    first: iceTypeWeapon,
    second: fireTypeWeapon
)  
opaqueTypeWeapon.attack()

스크린샷 2022-02-16 오후 10 20 56

generic과 opaque type을 사용하여 반환 타입을 랩핑하였다.
위에서 만든 dualTypeWeapon와 동일한 DualTypeWeapon 이지만, opaqueTypeWeapon는 불필요한 타입 정보를 제거하면서 Weapon 타입에 대한 정보만 남기고, Weapon의 기능을 사용할 수 있게 되었다.

Protocol TypeOpaque Type 차이점

(refer을 나타내다로 해석했는데.. 묘사한다? 라는 표현이 더 어울리려나...)

  • Opaque Type을 반환하는 것은 Protocol Type를 반환하는 것과 매우 유사하지만, 타입의 정보를 보존하는지 여부에 따라 다르다.
  • Opaque Type은 함수 호출자가 구체적으로 어떤 타입을 볼 수 없지만, 하나의 특정 타입을 나타낸다.
    (some Weapon 을 반환하면 호출자가 구체적으로 어떻게 구성된 Weapon인지는 모르지만, Weapon이라는 특정 타입으로 사용할 수 있다.)
  • Protocol Type은 해당 protocol을 준수하는 모든 타입을 나타낼 수 있다.
  • 일반적으로 Protocol Type을 사용하면 기본 타입에 대해 더 많은 유연성을 제공하는 반면, Opaque Type을 사용하면 기본 타입에 대해 더 강력한 보장을 할 수 있다.
  • Protocol Type을 반환하는것과 다르게 모든 반환 값의 유형이 동일해야한다

예제 코드에서 함수의 반환 타입을 Opaque Type 에서 Protocol Type로 변경해보자.
우선 테스트를 위해 Weapon protocol 에 == 를 구현해두자.

extension Weapon {
    static func == (lhs: Self, rhs: Self) -> Bool {
        // some logic
        return true
    }
}

Opaque Type 사용

func iceType<T: Weapon>(_ weapon: T) -> some Weapon {
    return IceTypeWeapon(weapon: weapon)
}
func fireType<T: Weapon>(_ weapon: T) -> some Weapon {
    return FireTypeWeapon(weapon: weapon)
}

let firstIceWeapon = iceType(noTypeWeapon)  
let secondIceWeapon = iceType(noTypeWeapon)  
print(firstIceWeapon == secondIceWeapon)  
// result: true  
let firstWeapon = iceType(noTypeWeapon)  
let secondWeapon = fireType(firstWeapon)  
// some Weapon 으로 반환 받은 값을 또 다른 Weapon 으로 넘길 수 있음.

Protocol Type 사용

func iceType<T: Weapon>(_ weapon: T) -> Weapon {
    return IceTypeWeapon(weapon: weapon)
}
func fireType<T: Weapon>(_ weapon: T) -> Weapon {
    return FireTypeWeapon(weapon: weapon)
}

let firstIceWeapon = iceType(noTypeWeapon)
let secondIceWeapon = iceType(noTypeWeapon)
secondIceWeapon.attack() // 프로토콜의 인터페이스는 사용 가능
print(firstIceWeapon == secondIceWeapon)
// error 발생, Binary operator '==' cannot be applied to two 'Weapon' operands (1)
let firstWeapon = iceType(noTypeWeapon)
let secondWeapon = fireType(firstWeapon)
// error 발생, Protocol 'Weapon' as a type cannot conform to the protocol itself (2)

(1) Opaque Type 과 다르게 Protocol Type으로 반환하게 되면, 프로토콜의 인터페이스는 사용 가능하지만, 해당 타입에 대한 정보를 이용할 수가 없다.
(2) 프로토콜 타입으로 반환 받은 프로토콜 타입의 'Weapon'은 해당 프로토콜을 채택하지 않은 것으로 간주되어 에러 발생