티스토리 뷰

🍏/Swift

[Swift] Opaque Type vs Protocol Type

eungding 2022. 1. 23. 19:08
반응형

 

Swift Docs > Opaque Types 에 나오는 내용입니다! 


 

Shape 라는 프로토콜이 있다고 해봅시다! 

protocol Shape {
    func draw() -> String
}

Triangle, Square처럼 이 프로토콜을 컨펌하는 타입을 만들 수 있습니다. 

struct Triangle: Shape {
    var size: Int
    func draw() -> String {
        var result: [String] = []
        for length in 1...size {
            result.append(String(repeating: "*", count: length))
        }
        return result.joined(separator: "\n")
    }
}

let triangle = Triangle(size: 3)
print(triangle.draw())
// 출력결과
*
**
***
struct Square: Shape {
    var size: Int
    func draw() -> String {
        let line = String(repeating: "*", count: size)
        let result = Array<String>(repeating: line, count: size)
        return result.joined(separator: "\n")
    }
}

let square = Square(size: 3)
print(square.draw())
// 출력결과
***
***
***

 

 

그리고 Shape을 뒤집어주는 FlippedShape가 있다고 해볼게요.

FlippedShape 도 Shape 프로토콜을 컨펌하고 있습니다. 

struct FlippedShape<T: Shape>: Shape {
    var shape: T
    func draw() -> String {
        let lines = shape.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")
    }
}

let triangle: Triangle = Triangle(size: 3)
print(triangle.draw())
// 출력결과 
*
**
***

let flippedTriangle: FlippedShape<Triangle> = FlippedShape(shape: triangle)
print(flippedTriangle.draw())
// 출력결과
***
**
*

 

 

[1] Opaque Type 이란?

Swift 5.1에 나온 Opaque Type은 말그대로 불투명한 Type 을 말합니다. 

 

위의 예제에서 flippedTriangle의 타입은 FlippedShape<Triangle> 으로 세부 타입들이 모두 노출되고 있습니다. 

let flippedTriangle: FlippedShape<Triangle> = FlippedShape(shape: triangle)

 

반면 이렇게 Opaque Type을 사용하게 해주면

Shape 라는 프로토콜 타입만 노출할 수 있습니다. 

func flip<T: Shape>(_ shape: T) -> some Shape {
    return FlippedShape(shape: shape)
}

let flippedTriangle: Shape = flip(triangle)

 

 

이렇게 타입 정보를 감추는 것은  모듈과 모듈을 호출하는 코드 사이의 경계에서 유용하게 사용할 수 있습니다. 

 

 

이 설명을 보면 이런 의문이 찾아옵니다. 

"Opaque Type 말고 Protocol Type을 리턴해도 세부타입을 감출 수 있는데, 왜 Opaque Type 을 쓰지?"

func flip<T: Shape>(_ shape: T) -> Shape {
    return FlippedShape(shape: shape)
}

let flippedTriangle: Shape = flip(triangle)

 

[2] Opaque Type 과 Protocol Type의 차이

flip 함수의 두가지 버전을 다시 살펴보겠습니다. 

 

1.  리턴 타입이 Opaque Type

func flip<T: Shape>(_ shape: T) -> some Shape {
    return FlippedShape(shape: shape)
}

 

2. 리턴 타입이 Protocol Type

func flip<T: Shape>(_ shape: T) -> Shape {
    return FlippedShape(shape: shape)
}

 

구분을 위해, 아래서 부터 opaqueFlip, protocolFlip 이라고 부를게요! 

func opaqueFlip<T: Shape>(_ shape: T) -> some Shape {
    return FlippedShape(shape: shape)
}

func protocolFlip<T: Shape>(_ shape: T) -> Shape {
    return FlippedShape(shape: shape)
}

 

이 두가지는 많이 비슷해보인데, 어떤 차이가 있을 까요?

아래 문장만 기억하면 됩니다. 


Unlike returning a value whose type is a protocol type, opaque types preserve type identity

—the compiler has access to the type information, but clients of the module don’t.


 

[2.1] 타입에 대한 강력한 보장 (single type) vs 유연성 (multiple types)

protocol type과 달리 opaque type은 type identity 를 보존합니다.

- opaque type은 하나의  specific type 만 참조합니다 (비록 function의 caller는  어떤 타입인지 볼 수 없지만)

- protocol type은 해당 protocol을 conform하는 어떤 타입이든 참조할 수 있습니다. 

 

예제를 살펴봅시다.

 

1. Opaque Type

func opaqueFlip<T: Shape>(_ shape: T) -> some Shape {
    if shape is Square {
        return shape // 컴파일 에러! 
    }
    
    return FlippedShape(shape: shape)
}

 

2. Protocol Type

func protocolFlip<T: Shape>(_ shape: T) -> Shape {
    if shape is Square {
        return shape
    }
    
    return FlippedShape(shape: shape)
}

Protocol Type은 Shape protocol을 conform하고 있기만 하면 mulitple type (Square, FlippedShape 등) 을 리턴할 수 있습니다.

반면 Opaque Type은 모든 가능한 return value들이 반드시 같은 single type이여야합니다. 

그렇지 않으면 아래와 같은 컴파일 에러가 발생합니다. 

"Function declares an opaque return type, but the return statements in its body do not have matching underlying types"

 

그래서 protocol type은 더 많은 유연성을 제공하는 반면 opaque type은 type에 대한 더 강력한 보장을 제공합니다. 

 

[2.2] return value 에 대한 연산

opaque type은 type identity를 보존하는데, 이로 인해 Swift는 해당  type을 추론할 수 있습니다.

그래서 return value에 대한 연산이 필요한 경우, opaque return type은 사용할 수 있지만

반면 protocol return type은 사용할 수 없는 경우가 있습니다. 

 

예제를 살펴봅시다.

 

return value로 해당 함수를 또 호출하는 경우,

let flipped1 = opaqueFlip(triangle)
opaqueFlip(flipped1)

let flipped2 = protocolFlip(triangle)
protocolFlip(flipped2) // 컴파일 에러!

opaque type은 Triangle이라는 타입을 보존해서 (또는 컴파일러가 Triangle이라는 타입 정보에 접근할 수 있어서) 

위와 같은 연산을 진행할 수 있습니다.

반면 protocol type은 아래와 같은 컴파일 에러가 납니다.

"Protocol 'Shape' as a type cannot conform to the protocol itself"

 

이렇게 return value로 계속 연산을 하는 게 편한 경우, opaque type을 사용해주는 게 좋겠네요! 

opaqueFlip(opaqueFlip(triangle))
protocolFlip(protocolFlip(triangle)) // 컴파일 에러!

 

[2.3] return type (Self or associated type requirements)

associatedtype 을 가지고 있는 프로토콜의 경우 fuction의 return type으로 사용할 수 없습니다. (generic type을 추론할 정보가 부족하기 때문인 것 같아요) 

하지만 opaque type의 경우, type identity 를 보존하기 때문에 fuction의 return type으로 사용할 수 있습니다. 

 

예제를 살펴봅시다.

protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}
extension Array: Container { }
func makeOpaqueContainer<T>(item: T) -> some Container {
    return [item]
}

// 컴파일 에러! 
// Error: Protocol with associated types can't be used as a return type.
func makeProtocolContainer<T>(item: T) -> Container {
    return [item]
}

 

아래 예제를 보면 associated type이 Int로 잘 추론되는 것을 볼 수 있습니다. 

let opaqueContainer = makeOpaqueContainer(item: 12)
print(opaqueContainer) 
// [12]

let twelve = opaqueContainer[0]
print(type(of: twelve))
// Int

 

associatedtype 뿐만 아니라 Self를 가지고 있는 프로토콜도 function의 return type으로 사용할 수 없습니다. 

하지만 opaque type은 return type으로 사용할 수 있어요! 

 

 

Equatable을 예제로 살펴볼게요 (출처: How to use opaque return types in Swift)

public protocol Equatable {

    /// Returns a Boolean value indicating whether two values are equal.
    ///
    /// Equality is the inverse of inequality. For any values `a` and `b`,
    /// `a == b` implies that `a != b` is `false`.
    ///
    /// - Parameters:
    ///   - lhs: A value to compare.
    ///   - rhs: Another value to compare.
    static func == (lhs: Self, rhs: Self) -> Bool
}

 

이렇게 하면 컴파일 에러가 나지만

func makeInt() -> Equatable {
    Int.random(in: 1...10)
}

// 컴파일 에러!
// Protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements

 

이렇게 하면 컴파일 에러가 안납니다!

func makeInt() -> some Equatable {
    Int.random(in: 1...10)
}

let int1 = makeInt()
let int2 = makeInt()
print(int1 == int2)



반응형
댓글