ジェネリック

ジェネリックとは、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にそのまま渡すとデータ型名を表示します。
このメソッドはObject型に定義されているので、すべてのデータ型で使用できます。

呼び出し側はメソッド名を記述した後に<int>という形式でデータ型を指定します。
ここで指定したデータ型がメソッド側に渡されます。

メソッド側ではメソッド名の直後の<T>でデータ型を受け取ります。
今回はint型なので、Tはint型ということになります。
呼び出し側でstringを指定すればメソッド側でもstring型として扱われます。
このTは、そのメソッド内ではデータ型としての意味を持つので、引数やローカル変数などにT型を使用することができます。
このような機能を持つメソッドがジェネリックメソッドです。

なお、実引数からデータ側が推測できる場合は型引数の指定は省略できます。
例えば数値リテラルはそのまま記述するとint型なので、型引数には自動的にint型が渡されます。
(14行目)

型引数名の「T」はTypeのTで、名前に特に意味を持たない場合によく使用されます。
「意味のない名前」は多用するとコードが分かりにくくなるので、可能ならば意味が特定できるものにすると良いでしょう。
例えば戻り値の型であることを明確にしたい場合は「TResult」などの名前がよく使われます。

使用したい型引数が複数ある場合は以下のように別の名称を付けます。
また、型引数は戻り値の型指定にも使用することができます。


//型引数をふたつ持つジェネリックメソッド
//T1型を返す
static T1 Test<T1, T2>(T1 x, T2 y)
{
}

ジェネリックメソッドの制限

ジェネリックメソッドは便利な機能ですが、制限もあります。
あらゆるデータ型が型引数に指定できてしまうため、そのままでは型引数に指定されたデータ型が持たない機能をメソッド側で使用してしまう可能性があります。

例えば足し算などの四則計算には「+」や「-」などの算術演算子を使用します。
これらの演算子で計算ができるのは、実はint型やfloat型などで内部的に演算子の動作が定義されているからです。
しかし算術演算子に対応していないデータ型も存在し、そのような型も型引数に指定可能であるため、以下の例のT型は算術演算ができません。


//指定されるデータ型が
//「+」演算子に対応しているとは限らない。
//なのでこれはNG
static T Test<T>(T x, T y)
{
    //コンパイルエラー
    return x + y;
}

算術演算子に限らず、実はほとんどの機能が使用できません。
これは、値をobject型変数に格納すると元のデータ型の情報が失われるため、ほとんどの機能が利用できなくなるのと同じことです。

C++にも似たような機能があり(関数テンプレート)、こちらは単純な置き換えのような機能なので算術演算子も使用できるのですが、C#ではそういうわけにはいかないのです。

ジェネリック型制約

ほとんどの機能が使用できないのではジェネリックメソッドにあまり有効な使い道はありません。
そこでジェネリック型制約というものを使用して、型引数がどのような機能を持っているかを指定します。

ジェネリック型制約は、引数の指定の後にwhere 型引数名 : インターフェイス型と言う形で指定します。
(インターフェイス型以外も指定可能。後述)


//「where ~」が型制約
static T Test<T>() where T : IComparable
{
}

これは「型引数T」に指定可能な型引数は「IComparable」の派生型に制限される、という意味になります。
IComparableというのはC#で提供されるインターフェイスクラスで、データを比較するCompareToメソッドの実装を要求します。
int型やfloat型などは内部的にIComparableインターフェイスを基底クラスにして定義されているので、CompareToメソッドを持っています。
(基底クラスや派生クラスなどについては継承を参照してください)

このジェネリックメソッドの型引数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); //コンパイルエラー
}

呼び出し側で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の大きい方の値が得られます。

詳しくはオブジェクトの比較を参照してください。

上記コードでは型制約のないジェネリックメソッド内でConsole.WriteLineメソッドを使用していますが、このメソッドは内部でToStringメソッドを呼び出しています。
ToStringメソッドはObject型内で定義されているため、すべてのデータ型で使用できます。

ジェネリック型制約の種類

ジェネリック型制約には以下のような種類があります。

where T : struct 値型のみ
値型と参照型参照
他の型制約と併用する場合、この型制約は最初に記述する
where T : class 参照型のみ
値型と参照型参照
他の型制約と併用する場合、この型制約は最初に記述する
where T : クラス名 指定したクラス、またはその派生クラス(構造体)
where T : インターフェイス名 指定したインターフェイスクラスを継承するクラス(構造体)
where T : new() 引数なしのコンストラクターを持つクラス(構造体)
他の型制約と併用する場合、この型制約を最後に指定する
where T : unmanaged アンマネージ型
値型をポインタで扱う場合の型
(C#7.3以降)
where T : Enum 列挙型
(C#7.3以降)
where T : Delegate デリゲート型
(C#7.3以降)
where T : notnull null非許容型
(C#8以降)

これらの型制約は複数同時に指定可能です。
ただしstructclassは他の指定よりも前に記述する必要があります。
また、意味が矛盾する場合や重複する場合など、指定できない組み合わせもあります。


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クラスなどのジェネリックコレクションもジェネリッククラスの一種です。

その他、構造体インターフェイスデリゲートもジェネリック化可能です。