C++/CLI でのジェネリックの概要
ジェネリックは、共通言語ランタイムによってサポートされるパラメーター化された型です。 パラメーター化された型とは、ジェネリックを使用するときに指定される、不明な型パラメーターを使用して定義される型です。
なぜジェネリックか
C++ はテンプレートをサポートします。また、テンプレートとジェネリックのどちらも、型指定されたコレクション クラスを作成するパラメーター化された型をサポートします。 ただし、テンプレートにはコンパイル時のパラメーター化が用意されています。 テンプレートの定義を含むアセンブリを参照して、テンプレートの新しい特殊化を作成することはできません。 コンパイルが完了すると、特殊化されたテンプレートは他のクラスやメソッドと同じように見えます。 対照的に、ジェネリックはパラメーター化された型として MSIL に出力され、ランタイムではパラメーター化された型と見なされます。つまり、ジェネリック型を含むアセンブリを参照するソース コードでは、ジェネリック型の特殊化を作成できます。 標準 C++ テンプレートとジェネリックの比較の詳細については、「Generics and Templates (C++/CLI) (ジェネリックとテンプレート (C++ /CLI))」を参照してください。
ジェネリック関数と型
クラスの型は、マネージド型である限り、ジェネリック型にすることができます。 この一例として List
クラスがあります。 リスト内のオブジェクトの型は、型パラメーターです。 さまざまな型のオブジェクトに List
クラスが必要な場合、ジェネリックの前には、項目の型として System::Object
を受け取る List
が使用されていたことがあります。 ただし、この場合、任意のオブジェクト (不適切な型のオブジェクトを含む) をリストに使用できます。 そのようなリストは、型指定のないコレクション クラスと呼ばれます。 できることは、実行時に型をチェックし、例外をスローすることくらいです。 また、テンプレートをする場合もありました。この場合、テンプレートをアセンブリにコンパイルすると、そのジェネリックの質が失われます。 このアセンブリの利用者は、テンプレートの独自の特殊化を作成できないでしょう。 ジェネリックを使用すると、型指定されたコレクション クラスを作成できます。たとえば List<int>
("int のリスト" と読みます) や List<double>
("double のリスト") の場合、受け取るようにコレクションに設計されていない型を、型指定されたコレクションに適用しようとすると、コンパイル時エラーが発生します。 さらに、これらの型はコンパイル後もジェネリックのままです。
ジェネリック クラスの構文の説明については、「Generic Classes (C++/CLI) (ジェネリック クラス (C++/CLI))」を参照してください。 新しい名前空間 System.Collections.Generic では、Dictionary<TKey,TValue>、List<T>、LinkedList<T> などの一連のパラメーター化されたコレクション型が導入されました。
インスタンスおよび静的クラス メンバー関数、デリゲート、およびグローバル関数は、いずれもジェネリックにすることができます。 ジェネリック関数が必要になるのは、関数のパラメーターが不明な型である場合や、関数自体がジェネリック型を使用する必要がある場合です。 多くの場合、過去には不明なオブジェクト型のパラメーターとして System::Object
が使用されていましたが、代わりにジェネリック型パラメーターを使用すると、よりタイプ セーフなコードにすることができます。 関数に設計されていない型を渡そうとすると、コンパイル時にエラーというフラグが立てられます。 関数パラメーターとして System::Object
を使用すると、本来は関数で処理できないオブジェクトを誤って渡した場合に検出されないので、不明なオブジェクト型を関数本体の特定の型にキャストし、InvalidCastException の可能性を考慮する必要があります。 ジェネリックを使用する場合、コードで関数にオブジェクトを渡そうとすると型の競合が発生するので、関数本体を確実に正しい型にすることができます。
同じ利点が、ジェネリックに基づいて構築されたコレクション クラスにも当てはまります。 これまでのコレクション クラスでは、要素をコレクションに格納するために System::Object
が使用されていました。 コレクションが設計されていない型のオブジェクトを挿入しても、コンパイル時にはフラグが立てられませんでした。オブジェクトが挿入されたときであっても、多くの場合は同様です。 通常、オブジェクトはコレクション内でアクセスされるときに他の型にキャストされます。 キャストが失敗したときにのみ、予期しない型が検出されます。 ジェネリックでは、コンパイル時のこの問題を解決するために、ジェネリック コレクションの型パラメーターと一致しない (または暗黙的に変換される) 型を挿入するコードを検出しています。
構文の説明については、「Generic Functions (C++/CLI) (ジェネリック関数 (C++/CLI))」を参照してください。
ジェネリックで使用される用語
型パラメーター
ジェネリック宣言には、"型パラメーター" と呼ばれる 1 つ以上の不明な型が含まれています。 型パラメーターには、ジェネリック宣言の本体内で型を表す名前が付けられます。 型パラメーターは、ジェネリック宣言の本体内で型として使用されます。 List<T>
のジェネリック宣言には型パラメーター T が含まれています。
型引数
"型引数" は、ジェネリックが 1 つまたは複数の特定の型に特殊化される場合に、型パラメーターの代わりに使用される実際の型です。 たとえば、int
は List<int>
の型引数です。 ジェネリック型引数として使用できるのは、値の型とハンドルの型のみです。
構築型
ジェネリック型から構築された型は、"構築型" と呼ばれます。 オープン構築型などList<T>
、完全に指定されていない型。完全に指定された型 (たとえばList<double>
、閉じた構築型または特殊化された型)。 オープン構築型は、他のジェネリック型またはメソッドの定義に使用できます。また、それを囲むジェネリックが指定されるまで完全に指定することはできません。 ジェネリックの基底クラスとしてオープン構築型を使用する例を次に示します。
// generics_overview.cpp
// compile with: /clr /c
generic <typename T>
ref class List {};
generic <typename T>
ref class Queue : public List<T> {};
制約
制約は、型パラメーターとして使用できる型に対する制限です。 たとえば、あるジェネリック クラスが、指定したクラスから継承されたクラスのみを受け取ることや、指定したインターフェイスを実装することができます。 詳細については、「Constraints on Generic Type Parameters (C++/CLI) (ジェネリック型パラメーターの (C++/CLI))」を参照してください。
参照型と値の型
ハンドルの型と値の型は型引数として使用できます。 どちらの型も使用できるジェネリック定義では、参照型の構文になります。 たとえば、->
演算子は、最終的に使用される型が参照型か値の型かにかかわらず、型パラメーターの型のメンバーにアクセスするために使用されます。 型引数として値の型を使用すると、値の型はボックス化されず、値の型を直接使用するコードがランタイムによって生成されます。
ジェネリック型引数として参照型を使用するときは、ハンドル構文を使用します。 ジェネリック型引数として値の型を使用するときは、型の名前を直接使用します。
// generics_overview_2.cpp
// compile with: /clr
generic <typename T>
ref class GenericType {};
ref class ReferenceType {};
value struct ValueType {};
int main() {
GenericType<ReferenceType^> x;
GenericType<ValueType> y;
}
型パラメーター
ジェネリック クラスの型パラメーターは、他の識別子と同じように扱われます。 ただし、型が不明なので、使用に制限があります。 たとえば、型パラメーター クラスのメンバーとメソッドは、型パラメーターがそれらのメンバーをサポートすることがわかっていない場合に使用できません。 つまり、型パラメーターを介してメンバーにアクセスするには、メンバーを含む型を型パラメーターの制約リストに追加する必要があります。
// generics_overview_3.cpp
// compile with: /clr
interface class I {
void f1();
void f2();
};
ref struct R : public I {
virtual void f1() {}
virtual void f2() {}
virtual void f3() {}
};
generic <typename T>
where T : I
void f(T t) {
t->f1();
t->f2();
safe_cast<R^>(t)->f3();
}
int main() {
f(gcnew R());
}
これらの制限は演算子にも適用されます。 制約のないジェネリック型パラメーターでは、型が ==
および !=
演算子をサポートしていない場合、型パラメーターの 2 つのインスタンスを比較するときにそれらの演算子を使用できません。 ジェネリックにはこれらのチェックが必要ですが、テンプレートには必要ありません。なぜなら、ジェネリックは、実行時に制約を満たす任意のクラスで特殊化される場合があり、そのときには無効なメンバーの使用をチェックするには遅すぎるからです。
型パラメーターの既定のインスタンスは、()
演算子を使用して作成できます。 次に例を示します。
T t = T();
ここで、T
はジェネリック クラスまたはメソッド定義内の型パラメーターであり、変数がその既定値に初期化されます。 T
が参照クラスの場合は null ポインターになります。T
が値クラスの場合、オブジェクトは 0 に初期化されます。 これは "既定の初期化子" と呼ばれます。