Opaque Types

Groot·2022년 10월 10일
0

Swift Language Guide

목록 보기
23/24
post-thumbnail

Opaque Types

  • Qpaque한 반환 Type을 가진 함수 또는 메서드는 반환 값의 Type 정보를 숨깁니다.
  • 함수의 반환 Type으로 구체적인 Type을 제공하는 대신 반환 값은 지원하는 프로토콜 측면에서 설명됩니다.
  • 반환 값의 기본 형식이 비공개로 유지될 수 있으므로 형식 정보를 숨기는 것은 모듈과 모듈을 호출하는 코드 사이의 경계에서 유용합니다.
  • Type이 프로토콜 Type인 값을 반환하는 것과 달리 Qpaque Type은 Type ID를 유지합니다.
  • 컴파일러는 Type 정보에 액세스할 수 있지만 모듈의 클라이언트는 액세스할 수 없습니다.

📌 The Problem That Opaque Types Solve

  • 예를 들어, ASCII 아트 모양을 그리는 모듈을 작성한다고 가정합니다.

  • ASCII 아트 셰이프의 기본 특성은 해당 셰이프의 문자열 표현을 반환하는 draw() 함수로, Shape 프로토콜의 요구 사항으로 사용할 수 있습니다.

    protocol Shape {
        func draw() -> String
    }
    
    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 smallTriangle = Triangle(size: 3)
    print(smallTriangle.draw())
    // *
    // **
    // ***
  • 아래 코드와 같이 제네릭을 사용하여 모양을 세로로 뒤집는 것과 같은 작업을 구현할 수 있습니다.

  • 그러나 이 접근 방식에는 중요한 제한이 있습니다.

  • 뒤집힌 결과는 생성에 사용된 정확한 제네릭 Type을 노출합니다.

    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 flippedTriangle = FlippedShape(shape: smallTriangle)
    print(flippedTriangle.draw())
    // ***
    // **
    // *
  • 아래 코드와 같이 두 모양을 수직으로 결합하는 JoinedShape<T: Shape, U: Shape> 구조를 정의하는 이 접근 방식은 반전된 삼각형을 다른 삼각형과 결합하여 JoinedShape<FlippedShape\, Triangle>과 같은 Type을 생성합니다.

    struct JoinedShape<T: Shape, U: Shape>: Shape {
        var top: T
        var bottom: U
        func draw() -> String {
            return top.draw() + "\n" + bottom.draw()
        }
    }
    let joinedTriangles = JoinedShape(top: smallTriangle, bottom: flippedTriangle)
    print(joinedTriangles.draw())
    // *
    // **
    // ***
    // ***
    // **
    // *
  • 모양 생성에 대한 자세한 정보를 노출하면 전체 반환 Type을 명시해야 하기 때문에 ASCII 아트 모듈의 공개 인터페이스의 일부가 아닌 Type이 누출될 수 있습니다.

  • 모듈 내부의 코드는 다양한 방식으로 동일한 모양을 만들 수 있으며 모양을 사용하는 모듈 외부의 다른 코드는 변환 목록에 대한 구현 세부 정보를 설명할 필요가 없습니다.

  • JoinedShape 및 FlippedShape와 같은 래퍼 Type은 모듈 사용자에게 중요하지 않으며 표시되지 않아야 합니다.

  • 모듈의 공용 인터페이스는 모양 결합 및 뒤집기와 같은 작업으로 구성되며 이러한 작업은 다른 모양 값을 반환합니다.

📌 Returning an Opaque Type

  • Generic Type의 반대와 같은 Qpaque Type을 생각할 수 있습니다.

  • Generic Type을 사용하면 함수를 호출하는 코드가 해당 함수의 매개변수에 대한 Type을 선택하고 함수 구현에서 추상화된 방식으로 값을 반환합니다.

  • 예를 들어 다음 코드의 함수는 호출자에 따라 달라지는 형식을 반환합니다.

    func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }
  • max(::)를 호출하는 코드는 x와 y에 대한 값을 선택하고 이러한 값의 Type은 T의 구체적인 Type을 결정합니다.

  • 호출 코드는 Comparable 프로토콜을 준수하는 모든 Type을 사용할 수 있습니다.

  • 함수 내부의 코드는 호출자가 제공하는 모든 Type을 처리할 수 있도록 일반적인 방식으로 작성됩니다.

  • max(::) 구현은 모든 Comparable Type이 공유하는 기능만 사용합니다.

  • 이러한 역할은 Qpaque한 반환 Type이 있는 함수에 대해 반대입니다.

  • Qpaque한 Type을 사용하면 함수 구현이 함수를 호출하는 코드에서 추상화된 방식으로 반환하는 값의 Type을 선택할 수 있습니다.

  • 예를 들어 다음 예제의 함수는 해당 모양의 기본 Type을 노출하지 않고 사다리꼴을 반환합니다.

    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")
        }
    }
    
    func makeTrapezoid() -> some Shape {
        let top = Triangle(size: 2)
        let middle = Square(size: 2)
        let bottom = FlippedShape(shape: top)
        let trapezoid = JoinedShape(
            top: top,
            bottom: JoinedShape(top: middle, bottom: bottom)
        )
        return trapezoid
    }
    let trapezoid = makeTrapezoid()
    print(trapezoid.draw())
    // *
    // **
    // **
    // **
    // **
    // *
  • 이 예제의 makeTrapezoid() 함수는 반환 Type을 some Shape로 선언합니다.

  • 결과적으로 함수는 특정 구체적인 Type을 지정하지 않고 Shape 프로토콜을 준수하는 특정 Type의 값을 반환합니다.

  • 이러한 방식으로 makeTrapezoid()를 작성하면 공용 인터페이스의 일부에서 모양이 만들어지는 특정 Type을 만들지 않고도 공용 인터페이스의 기본 측면(반환 값이 모양임)을 표현할 수 있습니다.

  • 이 구현은 두 개의 삼각형과 사각형을 사용하지만 반환 Type을 변경하지 않고 다른 다양한 방법으로 사다리꼴을 그리도록 함수를 다시 작성할 수 있습니다.

  • 이 예제는 Qpaque 반환 Type이 제네릭 Type의 반대와 같은 방식을 강조합니다.

  • makeTrapezoid() 내부의 코드는 호출 코드가 Generic 함수에 대해 수행하는 것처럼 해당 Type이 Shape 프로토콜을 준수하는 한 필요한 모든 Type을 반환할 수 있습니다.

  • 함수를 호출하는 코드는 Generic 함수의 구현과 같은 일반적인 방식으로 작성되어야 합니다, 그래야 makeTrapezoid()가 반환하는 모든 Shape 값과 함께 작동할 수 있습니다.

  • Qpaque한 반환 Type을 제네릭과 결합할 수도 있습니다.

  • 다음 코드의 함수는 모두 Shape 프로토콜을 준수하는 일부 Type의 값을 반환합니다.

    func flip<T: Shape>(_ shape: T) -> some Shape {
        return FlippedShape(shape: shape)
    }
    func join<T: Shape, U: Shape>(_ top: T, _ bottom: U) -> some Shape {
        JoinedShape(top: top, bottom: bottom)
    }
    
    let opaqueJoinedTriangles = join(smallTriangle, flip(smallTriangle))
    print(opaqueJoinedTriangles.draw())
    // *
    // **
    // ***
    // ***
    // **
    // *
  • 이 예제의 opaqueJoinedTriangles 값은 이 장의 앞부분에 있는 The Problem That Opaque Types Solve 섹션의 제네릭 예제에 있는 joinTriangles와 동일합니다.

  • 그러나 해당 예제의 값과 달리 flip(:) 및 join(:_:)은 Generic 모양 작업이 반환하는 기본 Type을 Qpaque한 반환 Type으로 래핑하므로 해당 Type이 표시되지 않습니다.

  • 두 함수 모두 의존하는 형식이 제네릭이고 함수에 대한 형식 매개 변수가 FlippedShape 및 JoinedShape에 필요한 형식 정보를 전달하기 때문에 제네릭입니다.

  • Qpaque한 반환 Type을 가진 함수가 여러 위치에서 반환되는 경우 가능한 모든 반환 값은 동일한 Type이어야 합니다.

  • 제네릭 함수의 경우 해당 반환 Type은 함수의 제네릭 Type 매개변수를 사용할 수 있지만 여전히 단일 Type이어야 합니다.

  • 예를 들어, 다음은 정사각형에 대한 특별한 경우를 포함하는 잘못된 버전의 모양 뒤집기 기능입니다.

    func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
        if shape is Square {
            return shape // Error: return types don't match
        }
        return FlippedShape(shape: shape) // Error: return types don't match
    }
  • Square와 함께 이 함수를 호출하면 Square가 반환됩니다.

  • 그렇지 않으면 FlippedShape를 반환합니다.

  • 이는 한 가지 Type의 값만 반환해야 한다는 요구 사항을 위반하고 invalidFlip(_:) 코드를 유효하지 않게 만듭니다.

  • invalidFlip(_:)을 수정하는 한 가지 방법은 정사각형의 특수한 경우를 FlippedShape의 구현으로 옮기는 것입니다. 그러면 이 함수는 항상 FlippedShape 값을 반환할 수 있습니다.

    struct FlippedShape<T: Shape>: Shape {
        var shape: T
        func draw() -> String {
            if shape is Square {
                return shape.draw()
            }
            let lines = shape.draw().split(separator: "\n")
            return lines.reversed().joined(separator: "\n")
        }
    }
  • 항상 단일 Type을 반환해야 한다는 요구 사항이 Qpaque한 반환 Type에서 제네릭을 사용하는 것을 막지는 않습니다.

  • 다음은 반환하는 값의 기본 Type에 해당 Type 매개변수를 통합하는 함수의 예입니다.

    func `repeat`<T: Shape>(shape: T, count: Int) -> some Collection {
        return Array<T>(repeating: shape, count: count)
    }
  • 이 경우 반환 값의 기본 Type은 T에 따라 다릅니다, 어떤 모양이 전달되든 repeat(shape:count:)는 해당 모양의 배열을 만들고 반환합니다.

  • 그럼에도 불구하고 반환 값은 항상 동일한 기본 Type인 [T]를 가지므로 Qpaque한 반환 Type을 가진 함수는 단일 Type의 값만 반환해야 한다는 요구 사항을 따릅니다.

📌 Differences Between Opaque Types and Protocol Types

  • Qpaque한 형식을 반환하는 것은 프로토콜 형식을 함수의 반환 형식으로 사용하는 것과 매우 유사하지만 이 두 종류의 반환 형식은 형식 ID를 유지하는지 여부가 다릅니다.

  • Qpaque한 Type은 하나의 특정 Type을 참조하지만 함수 호출자는 어떤 Type을 볼 수 없습니다.

  • 프로토콜 Type은 프로토콜을 준수하는 모든 Type을 참조할 수 있습니다.

  • 일반적으로 프로토콜 Type은 저장하는 값의 기본 Type에 대해 더 많은 유연성을 제공하고 Qpaque Type을 사용하면 이러한 기본 Type에 대해 더 강력한 보장을 할 수 있습니다.

  • 예를 들어, 다음은 Qpaq****ue한 반환 Type 대신 프로토콜 Type을 반환 Type으로 사용하는 flip(_:) 버전입니다.

    func protoFlip<T: Shape>(_ shape: T) -> Shape {
        return FlippedShape(shape: shape)
    }
  • 이 버전의 protoFlip(:)은 flip(:)과 동일한 본문을 가지며 항상 동일한 Type의 값을 반환합니다.

  • flip(:)과 달리 protoFlip(:)이 반환하는 값은 항상 같은 Type을 가질 필요는 없습니다, Shape 프로토콜을 준수하기만 하면 됩니다.

  • 다시 말해 protoFlip(:)은 호출자와의 API 계약이 flip(:)보다 훨씬 느슨합니다. 여러 Type의 값을 반환할 수 있는 유연성을 보유합니다.

    func protoFlip<T: Shape>(_ shape: T) -> Shape {
        if shape is Square {
            return shape
        }
    
        return FlippedShape(shape: shape)
    }
  • 코드의 수정된 버전은 전달된 모양에 따라 Square 인스턴스 또는 FlippedShape 인스턴스를 반환합니다.

  • 이 함수에서 반환된 두 개의 뒤집힌 모양은 완전히 다른 Type을 가질 수 있습니다.

  • 이 함수의 다른 유효한 버전은 동일한 모양의 여러 인스턴스를 뒤집을 때 다른 Type의 값을 반환할 수 있습니다.

  • protoFlip(_:)의 덜 구체적인 반환 Type 정보는 Type 정보에 의존하는 많은 작업을 반환된 값에서 사용할 수 없음을 의미합니다.

  • 예를 들어, 이 함수에서 반환된 결과를 비교하는 == 연산자를 작성할 수 없습니다.

    let protoFlippedTriangle = protoFlip(smallTriangle)
    let sameThing = protoFlip(smallTriangle)
    protoFlippedTriangle == sameThing  // Error
  • 예제의 마지막 줄에 있는 오류는 여러 가지 이유로 발생합니다.

  • 즉각적인 문제는 Shape에 프로토콜 요구 사항의 일부로 == 연산자가 포함되어 있지 않다는 것입니다.

  • 추가하려고 하면 다음 문제에 직면하게 될 == 연산자가 왼쪽 및 오른쪽 인수의 Type을 알아야 한다는 것입니다.

  • 이러한 종류의 연산자는 일반적으로 프로토콜을 채택하는 구체적인 Type과 일치하는 Self Type의 인수를 사용하지만 프로토콜에 Self 요구 사항을 추가하면 프로토콜을 Type으로 사용할 때 발생하는 Type 삭제가 허용되지 않습니다.

  • 프로토콜 Type을 함수의 반환 Type으로 사용하면 프로토콜을 준수하는 모든 Type을 반환할 수 있는 유연성을 얻을 수 있습니다.

  • 그러나 이러한 유연성의 대가는 반환된 값에 대해 일부 작업을 수행할 수 없다는 것입니다.

  • 예제는 == 연산자를 사용할 수 없는 방법을 보여줍니다. 이는 프로토콜 Type을 사용하여 보존되지 않는 특정 Type 정보에 따라 다릅니다.

  • 이 접근 방식의 또 다른 문제는 모양 변환이 중첩되지 않는다는 것입니다.

  • 삼각형을 뒤집은 결과는 Shape Type의 값이고 protoFlip(_:) 함수는 Shape 프로토콜을 준수하는 일부 Type의 인수를 취합니다.

  • 그러나 프로토콜 Type의 값은 해당 프로토콜을 따르지 않습니다.

  • protoFlip(_:)이 반환한 값이 Shape를 따르지 않습니다.

  • 이것은 뒤집힌 모양이 protoFlip(_:)에 대한 유효한 인수가 아니기 때문에 다중 변환을 적용하는 protoFlip(protoFlip(smallTriange))과 같은 코드가 유효하지 않음을 의미합니다.

  • 대조적으로 Qpaque 형식은 기본 형식의 ID를 유지합니다.

  • Swift는 관련 Type을 유추할 수 있으므로 프로토콜 Type을 반환 값으로 사용할 수 없는 위치에서 Qpaque한 반환 값을 사용할 수 있습니다.

  • 예를 들어 다음은 Generics의 Container 프로토콜 버전입니다.

    protocol Container {
        associatedtype Item
        var count: Int { get }
        subscript(i: Int) -> Item { get }
    }
    extension Array: Container { }
  • 프로토콜에 연결된 Type이 있기 때문에 컨테이너를 함수의 반환 Type으로 사용할 수 없습니다.

  • 또한 함수 본문 외부에 제네릭 형식이 필요한 것을 유추할 수 있는 정보가 충분하지 않기 때문에 제네릭 반환 형식의 제약 조건으로 사용할 수 없습니다.

    // Error: Protocol with associated types can't be used as a return type.
    func makeProtocolContainer<T>(item: T) -> Container {
        return [item]
    }
    
    // Error: Not enough information to infer C.
    func makeProtocolContainer<T, C: Container>(item: T) -> C {
        return [item]
    }
  • Qpaque Type을 사용하여 반환 Type으로 일부 Container는 원하는 API 계약을 표현합니다.

  • 함수는 컨테이너를 반환하지만 컨테이너 Type 지정을 거부합니다.

    func makeOpaqueContainer<T>(item: T) -> some Container {
        return [item]
    }
    let opaqueContainer = makeOpaqueContainer(item: 12)
    let twelve = opaqueContainer[0]
    print(type(of: twelve))
    // Prints "Int"
  • 12개의 Type은 Int로 유추되며, 이는 Type 유추가 Qpaque한 Type과 함께 작동한다는 사실을 보여줍니다.

  • makeOpaqueContainer(item:) 구현에서 Qpaque 컨테이너의 기본 Type은 [T]입니다.

  • 이 경우 T는 Int이므로 반환 값은 정수 배열이고 Item 관련 Type은 Int로 유추됩니다.

  • Container의 첨자는 Item을 반환합니다. 즉, 12개의 Type도 Int로 유추됩니다.

profile
I Am Groot

0개의 댓글