ジェネリック
ジェネリックとは、C#が提供する複数のデータ型に対応したオブジェクト(メソッドやクラス)を定義する機能です。
C++では関数テンプレートやクラステンプレートが該当しますが、これらとは若干扱い方が異なります。
ジェネリックメソッド
メソッド(関数)は、引数の数やデータ型、戻り値の型を指定して定義します。
この定義と異なる呼び出し方をするとエラーになります。
(引数の型や数が異なるなど)
データ型が制限されていることは間違いが起きにくいというメリットである一方で、同じような処理なのにデータ型が異なるだけで使えない、ということが起こり得ます。
//大きい方の数値を返す
//int型版
static int Max(int x, int y)
{
return x > y ? x : y;
}
//大きい方の数値を返す
//float型版
static float Max(float x, float y)
{
return x > y ? x : y;
}
上記のコードはメソッド内の処理は全く同じなのに、引数と戻り値の型が違うためそれぞれのデータ型に応じたメソッドを定義せねばなりません。
データ型はshort型やdouble型などもっとたくさんあるので、それらに対応していたのでは大変です。
このような場合に便利なのがジェネリックメソッドです。
ジェネリックメソッドは、多数のデータ型に対応したメソッドです。
上記と同等のメソッドをジェネリックメソッド版に書き直すと以下のようになります。
このメソッドはデータ型に依存することなく使用できます。
(正確には、比較可能なデータ型であれば使用可能。詳しくは後述)
//大きい方の数値を返す
//ジェネリックメソッド版
//int型でもfloat型でも使用可能
static T Max<T>(T x, T y) where T : IComparable
{
return x.CompareTo(y) > 0 ? x : y;
}
メソッド内の処理が少し変わっているのは後述します。
ジェネリックメソッドの作り方
ジェネリックメソッドは以下の形式で定義します。
戻り値の型 メソッド名<型引数>()
{
}
「static」は必須ではありません。
詳しくはstaticを参照してください。
通常のメソッドと異なるのは型引数(型パラメーター)です。
型引数は「データ型をメソッド側に渡す」ためのものです。
例えば以下のサンプルコードを見て下さい。
//ジェネリックメソッドの例
static void Test<T>(T x)
{
Console.WriteLine(x.GetType());
}
//呼び出し側
static void Main(string[] args)
{
Test<int>(5);
Test<string>("abc");
//型引数の指定の省略
Test(5);
Console.ReadLine();
}
System.Int32 System.String System.Int32
GetTypeメソッドはオブジェクト(変数など)からTypeオブジェクトを取得するメソッドです。
Typeオブジェクトはその値のデータ型の情報をまとめたオブジェクトで、Console.WriteLineにそのまま渡すとデータ型名を表示します。
呼び出し側では従来通り呼び出すメソッド名を記述した後に「<int>」という形式で、データ型を指定します。
ここで指定したデータ型がメソッド側に渡されます。
メソッド側では「<T>」でデータ型を受け取ります。
今回はint型ですので、「T」はint型ということになります。
呼び出し側でstringを指定すればメソッド側でもstring型として扱われます。
これがジェネリックメソッドです。
なお、実引数からデータ側が推測できる場合は型引数の指定は省略できます。
例えば数値リテラルはそのまま記述するとint型なので、型引数には自動的にint型が渡されます。
(14行目)
型引数名の「T」はTypeのTで、名前に特に意味を持たない場合によく使用されます。
「意味のない名前」は多用するとコードが分かりにくくなるので、可能ならば意味が特定できるものにすると良いでしょう。
使用したい型引数が複数ある場合は以下のように別の名称を付けます。
また、型引数は戻り値の型指定にも使用することができます。
//型引数をふたつ持つジェネリックメソッド
//T1型を返す
static T1 Test<T1, T2>(T1 x, T2 y)
{
}
ジェネリックメソッドの制限
ジェネリックメソッドは便利な機能ですが、制限もあります。
あらゆるデータ型が型引数に指定できてしまうため、そのままでは型引数に指定されたデータ型が持たない機能をメソッド側で使用してしまう可能性があります。
例えば足し算などの四則計算には「+」や「-」などの算術演算子を使用します。
これらの演算子で計算ができるのは、実はint型やfloat型などで内部的に演算子の動作が定義されているからです。
しかし算術演算子に対応していないデータ型も存在し、そのような型も型引数に指定可能であるため、ジェネリックメソッドでは型引数による仮引数は算術演算ができないように制限されています。
//指定されるデータ型が
//「+」演算子に対応しているとは限らない。
//なのでこれはNG
static T Test<T>(T x, T y)
{
return x + y;
}
これは算術演算子に限らず、実はほとんどの機能が使用できません。
C++にも似たような機能があり(関数テンプレート)、こちらは単純な置き換えのような機能なので算術演算子も使用できるのですが、C#ではそういうわけにはいかないのです。
ジェネリック型制約
ほとんどの機能が使用できないのではジェネリックメソッドにあまり有効な使い道はありません。
そこでジェネリック型制約というものを使用して、型引数がどのような機能を持っているかを指定します。
ジェネリック型制約は、引数の指定の後に「where 型引数名 : インターフェイス型」と言う形で指定します。
(インターフェイス型以外も指定可能。後述)
//「where ~」が型制約
static T Test<T>() where T : IComparable
{
}
これは「型引数T」に指定可能な型引数は「IComparable」の機能を持つデータ型に制限される、という意味になります。
「IComparable」というのはC#で提供されるインターフェイスクラスというデータ型です。
詳しくはインターフェイスの項で解説しますが、これは「比較機能を持つデータ型」であることを意味します。
int型やfloat型などは値の大小の比較が可能なデータ型で、これらはIComparableの性質を持っています。
正しく言えば、int型などはIComparableをベースにして定義されているデータ型です。
(継承を参照)
このジェネリックメソッドの型引数TにはIComparableベースで作られていないデータ型は指定できなくなります。
例えば配列型はIComparableではないので指定できません。
//型制約なし
static void Test1<T>(T x)
{
Console.WriteLine(x);
}
//IComparableに制限
static void Test2<T>(T x) where T : IComparable
{
Console.WriteLine(x);
}
static void Main(string[] args)
{
int num = 3;
int[] arr = new int[3];
Test1(num); //OK
Test1(arr); //OK
Test2(num); //OK
Test2(arr); //NG
}
呼び出し側でIComparableなデータ型しか指定できないということは、ジェネリックメソッド側からすれば型引数にはIComparableなデータ型が指定されることが保障されているということです。
IComparableなデータ型は必ず「CompareTo」というメソッドを持っています。
つまり、IComparableに型制約されているジェネリックメソッドからはCompareToメソッドを呼び出すことが可能になります。
static T Max<T>(T x, T y) where T : IComparable
{
//「T型の引数x」は必ず「CompareTo」メソッドを持っている
return x.CompareTo(y) > 0 ? x : y;
}
ちなみにCompareToメソッドは、インスタンス(変数)の値と引数の値を比較し、インスタンスのほうが小さければ0未満を返し、大きければ0より大きい値を返すメソッドです。
どちらも同じ値ならば0を返します。
上記のようにすれば、引数xとyの大きい方の値が得られます。
詳しくはオブジェクトの比較を参照してください。
ジェネリック型制約の種類
ジェネリック型制約には以下のような種類があります。
where T : struct | 値型のみ 値型と参照型参照 他の型制約と併用する場合、この型制約は最初に記述する |
where T : class | 参照型のみ 値型と参照型参照 他の型制約と併用する場合、この型制約は最初に記述する |
where T : クラス名 | 指定したクラス、またはその派生クラス |
where T : インターフェイス名 | 指定したインターフェイスクラスを継承するクラス |
where T : new() | 引数なしのコンストラクターを持つクラス 他の型制約と併用する場合、この型制約を最後に指定する |
where T : unmanaged | アンマネージ型 値型をポインタで扱う場合の型 |
これらの型制約は複数同時に指定可能です。
ただし「struct」と「class」は他の指定よりも前に記述する必要があります。
また、矛盾する場合や意味が重複する場合など、指定できない組み合わせもあります。
class TestClass
{ }
//型制約の複数指定
static void Test1<T>() where T : IComparable, IDisposable
{ }
//NG。値型かつ参照型はあり得ない
static void Test2<T>() where T : struct, class
{ }
//NG。TestClassは参照型なので改めて参照型の指定は必要ない
static void Test3<T>() where T : class, TestClass
{ }
型引数が複数の場合
複数の型引数にそれぞれ型制約を指定する場合は半角スペースもしくは改行で区切ります。
static void Test<T1, T2>(T1 x, T2 y)
where T1 : IComparable
where T2 : ICloneable
{
Console.WriteLine(x);
Console.WriteLine(y);
}
半角スペースでは横に長くなりがちなので改行したほうが良いと思います。
ジェネリッククラス
ジェネリックはクラスをジェネリック化することもできます。
class Test<T> where T : struct
{
T x;
T y;
public Test(T x, T y)
{
this.x = x;
this.y = y;
}
public void Method()
{
Console.WriteLine(x);
Console.WriteLine(y);
}
}
static void Main(string[] args)
{
Test<int> t = new Test<int>(1, 2);
t.Method();
Console.ReadLine();
}
ListクラスやDictionaryクラスなどのジェネリックコレクションもジェネリッククラスの一種です。
その他、構造体、インターフェイス、デリゲートもジェネリック化可能です。