Qpaque한 반환 Type을 가진 함수 또는 메서드는 반환 값의 Type 정보를 숨깁니다.
함수의 반환 Type으로 구체적인 Type을 제공하는 대신 반환 값은 지원하는 프로토콜 측면에서 설명됩니다.
예를 들어, 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은 모듈 사용자에게 중요하지 않으며 표시되지 않아야 합니다.
모듈의 공용 인터페이스는 모양 결합 및 뒤집기와 같은 작업으로 구성되며 이러한 작업은 다른 모양 값을 반환합니다.
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의 값만 반환해야 한다는 요구 사항을 따릅니다.
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로 유추됩니다.