Menu

C#のジェネリック:ジェネリッククラス、ジェネリックメソッド、制約

C#のジェネリックの仕組みを解説します。型パラメーターを使うジェネリッククラスとメソッドの書き方、型推論、where(class、struct、new()、基底クラス、インターフェース)による制約、default(T)、型ごとの静的メンバー、そしてIEnumerable<out T>による共変性を扱います。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

ジェネリックを使うと、型やメソッドを一度書くだけで、型安全性を失わずに多くの型で使えます。キャストが必要で何でも受け入れる 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 : structnull非許容の値型T? を Nullable<T> として使う
where T : new()公開の引数なしのコンストラクターnew T() の呼び出し(最後に書く必要がある)
where T : EntityEntity から派生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; もコンパイルできます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める