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