.NET Framework 2.0 及び .NET Framework SDK version 2.0 から、C# 言語や .NET Framework 標準クラスライブラリにいくつかの追加変更が加えられました。 本書では、C# 2.0 で従来の C# 言語に追加された新しい言語仕様を解説いたします。
新しく追加された C# 2.0 言語の仕様では、プログラミング言語として新しい機能が追加されるようなものではありません。 CLR が従来の C++ 言語で使われていたテンプレートの機能に対応したため、言語仕様でもこれに対する追加があったほか、従来はいくつかの面倒な手続きが必要だったものを簡略できるようになる機能が主な追加点となっています。
C++ 言語などでテンプレートと呼ばれている機能を .NET では Generics と呼びます。 Generics を用いれば、利用される型をインスタンス生成時まで保留することが可能であり、より最適化されたコードで汎用的なクラスを作成することができます。
従来、.NET ではインスタンスを汎用的に抽象化して扱うには Object 型にキャストしていました。 代表的な存在としては IList インタフェースです。 IList インタフェースは、Add() メソッドや Remove() メソッドで動的にオブジェクトの配列を管理するためのインタフェースです。 このインタフェースの実装は、Object 型の参照を実行時に追加したり削除することができ、動的な配列を実現することができます。
Java プラットフォームも同様の手法で動的な配列を実現しており、この手法そのものに設計論・デザインパターンなどの見地から問題があるとは考えられません。 しかし、値型の情報を IList などの動的配列に汎用的な Object 型として保存するには、ボクシング、アンボクシング処理が挟まれるためパフォーマンスに問題があると指摘されます。
また、Object 型の動的配列は事実上あらゆる型を許容するため、設計では String 型の動的配列であると定義されていても、プログラマが誤って互換性の無い型を追加する危険性があります。 この場合、キャスト処理でミスを発見できますがコンパイル時に発見することはできません。 Generics はコンパイル時に誤った型が代入されることを認識することができます。
Generics を定義するには、クラスなどの型宣言に続いて < > で囲まれた 型パラメータ (type parameter) を指定します。 型パラメータはクラス定義では型が決定されず、インスタンス生成時に決定されます。 クラス定義時には、型パラメータで宣言された抽象的な名前であると考えることができます。
class クラス名 <型パラメータリスト> { ...
型パラメータリストにはカンマ , を用いて複数の型パラメータを宣言することができます。 型パラメータは識別子として扱われるため、C# 言語の識別子宣言規則に従い、かつ一意に認識できるものであればどのようなものでもかまいません。 一般的な命名規則としては大文字が使われます。 これを用いた Generics クラスの定義は、例えば次のようなものとなるでしょう。
class Generics1 <T> { private T x; public Generics1(T argv) { this.x = x; } }
この例では、Generics1 クラスの定義で型パラメータ T を宣言しています。 この T 型は具体的に何の型なのかが決定されているものではありませんが、クラス定義部では通常の型のように扱うことができます。 クラス定義のスコープ内で T 型は変数宣言、メソッド宣言、型キャストなどで利用することができます。
型パラメータを伴う定義ができる範囲は、クラス、構造体、インタフェース、デリゲート、及びメソッドとされています。
型パラメータを持つ Generics なクラスや構造体をインスタンス化するには、インスタンス生成式において型パラメータに型を与えなければなりません。 型パラメータを持つクラスや構造体の変数宣言、及びインスタンス生成式は次のようになります。
Generics1<int> intObj = new Generics1<int>(x);
このように、変数宣言文の型指定子では型名の直後に < > で囲んだ型を与えなければなりません。 ここで指定した型が、インスタンス生成時に型パラメータで指定した抽象的な型表現 T に替わるのだと考えてください。 ただし、.NET は単純なテキスト入れ替えではなく中間言語で型情報が維持され、実行時に最適化される仕組みです。 上記の文は、int 型の Generics1 クラスを作成しています。 このインスタンスの場合、T 型として宣言されていたメンバ変数 x は int 型として扱われるという意味になります。
class GenericsTest<TYPE> { public TYPE gValue; public GenericsTest(TYPE gValue) { this.gValue = gValue; } } class Test { static void Main() { GenericsTest<int> intObj = new GenericsTest<int>(10); GenericsTest<string> strObj = new GenericsTest<string>("Kitty on your lap"); System.Console.WriteLine("inbObj=" + intObj.gValue); System.Console.WriteLine("strObj=" + strObj.gValue); } }
inbObj=10 strObj=Kitty on your lap
このプログラムを実行すると、標準出力に上記のような結果が出力されます。 GenericsTest クラスは型パラメータ TYPE を持つクラスで gVluae メンバ変数が TYPE 型として宣言されています。 同様に、コンストラクタの引数にも gValue メンバ変数を初期化するための TYPE 型の仮パラメータが宣言されています。 この時点で TYPE 型の実体は不明です。
Main() メソッドでは int 型と string 型の GenericsTest インスタンスを生成していることがわかります。 GenericsTest<int> 型の変数が参照している GenericsTest インスタンスの gValue メンバ変数は int 型ですが、GenericsTest<string> 変数がん参照している GenericsTest インスタンスの gValue メンバ変数は string 型となります。 このように、インスタンス生成時にクラスが内包する型を決定することで、汎用的でありながら実行時には最適化された形で処理することができるようになるのです。 特に、値型をボクシング・アンボクシングする動的配列に比べれば、Generics による値型の動的配列は倍以上のパフォーマンスを期待することができます。
Generics は動的配列以外にも、設計上、または理論上異なる型で表現可能なデータモデルを汎用化する場合に威力を発揮します。 例えばグラフィックス・プログラミングの世界で、整数型で 2 次元座標やサイズが表され、一方で 3 次元グラフィックス環境では不動少数点数が使われる場合などが考えられます。 OpenGL では型ごとに関数を用意していますが、.NET の Generics はこれを単一のクラス定義で実現することができるのです。
class GPoint<TYPE> { private TYPE x , y; public GPoint(TYPE x , TYPE y) { this.x = x; this.y = y; } public TYPE X { set { this.x = value; } get { return x; } } public TYPE Y { set { this.y = value; } get { return y; } } public override string ToString() { return GetType() + " x = " + x + " : y = " + y; } } class Test { static void Main() { GPoint<int> intPt = new GPoint<int>(10 , 100); GPoint<float> fPt = new GPoint<float>(0.5F , 0.72F); GPoint<string> strPt = new GPoint<string>("Center" , "Top"); System.Console.WriteLine(intPt); System.Console.WriteLine(fPt); System.Console.WriteLine(strPt); } }
GPoint`1[System.Int32] x = 10 : y = 100 GPoint`1[System.Single] x = 0.5 : y = 0.72 GPoint`1[System.String] x = Center : y = Top
このプログラムでは、型パラメータを持つ 2 次元空間の座標を表現するための GPoint クラスを定義しています。 GPoint クラスは座標を表す X プロパティと Y プロパティを提供しますが、これらのプロパティの型は Generics の機能によって汎用化されています。 これによって、この座標型は整数でも、不動小数点数でも、列挙型でも、文字列でも表現可能となるのです。
また、実行結果を見れば GetType() メソッドによって出力された GPoint インスタンスの型を確認することができます。 これを見れば、.NET の Generics はプリプロセッサによるテキストの置き換えではなく、中間言語と実行時の JIT コンパイラレベルで Generics がサポートされていることを確認できます。
型パラメータリストにはカンマ , で区切って複数の型パラメータを指定することができます。 この場合、当然インスタンス生成時には型パラメータリストに従って型を与えなければいけません。 この関係は、メソッドの仮パラメータリストとパラメータの関係と同様です。
class Generics1 <T1 , T2> { ...
Generics1<Point , Size> gobj = new Generics1<Point , Size>();
例えば上記の例では、T1 と T2 型パラメータはそれぞれが独立していると考えられます。 T1 と T2 は、クラス定義のスコープ内で、それぞれ独立した型として利用することができ、インスタンス生成時にはそれぞれの型パラメータに型を当てはめることができます。
class GPoint<TX , TY> { private TX x; private TY y; public GPoint(TX x , TY y) { this.x = x; this.y = y; } public TX X { set { this.x = value; } get { return x; } } public TY Y { set { this.y = value; } get { return y; } } public override string ToString() { return GetType() + " x = " + x + " : y = " + y; } } class Test { static void Main() { GPoint<int , int> intPt = new GPoint<int , int>(10 , 100); GPoint<string , float> sfPt = new GPoint<string , float>("Left" , 0.75F); System.Console.WriteLine(intPt); System.Console.WriteLine(sfPt); } }
GPoint`2[System.Int32,System.Int32] x = 10 : y = 100 GPoint`2[System.String,System.Single] x = Left : y = 0.75
このプログラムの Gpoint クラスは、X プロパティと Y プロパティにそれぞれ異なる型パラメータを与えています。 そのため、前のプログラムとは異なり X プロパティは int 型で Y プロパティは float 型というような扱いが可能です。 もちろん、設計的に意味のあるものとは思えないので実践では利用できませんが、このような使い方が可能です。
複数の型パラメータを使う場面は、例えば特定のキーと値を関連付ける辞書クラスなどで有効でしょう。 キーの型とキーに関連付けられる値の型を、それぞれ独自に定義することができます。