単純に、プロパティやインデクサを通して値やオブジェクトを入出力するだけの管理用クラスであれば型パラメータの存在は強力です。 その一例が動的配列や辞書クラスですが、あらゆる型が当てはめられると一部でロジックの矛盾が発生してしまうというケースもあるでしょう。
例えば、座標を表すオブジェクトのプロパティに対してデータベースのテーブル型が指定されたり、イメージ型が設定されるのは奇妙です。 こうした誤りをなくすために、一定の制限を型パラメータに与えることができます。 これを型パラメータの制約と呼びます。
型パラメータに制約を与えるには、型パラメータリストの直後に次のような where 句を指定します。 ただし、制約にはいくつかの種類があります。 まずはセカンダリ制約から説明しましょう。
where 型パラメータ : 制約リスト
セカンダリ制約の制約リストには、指定した型パラメータが実装していなければならないインタフェース型、または型パラメータを指定します。 例えば、ここに IList インタフェースを制約として加えた場合、型パラメータは IList インタフェースを実装している型でなければならないということになります。 制約リストには、カンマ , で区切ることで複数のインタフェース型を制約として指定することもできます。
class Generics1<T> where T : I { ...
上記のような宣言のクラスは、型パラメータ T が I 型のインタフェースを実装していなければならないということを制約しています。 インスタンス生成時に、このクラスの型パラメータに指定する型は I を実装(すなわち、I 型にキャスト可能な型)していなければなりません。
interface IA {} class A : IA {} class B : A {} class GTest<TYPE> where TYPE : IA {} class Test { static void Main() { GTest<A> aObj = new GTest<A>(); GTest<B> bObj = new GTest<B>(); //GTest<int> intObj = new GTest<int>(); //エラー System.Console.WriteLine(aObj); System.Console.WriteLine(bObj); } }
このプログラムの GTest クラスの型パラメータ TYPE は IA インタフェース型に制約されています。 そのため、GTest インスタンス生成時に指定する型は IA 型、または IA 型を実装する型でなければならないのです。 A クラスは IA インタフェースを実装し、B クラスは A クラスを継承しているため、これらのクラス型は GTest の型パラメータ TYPE に指定することができます。 しかし、これ以外の型は制約があるため指定することはできません。
制約に複数のインタフェース型を指定した場合は、OR ではなく AND 制約として認識されます。 例えば IA インタフェース型と IB インタフェース型が制約として指定されている場合、IA インタフェースと IB インタフェースの両方を実装している型でなければ指定することができません。
interface IA {} interface IB {} class A : IA {} class B : A , IB {} class GTest<TYPE> where TYPE : IA , IB {} class Test { static void Main() { //GTest<A> aObj = new GTest<A>(); //エラー GTest<B> bObj = new GTest<B>(); System.Console.WriteLine(bObj); } }
このプログラムの GTest クラスの TYPE 型パラメータには IA インタフェース型と IB インタフェース型の制約が指定されています。 そのため、GTest のインスタンス生成時に指定する型は IA インタフェース及び IB インタフェースの両方を実装している型でなければならないのです。
複数の型パラメータが宣言されているクラスで、複数の型パラメータに制約を与える場合は where 句を複数指定します。 where 句の区切り子は無く、空白文字などトークンの分離で次の where 句に移ることができます。
class Generics<T1 , T2> where T1 : I1 whereT2 : I2
この例では、T1 型パラメータに I1 インタフェース型の制約を、T2 に I2 型の制約を与えています。
セカンダリ制約では、制約リストの中に型パラメータを含めることができます。 型パラメータが制約に指定された場合、インスタンス生成時にその型パラメータに指定された型が制約対象となります。
class Generics<T1 , T2> where T1 : I where T2 : T1
この宣言の型パラメータ T2 に指定されている制約は T1 型パラメータです。 この場合、T1 に指定された制約ではなく、インスタンス生成時に T1 に指定された型が制約対象になるという点に注意してください。 すなわち、Generics では制約対象すら抽象化して、インスタンス生成時に決定するということができるのです。
interface IA {} interface IB {} class A : IA {} class B1 : A , IB {} class B2 : IA , IB {} class GTest<T1 , T2> where T1 : IA where T2 : T1 , IB {} class Test { static void Main() { GTest<A , B1> b1Obj = new GTest<A , B1>(); //GTest<A , B2> b2Obj = new GTest<A , B2>(); //エラー System.Console.WriteLine(b1Obj); } }
このプログラムの GTest クラスの T2 型パラメータは T1 型パラメータを制約としています。 そのため、T2 は、T1 に指定された型が制約対象となります。
GTest のインスタンス生成時は T1 に対して A クラス型を指定しています。 T1 の制約は IA インタフェースであり、A クラスは IA を実装しているため問題はありません。 この時点で、T2 の制約は A クラス型と IB インタフェースに決定されます。
T2 型パラメータに指定できるのは、この時点で A クラス型と IB インタフェース型に互換性のある型でなければなりません。 B1 クラスは A クラスを継承し IB インタフェースを実装しているため T2 に指定することができます。 しかし、B2 クラスは IA と IB を実装しているにもかかわらず T2 に指定することはできません。 これは B2 が T1 の制約である A 型に互換性がないためです。
プライマリ制約は、セカンダリ制約とは異なり 1 つの型だけを制約に指定することができます。 セカンダリ制約とは異なり、複数の型を制約に指定することができません。
プライマリ制約とセカンダリ制約の見分け方は、型の種類です。 セカンダリ制約はインタフェース型や型パラメータを制約型として指定しています。 プライマリ制約には、インタフェース型や型パラメータを指定することはできず、任意のクラス型、キーワードのみとなります。
プライマリ制約では、型パラメータが値型でなければならないとか参照型でなければならないという制約を指定することができます。 制約に値型でなければならないということを宣言するには struct キーワードを、参照型として指定するには class キーワードを指定します。 プライマリ制約は常に 1 つの型しか指定できないため、struct や class を同時に指定したり、他の型と一緒に指定することはできません。
class GPoint<TYPE> where TYPE : struct { private TYPE x , y; public GPoint(TYPE x , TYPE y) { this.x = x; this.y = y; } public TYPE X { get { return x; } } public TYPE Y { 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.75F); //GPoint<string> strPt = new GPoint<string>("left" , "top"); //エラー System.Console.WriteLine(intPt); System.Console.WriteLine(fPt); } }
このプログラムの GPoint クラスはプライマリ制約の struct が指定されています。 よって、TYPE 型パラメータには値型しか指定することができません。 インスタンス生成時に参照型を型パラメータに指定しようとすると、コンパイルエラーを確認することができます。 同様に、制約に class を指定すれば参照型のみを指定することができるようになり、値型を指定するとエラーが発生します。
プライマリ制約にクラス型を指定した場合、そのクラス型、またはそのサブクラス型のみを指定することができます。
class A {} class B : A {} class GTest<TYPE> where TYPE : A {} class Test { static void Main() { GTest<A> aObj = new GTest<A>(); GTest<B> bObj = new GTest<B>(); System.Console.WriteLine(aObj); System.Console.WriteLine(bObj); } }
GTest クラスの型パラメータ TYPE の制約に A クラス型を指定しています。 これはインタフェースではなくクラス型なのでプライマリ制約に分類されます。 そのため、GTest クラスのインスタンス生成時には、型パラメータに対して A クラス、または A クラスのサブクラスを指定しなければなりません。
コンストラクタ制約は、型パラメータは引数の無いコンストラクタを公開していなければならないという制約を与えることができます。 この制約が型パラメータに指定されている場合、型パラメータに指定されている型は確実に new TYPE() をサポートしていると考えることができます。
コンストラクタ制約は new() を where 句の制約に指定します。
class Generics<T> where T : new() { ...
型パラメータに指定した型の引数の無いコンストラクタが private であったり、型が abstract である場合はコンパイルエラーとなります。
class A { public A() {} } class B { private B() {} } class GTest<TYPE> where TYPE : new() {} class Test { static void Main() { GTest<A> aObj = new GTest<A>(); //GTest<B> bObj = new GTest<B>(); //エラー System.Console.WriteLine(aObj); } }
このプログラムの GTest クラスの TYPE 型パラメータは new() によってコンストラクタ制約が採用されてます。 そのため、インスタンス生成時に指定する型は、必ず引数の無いコンストラクタを公開していなければなりません。
A クラスは public で引数の無いコンストラクタを公開しているため、TYPE 型パラメータに指定することができます。 しかし、B クラスは引数の無いコンストラクタを private 修飾子で隠蔽しているため、コンパイルエラーとなってしまうでしょう。