ジェネリックを使うと、型やメソッドを一度書くだけで、型安全性を失わずに多くの型で使えます。キャストが必要で何でも受け入れる object の List の代わりに、それぞれコンパイラが確認する List<int> や List<Order> が得られます。山かっこの中の仮の型(慣例では T)は型パラメーターで、呼び出し側が与える型は型引数です。
ジェネリックが必要な理由:解決する問題
ジェネリック以前(C# 1)は、コレクションは object を格納していました。それでコンパイルはできましたが、間違いは実行時にしか表に出ず、すべての int は入れるときにボックス化されていました。
var prices = new System.Collections.ArrayList();
prices.Add(9.99m);
prices.Add("12.50"); // compiles: anything is an object
decimal total = 0;
foreach (object p in prices)
total += (decimal)p; // InvalidCastException on the string
List<decimal> なら、2つ目の Add はコンパイルエラーになり、ループにキャストは不要で、decimalはdecimalとして格納されます。
ジェネリッククラス
クラス名の後に型パラメーターを宣言し、クラスの中では他の型と同じように使います。
出力:
Page 2/3: Chen, Dara, more: True
Page 1/1: 90, 72, 85, more: False
Page<string> と Page<int> は、1つの定義から作られた2つの異なる型です。クラスは複数の型パラメーターを取れます(Dictionary<TKey, TValue>)。1つなら T、複数なら TSomething という名前にします。
ジェネリックメソッドと型推論
メソッドは、クラスがジェネリックかどうかに関係なく、独自の型パラメーターを持てます。コンパイラが引数から推論するので、呼び出し側は通常、型引数を省略します。
出力:
2 1
back front
ho ho ho
4.50 EUR, 12.00 EUR, 0.99 EUR
推論は引数からだけ行われ、戻り値の型からは決して行われません。Repeat(0, 3) に注釈は不要ですが、T Create<T>() のようなメソッドには推論の元になるものがないので、呼び出し側は Create<Customer>() と書かなければなりません。LINQ の大部分は、この推論に頼るジェネリックメソッドです。prices.Where(p => p > 1) は、書かなくても Where<decimal> です。
制約:Tに許されるもの
ジェネリックメソッドの中では、コンパイラは、ありうるすべての T で動くことしか許しません。制約がなければ、それは object が対応するものとほとんど変わりません。CompareTo を呼んだり、インスタンスを作ったり、プロパティを読んだりするには、where で T が何でなければならないかをコンパイラに伝えます。
出力:
17
plum
19.99
42 Zoe
使うことになる制約:
| 制約 | 意味 | できるようになること |
|---|---|---|
where T : class | 参照型 | null の代入、as の使用 |
where T : struct | null非許容の値型 | T? を Nullable<T> として使う |
where T : new() | 公開の引数なしのコンストラクター | new T() の呼び出し(最後に書く必要がある) |
where T : Entity | Entity から派生 | Entity のメンバーを使う |
where T : IComparable<T> | インターフェースを実装 | CompareTo の呼び出し |
where T : U | 別の型パラメーターから派生 | 2つのパラメーターを関連付ける |
where T : unmanaged(C# 7.3) | 中に参照を持たない構造体 | ポインター、stackalloc |
where T : Enum(C# 7.3) | enumの型 | Enum のメソッドに渡す |
where T : notnull(C# 8) | null非許容の型 | null許容参照の注釈 |
1つのパラメーターに対する複数の制約はカンマで区切り、複数のパラメーターにはそれぞれ独自の where 句を付けます:class Cache<TKey, TValue> where TKey : notnull where TValue : class。
制約が足りないと、メンバーを使った箇所でコンパイルエラーとして現れます。たとえばCS1061('T' does not contain a definition for 'CompareTo')や、new() なしで new T() を呼んだ場合のCS0304です。
default(T)
ジェネリックなコードは、知らない型の「空の値」を必要とすることがあります。default(T) は、数値なら 0、bool なら false、参照型とnull許容型なら null、それ以外ならゼロにされた構造体を返します。
出力:
31
0
Porto
True
0001-01-01
2行目はこのパターンの弱点を示しています。値型では「見つからなかった」と「0が見つかった」が同じに見えます。だからフレームワークのメソッドは代わりに TryGet の形(bool TryGetValue(TKey key, out TValue value))を使います。C# 7.1以降、文脈から型が明らかなら、より短い return default; と書けます。
静的メンバーは構築された型ごと
構築された型(Counter<int>、Counter<string>)はそれぞれ、クラスの静的フィールドの独自のコピーを持ちます。
出力:
2
1
False
これはときどき役に立ち(T ごとに一度だけ計算される TypeInfo<T>.Name のような型ごとのキャッシュ)、共有のカウンターを1つ期待していた場合にはときどき驚かされます。共有の状態はジェネリックでないクラスに置きます。
ジェネリックコレクション
ジェネリックコレクションはほとんどの用途をまかないます。List<T>、Dictionary<TKey, TValue>、HashSet<T>、Queue<T>、Stack<T>、そしてそれらが共有するインターフェースの IEnumerable<T>、ICollection<T>、IList<T>、IReadOnlyList<T> です。メソッドは必要な最も狭いインターフェースを受け取り(ループするだけなら IEnumerable<T>)、具体的な型か読み取り専用の型を返します。
共変性と反変性
Dog は Animal ですが、List<Dog> は List<Animal> ではありません。
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs;
// error CS0029: Cannot implicitly convert type 'List<Dog>' to 'List<Animal>'
それが許されると、animals.Add(new Cat()) が犬のリストに猫を入れてしまいます。値を生成するだけのインターフェースは安全で、out で共変と宣言されています。
IEnumerable<Animal> readOnly = dogs; // fine: IEnumerable<out T>
IReadOnlyList<Animal> alsoFine = dogs; // fine: IReadOnlyList<out T>
その鏡像である反変性は in で示され、値を消費するだけのインターフェースやデリゲートに当てはまります。どんな動物でも扱えるものは犬も扱えるので、Action<Dog> が期待される場所で Action<Animal> を使えます。IComparer<in T> は反変で、Func<in T, out TResult> は両方の印を使っています。変性が当てはまるのはインターフェースとデリゲートだけで、型引数が参照型の場合に限ります。IEnumerable<int> は IEnumerable<object> に変換されません。
よくある間違い
- 型パラメーターを使うべき場所で
objectを使う。 コンパイル時のチェックを失い、値型をボックス化します。 - 2つの
Tの値を==で比較する。class制約がなければコンパイルできず(CS0019)、あれば参照を比較します。すべてのTで動き、型自身のEqualsを尊重するEqualityComparer<T>.Default.Equals(a, b)を使います。 - 「見つからない」として
default(T)を返す。 値型ではあいまいです。bool TryX(..., out T value)を使います。 - 制約が多すぎる。 制約の1つ1つが、メソッドを使える人を狭めます。本体が実際に必要とするものだけを制約します。
List<Derived>がList<Base>に変換されると思う。 代わりにIEnumerable<Base>かIReadOnlyList<Base>を受け取ります。
よくある質問
C#のジェネリックとは何ですか?
ジェネリックを使うと、クラス、インターフェース、メソッドを、呼び出し側が埋める仮の型 T とともに一度だけ書けます:List<int>、List<Customer>。コンパイラはすべての使用を実際の型に対して確認するので、キャストなしで型安全性が得られ、値型はボックス化されずに格納されます。System.Collections.Generic のコレクションが最もなじみのある例です。
C#でジェネリックメソッドを書くには?
メソッド名の後に型パラメーターを置きます:static T Largest<T>(List<T> items) where T : IComparable<T> { ... }。コンパイラが引数から型引数を推論するので、呼び出し側は通常それを省略します。prices が List<decimal> なら Largest(prices) で動きます。推論の元になるものがない場合は、Largest<decimal>(...) と明示的に書きます。
C#のwhere T : classとはどういう意味ですか?
制約で、T は参照型でなければなりません。そのためジェネリックなコードは T に null を代入でき、as T を使えます。他の制約には、where T : struct(null非許容の値型)、where T : new()(公開の引数なしのコンストラクターを持つので、コードが new T() を呼べる)、where T : SomeBaseClass、そして T に対してそのインターフェースのメンバーを呼べるようにする where T : ISomeInterface があります。
C#のdefault(T)とは何ですか?
T が何の型になっても、その既定値です。数値なら 0、bool なら false、参照型とnull許容型なら null、その他の値型ならゼロにされた構造体です。ジェネリックなコードが、型を知らずに「値がない」を必要とするとき、たとえば検索が失敗したときの戻り値に使います。C# 7.1では、より短い default リテラルが追加されました。
C#でList<Dog>をList<Animal>に代入できないのはなぜですか?
List<Animal> はどんな動物でも受け入れるからです。代入が許されると、コードは実際には List<Dog> であるものに Cat を追加できてしまいます。コンパイラはそれを拒否します(CS0029)。読み取り専用のインターフェースは共変なので、それらを通しては何も追加できないため、IEnumerable<Animal> animals = dogs; も IReadOnlyList<Animal> animals = dogs; もコンパイルできます。