Swift 5.8 日本語化計画 : Swift 5.8
不透明な型
不透明な戻り値の型を持つ関数またはメソッドは、その戻り値の型情報を隠します。関数の戻り値の型として具体的な型を提供する代わりに、戻り値はそれがサポートするプロトコルの観点で説明されます。型情報を隠すのは、モジュールとモジュールへと呼び出すコードの境界で役立ちます。と言うのも、戻り値の基になる型は private のままであるかもしれないためです。その型がプロトコルの型である値を返すのとは異なり、不透明の型は型の ID を保持します。コンパイラは型情報にアクセスできますが、モジュールのクライアントはアクセスできません。
不透明な型が解決する問題
たとえば、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())
- // *
- // **
- // ***
以下のコードに示すように、汎用を使用して、図形を垂直に反転させるような操作を実装できます。ただし、このアプローチには重要な制限があります。反転した結果は、作成に使用された正確な汎用型を公開します。
- 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())
- // ***
- // **
- // *
以下のコードが示すように、2 つの形を垂直に結合する JoinedShape<T : Shape, U : Shape> 構造体を定義するこのアプローチは、逆にされた三角形を別の三角形に結合することにより、 JoinedShape<FlippedShape<Triangle>, Triangle> のような型を返します。
- 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())
- // *
- // **
- // ***
- // ***
- // **
- // *
図形の作成に関する詳細な情報を公開すると、完全な戻り値の型を記述する必要があるため、ASCII アートモジュールの public インターフェイスの一部ではない型がリークする可能性があります。モジュール内のコードはさまざまな方法で同じ形をビルドできますが、形を使用するモジュール外の他のコードは、変換リストに関する実装の詳細を考慮する必要はありません。JoinedShape や FlippedShape などのラッパー型は、モジュールのユーザには関係なく、表示されるべきではありません。モジュールの public インターフェイスは、図形の結合や反転などの操作で構成され、これらの操作は別の Shape 値を返します。
不透明な型を返す
不透明 (Opaque) 型は、汎用型の逆のような物であると考えることができます。汎用型を使用すると、関数を呼び出すコードは、その関数のパラメータの型を選択し、関数の実装から抽象化された方法で値を返します。たとえば、以下のコードの関数は、呼び出し元に依存する型を返します。
max(_:_:) を呼び出すコードは、x と y の値を選択し、それらの値の型によって T の具体的な型が決まります。呼び出し元のコードは、Comparable プロトコルに準拠する任意の型を使用できます。関数内のコードは一般の方法で記述されているため、呼び出し元が提供するすべての型を処理できます。max(_:_:) の実装は、すべての Comparable 型が共有する機能のみを使用します。
これらの役割は、不透明な戻り値の型を持つ関数では逆になります。不透明 (Opaque) 型を使用すると、関数の実装は、関数を呼び出すコードから抽象化された方法で返される値の型を選択できます。たとえば、以下の例の関数は、その形の基になる型を公開せずに台形を返します。
- 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() 関数は、戻り値の型を some Shape として宣言しています。その結果、この関数は、特定の具体的な型を指定せずに、Shape プロトコルに準拠する、与えられた何らかの型の値を返します。この方法で makeTrapezoid() を記述すると、その public インターフェイスの一部から形を作成する特定の型を作成せずに、その public インターフェイスの基本的な側面 (それが返す値は形) を表現できます。この実装では 2 つの三角形と 1 つの正方形を使用しますが、関数を書き換えて、戻り値の型を変更せずに、さまざまな他の方法で台形を描画することができます。
この例では、不透明な戻り値の型が汎用型の逆のような方法を強調しています。makeTrapezoid() 内のコードは、呼び出し側のコードが汎用関数に対して行うように、その型が Shape プロトコルに準拠している限り、必要な任意の型を返すことができます。関数を呼び出すコードは、makeTrapezoid() によって返されるすべての Shape 値を処理できるように、汎用関数の実装のような一般的な方法で記述する必要があります。
不透明な戻り値の型を汎用と組み合わせることもできます。以下のコードの関数は両方とも、Shape プロトコルに準拠する何らかの型の値を返します。
- 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 の値は、この章で前述した 不透明な型が解決する問題 セクションの汎用の例の joinTriangles と同じです。ただし、その例の値とは異なり、flip(_:) および join(_:_:) は、汎用の形の操作が不透明な戻り値の型で返す基になる型を包み込み、これらの型が表示されないようにします。両方の関数が依存する型は汎用であり、関数への型パラメータは FlippedShape と JoinedShape が必要とする型情報を渡すため、両方の関数は汎用です。
不透明な戻り値の型の関数が複数の場所から戻る場合、可能な戻り値はすべて同じ型でなければなりません。汎用関数の場合、その戻り値の型は関数の汎用の型パラメータを使用できますが、単一の型でなければなりません。たとえば、正方形の特殊な場合を含む形状反転関数の 無効な バージョンは次のとおりです。
- 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 が返されます。これは、1 つの型の値のみを返すという要件に違反し、invalidFlip(_:) は無効なコードになります。invalidFlip(_:) を修正する 1 つの方法は、正方形 (square) の特殊な場合を 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")
- }
- }
常に単一の型を返すという要件は、不透明な戻り値の型で汎用 (ジェネリック) を使用することを妨げるものではありません。次に、返される値の基になる型に型パラメータを組み込む関数の例を示します。
- func `repeat`<T: Shape>(shape: T, count: Int) -> some Collection {
- return Array<T>(repeating: shape, count: count)
- }
この場合、戻り値の基になる型は T に応じて異なります。渡される形状に関係なく、repeat(shape:count:) はその形状の配列を作成して返します。それにもかかわらず、戻り値は常に同じ基本型 [T] を持つため、不透明な戻り値の型を持つ関数は単一の型の値のみを返さなければならないという要件に従います。
不透明な型とプロトコル型の違い
不透明 (Opaque) 型を返すことは、関数の戻り値の型としてプロトコル型を使用することに非常に似ていますが、これら 2 種類の戻り値の型は、型 ID を保持するかどうかが異なります。不透明な型は特定の 1 つの型を参照しますが、関数の呼び出し元はどの型かを見ることができません。プロトコル型は、プロトコルに準拠する全ての型を参照できます。一般的に、プロトコル型はそれらが格納する値の基になる型についてより柔軟性を提供し、不透明な型はこれらの基になる型についてより強力な保証を与えます。
たとえば、不透明な戻り値の型を使用する代わりに、プロトコル型の戻り値の型を使用する flip(_:) のバージョンは以下のとおりです。
- func protoFlip<T: Shape>(_ shape: T) -> Shape {
- return FlippedShape(shape: shape)
- }
このバージョンの protoFlip(_:) は、flip(_:) と同じ本体を持ち、常に同じ型の値を返します。flip(_:) とは異なり、protoFlip(_:) が返す値は常に同じ型である必要はなく、Shape プロトコルに準拠しなければなりません。別の言い方をすれば、protoFlip(_:) は、その呼び出し元との API の契約を、flip(_:) よりもはるかに緩やかにします。それは複数の型の値を返す柔軟性を確保します:
- func protoFlip<T: Shape>(_ shape: T) -> Shape {
- if shape is Square {
- return shape
- }
- return FlippedShape(shape: shape)
- }
コードのこの改訂版では、渡される形に応じて、Square のインスタンスまたは FlippedShape のインスタンスを返します。この関数によって返される 2 つの反転した形状 (flipped shape) は、完全に異なる型を持つ場合があります。この関数の他の有効なバージョンは、同じ形の複数のインスタンスを反転するときに異なる型の値を返す可能性があります。protoFlip(_:) からの特定性の低い戻り値型情報は、型情報に依存する多くの操作が戻り値で利用できないことを意味します。たとえば、以下の関数によって返された結果を比較する == 演算子を記述することはできません。
- let protoFlippedTriangle = protoFlip(smallTriangle)
- let sameThing = protoFlip(smallTriangle)
- protoFlippedTriangle == sameThing // Error
例の最後の行のエラーは、いくつかの理由で発生します。差し迫った問題は、Shape にはそのプロトコル要件の一部として == 演算子が含まれていないことです。1 つ追加すれば、次に遭遇する問題は、== 演算子が左辺と右辺の引数の型を知る必要があるということです。この種の演算子は通常、Self 型の引数を取り、プロトコルを採用する具体的な型にどれでも一致しますが、プロトコルに Self の要件を追加しても、プロトコルを型として使用するときに発生する型消去は許可されません。
関数の戻り値の型としてプロトコル型を使用すると、プロトコルに準拠する型を柔軟に返すことができます。ただし、その柔軟性の代償として、返された値に対して一部の操作ができないことがあります。この例は、== 演算子が使用できない例を示しており、これはプロトコル型を使用しても保持されない特定の型情報に依存します。
このアプローチの別の問題は、形の変換がネストされないことです。三角形の反転の結果は Shape 型の値であり、protoFlip(_:) 関数は、Shape プロトコルに準拠する何らかの型の引数を取ります。ただし、プロトコル型の値はそのプロトコルに準拠していません。protoFlip(_:) によって返される値は Shape に準拠していません。つまり、複数の変換を適用する protoFlip(protoFlip(smallTriange)) のようなコードは無効と言う意味であり、これは反転した形が protoFlip(_:) への有効な引数ではないためです。
対照的に、不透明 (Opaque) 型は、基になる型の ID を保持します。Swift は関連型を推測でき、これにより、プロトコル型を戻り値として使用できない場所で不透明な戻り値を使用できます。たとえば、ジェネリック(汎用) の Container プロトコルのバージョンは以下のとおりです。
- protocol Container {
- associatedtype Item
- var count: Int { get }
- subscript(i: Int) -> Item { get }
- }
- extension Array: Container { }
そのプロトコルには関連型があるため、関数の戻り値の型として Container を使用することはできません。また、汎用の型が何であるべきかを推測するのに十分な情報が関数の本体の外にないため、汎用の戻り値型内の制約として使用することもできません。
- // 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]
- }
不透明な型の some Container を戻り値の型として使用すると、目的の API の契約が表現されます。関数はコンテナを返しますが、コンテナの型の指定を拒否します。
- func makeOpaqueContainer<T>(item: T) -> some Container {
- return [item]
- }
- let opaqueContainer = makeOpaqueContainer(item: 12)
- let twelve = opaqueContainer[0]
- print(type(of: twelve))
- // Prints "Int"
twelve の型は Int と推測されます。これは、型推論が不透明な型で機能するという事実を示しています。 makeOpaqueContainer(item:) の実装では、不透明なコンテナの基本型は [T] です。この場合、T は Int であるため、戻り値は整数の配列であり、Item の関連型は Int であると推測されます。Container のサブスクリプトは Item を返しますが、つまり、twelve の型も Int であると推測されます。
前:ジェネリック(汎用) 次:自動参照カウント
トップへ
トップへ
トップへ
トップへ