型パラメータ T には制約を付けることで、T に対して呼び出せる操作を増やせます。制約がない T は object のメンバー(ToString など)しか使えませんが、制約を付けることで特定のインターフェイスのメソッドや new() などが使えるようになります。
T で使える操作の限界を説明できるwhere T : インターフェイス名 で操作を追加できるclass・struct・new() の各制約の意味を理解できる制約がない T に対して使える操作は object のメンバーだけです。
1
2
3
4
5
6
7
8
static T Max<T>(T a, T b)
{
// ❌ コンパイルエラー: T に > 演算子がない
return a > b ? a : b;
// ❌ コンパイルエラー: T に CompareTo がない
return a.CompareTo(b) >= 0 ? a : b;
}
比較や特定の操作を行いたい場合は型制約で T が満たすべき条件を指定します。
書式:型制約
1
型パラメータ名 メソッド名<T>(引数) where T : 制約
| 要素 | 説明 |
|---|---|
where T : |
型制約の開始キーワード |
制約 |
インターフェイス名・クラス名・class・struct・new() など |
where T : IComparable<T> を付けると、T に CompareTo メソッドが使えます。
1
2
3
4
static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}
1
2
int larger = Max(3, 7); // 7
string later = Max("abc", "xyz"); // xyz
1
2
7
xyz
int も string も IComparable<T> を実装しているため、どちらも渡せます。
class 制約 / struct 制約where T : class は参照型のみを許可します。null を扱うことが保証されます。
where T : struct は値型のみを許可します。null になり得ないことが保証されます。
1
2
3
4
5
6
// 参照型のみ受け付ける(null チェックが意味を持つ)
static T RequireNonNull<T>(T value) where T : class
{
if (value == null) throw new ArgumentNullException(nameof(value));
return value;
}
1
2
3
4
5
// 値型のみ受け付ける(ボクシングなしで使える)
static T Double<T>(T value) where T : struct
{
// ...
}
new() 制約where T : new() は、T がパラメータなしのコンストラクタを持つことを要求します。これにより、ジェネリッククラス内で new T() と書けるようになります。
1
2
3
4
static T CreateInstance<T>() where T : new()
{
return new T();
}
1
2
3
4
5
6
7
class Config
{
public int Timeout { get; set; } = 30;
}
Config cfg = CreateInstance<Config>();
Console.WriteLine(cfg.Timeout); // 30
1
30
インターフェイスだけでなく、特定のクラスを継承していることを制約にできます。
1
2
3
4
5
6
7
8
9
class Processor<T> where T : Stream
{
public void Process(T stream)
{
// T は Stream のメンバー(Read, Write など)を持つことが保証される
byte[] buffer = new byte[1024];
stream.Read(buffer, 0, buffer.Length);
}
}
複数の制約を同時に指定できます。インターフェイス制約と new() 制約を組み合わせる例です。
書式:複数制約
1
where T : 制約1, 制約2
1
2
3
4
5
static T CloneAndInitialize<T>() where T : ICloneable, new()
{
T instance = new T();
return (T)instance.Clone();
}
💡 ポイント:
class/struct制約は必ず最初に書く必要があります。
複数の型パラメータに個別に制約を付けることもできます。
1
2
3
4
5
6
static void Convert<TIn, TOut>(TIn input, out TOut output)
where TIn : IConvertible
where TOut : struct
{
output = (TOut)System.Convert.ChangeType(input, typeof(TOut));
}
T は object のメンバーしか使えないwhere T : IComparable<T> のようにインターフェイス制約でメソッドを呼び出せるwhere T : class — 参照型のみ許可where T : struct — 値型のみ許可where T : new() — new T() の呼び出しが可能になる次のメソッドにインターフェイス制約を追加して、コンパイルが通るようにしてください。
1
2
3
4
static void Print<T>(T value)
{
Console.WriteLine(value.Length); // Length がない
}
where T : new() 制約がない場合、new T() と書くとどうなりますか?
(応用)「参照型であり、かつパラメータなしコンストラクタを持つ」制約の書き方は?
Length を持つ標準インターフェイスはないため、直接はできません。IEnumerable などで Count() を使うか、独自インターフェイス interface IHasLength { int Length { get; } } を定義して where T : IHasLength とします。
コンパイルエラーになります。new() 制約がない T に対して new T() は書けません。
where T : class, new()(class 制約を先に書く)
共変・反変 では、ジェネリック型の代入互換性(IEnumerable<Derived> を IEnumerable<Base> に代入できるか)を制御する out・in キーワードを学びます。