ジェネリックとは
ジェネリックの意味
型パラメーターを使用することで、汎用的なクラスやメソッドなどを作ることができる機能。
Microsoftのドキュメント(英語版)ではgenericsとなっており、Javaなどではジェネリクスという訳語が一般的だが、Microsoftドキュメント(日本語版)ではジェネリックという訳語になっている。
英単語のgenericは、「汎用の」「一般的な」等の意味の形容詞。
ジェネリックを使うメリット
ジェネリックを使うと、クラス、構造体、インターフェイス、およびメソッドに、データ型のパラメーターを持たせることができる。
ジェネリックが存在しない場合、同じ処理をするメソッドでも、型ごとに別のメソッドを作らなければならない。
例えば、2つの値の比較をするメソッドの場合、オーバーロードを使ったとしても、Compare(int x,int y)、Compare(double x,double y)、Compare(long x,long y)、Compare(string x,string y)・・・と型の数だけのメソッドを作る必要がある。
ジェネリックを使うと、Compare<T>というメソッド一つで済ませることができる。
型パラメーターと型引数
型パラメーターは、Microsoftドキュメント(英語版)では type parameter となっている。
型引数は、Microsoftドキュメント(英語版)では type argument となっている。
つまり、パラメーター(仮引数)と引数(実引数)の違いと同じである。
型パラメーターは、メソッド定義に記述する、型引数を受けるための変数であり、
型引数はメソッド呼び出しで指定する実際の型である。
ジェネリックメソッド
ジェネリックメソッドは以下のように定義する。whereについては後述の型パラメーターの制約を参照。
1 2 3 4 5 |
アクセス修飾子 戻り値の型 メソッド名<型パラメーター>(引数リスト) where 型引数:制約条件 { メソッド定義 } |
ジェネリックがないと、以下のように処理内容は全く同じでも、型が違うだけで別のメソッドを作らなければならない。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//2値を入れ替えるメソッド int版 static void Swap(ref int a, ref int b) { int temp; temp = a; a = b; b = temp; } //2値を入れ替えるメソッド double版 static void Swap(ref double a, ref double b) { double temp; temp = a; a = b; b = temp; } |
ジェネリックメソッドを使うと、上記のようなメソッドを1つにまとめることができる。山かっこ<>を使って指定しているものが型パラメーターである。
1 2 3 4 5 6 7 8 |
//2値を入れ替えるメソッド static void Swap<T>(ref T a, ref T b) { T temp; temp = a; a = b; b = temp; } |
呼び出し側は以下のようになる。<>に型引数を指定する。
1 2 3 4 5 |
int a = 2, b = 3; Console.WriteLine($"a={a} b={b}"); Swap<int>(ref a, ref b); Console.WriteLine($"a={a} b={b}"); |
ジェネリッククラス
ジェネリックなクラスを作成することもできる。whereについては後述の型パラメーターの制約を参照。
1 2 3 4 5 |
アクセス修飾子 class クラス名<型パラメーター> where 型パラメーター:制約条件 { メンバー定義 } |
以下の例では単純なクラスを用いているが、実際にクラスでジェネリックを使う場合は、コレクションクラスなどの集合を表すクラスが多い。ジェネリッククラスは、主にコレクションクラスのために存在している。
1 2 3 4 5 6 7 8 |
ItemContainer<int> itemInt = new ItemContainer<int>(123); //型引数にintを指定 ItemContainer<string> itemString = new ItemContainer<string>("Item"); //型引数にstringを指定 Console.WriteLine($"{itemInt.Item}"); //123 Console.WriteLine($"{itemString.Item}"); //Item //List<T>など、ほかのジェネリッククラスの型引数にジェネリッククラスを指定することもできる。 List<ItemContainer<int>> itemIntList = new List<ItemContainer<int>> { itemInt, new ItemContainer<int>(456) }; |
以下はジェネリッククラスに静的フィールドを定義した例。型引数が違うと、違う型として認識されていることがわかる。
変数の宣言には型推論(var)を使うことができる。冗長性を避けるため通常はvarで宣言する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
internal class ItemContainer2<T> { public static int TotalId { get; private set; }//静的フィールド public int Id { get; } public T Item { get; } public ItemContainer2(T item) { TotalId++; Id = TotalId; Item = item; } } |
1 2 3 4 5 6 7 |
var itemInt2_1 = new ItemContainer2<int>(123);//型引数にintを指定 var itemInt2_2 = new ItemContainer2<int>(456);//型引数にintを指定 var itemString2 = new ItemContainer2<string>("Item"); //型引数にstringを指定 Console.WriteLine($"{itemInt2_1.Id} {itemInt2_1.Item}");//1 123 Console.WriteLine($"{itemInt2_2.Id} {itemInt2_2.Item}");//2 456 Console.WriteLine($"{itemString2.Id} {itemString2.Item}");//1 Item |
以下のように、型パラメーターを複数定義することもできる。
1 2 3 4 5 6 7 8 9 10 11 12 |
internal class ItemContainer3<T1,T2> { public T1 Code { get; } public T2 Item { get; } public ItemContainer3(T1 code,T2 item) { Code = code; Item = item; } } |
1 2 3 4 5 |
var itemInt3_1 = new ItemContainer3<int,string>(1,"Item1");//型引数にintとstringを指定 var itemString3 = new ItemContainer3<string,string>("IC0001","Item1");//型引数にstringとstringを指定 Console.WriteLine($"{itemInt3_1.Code} {itemInt3_1.Item}"); //1 Item1 Console.WriteLine($"{itemString3.Code} {itemString3.Item}"); //IC0001 Item1 |
型パラメーターの制約
whereで制約条件を指定すると、引数にとれる型を限定することができる。
どんな型でも引数にできてしまうと、引数の型が持っていない機能をメソッドやクラス内で使ったときにエラーになってしまう。
制約にはいろいろな種類があるが、以下の例はインターフェイス制約を使った例である。
1 2 3 4 5 |
static Type Max<Type>(Type a, Type b) where Type : IComparable //インターフェイス制約 { return a.CompareTo(b) > 0 ? a : b; } |
上記のコードでは、メソッド内でCompareToを使っている。CompareToを使うには、型がIComparableを実装している必要がある。
(※インターフェイスは規定されたメソッドの実装を強制する働きがある。IComparableを実装した型は、CompareToメソッドを実装しなければならない。IComparableを実装している型は必ずCompareToメソッドを持っている。)
つまり、上記のメソッドの場合、IComparableを実装している型しか引数にとることはできない。
したがって、CompareToを使うには、制約条件としてIComparableを指定する必要がある。この制約条件がないと、コンパイルエラーとなってしまう。
なお、ジェネリックでは型引数に必ずしも演算子が定義されているわけではないため、演算子は使用できなくなる。ただし、C#11のGeneric Mathの機能を使うことで、演算子を使用できるようになる。
制約には以下のような種類がある。
詳しくは「型パラメーターの制約 – C# プログラミング ガイド | Microsoft Docs」を参照
種類 | 説明 |
where T : struct | Tは値型でなければならない。 Tにはnull許容値型は使用できない。 他の制約がある場合、先頭に置く必要がある。 |
where T : class | Tは参照型でなければならない。 他の制約がある場合、先頭に置く必要がある。 |
where T : new() | Tはパラメーターなしのパブリック コンストラクターを持たなければならない。 他の制約がある場合、最後に置く必要がある。 |
where T :クラス名 | Tは指定されたクラスであるか、そのクラスを継承していなければならない。 |
where T : インターフェイス名 | Tは指定されたインターフェイスであるか、そのインターフェイスを実装していなければならない。 複数のインターフェイス制約を指定することができる。 |
where T : unmanaged | Tはアンマネージ型でなければならない。 |
where T : Enum | Tは列挙型でなければならない。 |
where T : Delegate | Tはデリゲート型でなければならない。 |
where T : notnull | Tは非 null な型でなければならない。 |
型引数を複数指定する場合や、制約条件を複数指定する場合は以下のようにする。
1 2 3 4 5 |
戻り値の型 メソッド名<T1,T2>(T1 a, T2 b) where T1 : 制約条件1,制約条件2 where T2 : 制約条件1,制約条件2,制約条件3 { } |
すべての型の基底クラスであるobject型で定義されているメソッドは、制約なしで呼び出すことができる。
1 2 3 4 5 6 7 8 |
string x1 = "aiu"; ToString<string>(x1); void ToString<T>(T x) { Console.WriteLine(x.ToString()); } |
デフォルト値(ゼロ系の値)を指定する
型のデフォルト値は、数値型であれば0であり、参照型であればnull、bool型ではfalseとなる。
型パラメーターが何の型であるかはコンパイル時にはわからないため、コードにリテラルのデフォルト値を用いることはできない。
ジェネリックでデフォルト値を表すには、default(T)を用いる。
1 2 3 4 5 |
internal class ItemContainer<T> { public T Item { get; } = default(T); } |
コメント