C# のジェネリクスを書いていると、List<T> の T のように 「何でも入る型パラメーター」 だけでは困る場面が出てきます。たとえば「T のインスタンスを new T() で作りたい」「T を null と比較したい」「T の Dispose() を呼びたい」など、T に対して何らかの能力を仮定したい とき、コンパイラに「T はこういう型に限る」と教えてあげる必要があります。
そのための仕組みが 型制約(Constraining Type Parameters) で、ジェネリック宣言の末尾に where T : ... という形で書きます。
この記事では、以下の代表的な 5 つの制約を取り上げ、それぞれが何を意味するのか・いつ使うのか・どう書くのか を整理します。
where T : structwhere T : classwhere T : new()where T : NameOfBaseClasswhere T : NameOfInterface
なぜ型制約が必要なのか
制約のないジェネリックでは、T はあらゆる型に化け得るため、コンパイラは T について object が持っているメンバーしか 信用しません。
public static T CreateDefault<T>()
{
return new T(); // ❌ コンパイルエラー: T が引数なしのコンストラクタを持つとは限らない
}
T に「引数なしコンストラクタを持つ型に限る」という制約を付ければ、このコードは通るようになります。
public static T CreateDefault<T>() where T : new()
{
return new T(); // ✅ OK
}
このように、型制約は 「T にこれだけの能力があると約束するから、それを使った操作を許可してくれ」 とコンパイラに伝えるためのものです。同時に、呼び出し側にも「この型パラメーターには制約を満たす型しか渡せない」という保証が与えられます。
where T : struct — 値型に限定
T を 値型(struct、enum、プリミティブ型など) に限定する制約です。Nullable<T>(int? など)は 含まれません。
public static T Sum<T>(T a, T b) where T : struct
{
// T は値型だが、+ 演算子があるとは限らないので注意(後述)
return a; // ここでは型制約の例として
}
何が嬉しいのか
Tのデフォルト値は 常に意味のあるゼロ値(default(T)がnullにならない)Nullable<T>の型パラメーターに使える(Nullable<T>自体がwhere T : structを要求している)- ボックス化を避けやすい設計になる
典型的な使いどころ
Nullable<T> のように 「値型ラッパー」 を作るケースです。
public readonly struct Maybe<T> where T : struct
{
public bool HasValue { get; }
public T Value { get; }
public Maybe(T value)
{
HasValue = true;
Value = value;
}
}
var m = new Maybe<int>(42);
// var m2 = new Maybe<string>("x"); // ❌ string は参照型なので渡せない
注意点
where T : struct を付けても、T が + や * などの演算子を持つ保証はありません。算術演算を行いたい場合は C# 11 以降の 静的抽象メンバー(INumber<T> などの汎用数値インターフェース) を使うのが現代的です。
where T : class — 参照型に限定
T を 参照型(クラス、インターフェース、デリゲート、配列など) に限定する制約です。
public sealed class WeakRef<T> where T : class
{
private readonly WeakReference<T> _ref;
public WeakRef(T target) => _ref = new WeakReference<T>(target);
public T? Get() => _ref.TryGetTarget(out var t) ? t : null;
}
何が嬉しいのか
Tをnullと比較できる(t == null、t is null)T?のように null 許容参照型 として扱えるWeakReference<T>のように 参照型でしか意味を持たない API に渡せる
where T : class? との違い(nullable 対応)
null 許容参照型を有効にしたコンテキストでは、次の 2 つを区別します。
| 制約 | 意味 |
|---|---|
where T : class |
非 null な参照型に限る |
where T : class? |
null 許容も含めた参照型に限る |
呼び出し側が string? を渡せるようにしたいなら class?、null を許さない API として設計したいなら class を選びます。
典型的な使いどころ
- キャッシュやリポジトリなど 参照を保持してから使う 系のクラス
nullを「未設定」「未取得」のセンチネル値として使いたいケースeventハンドラのレジストリのように デリゲート(参照型) を扱う基盤
where T : new() — 引数なしコンストラクタを持つ型
T が public で引数なしのコンストラクタを持つ ことを要求する制約です。これを付けると new T() がジェネリックメソッド内で書けるようになります。
public static class Factory
{
public static T Create<T>() where T : new()
{
return new T();
}
}
var list = Factory.Create<List<int>>(); // ✅ List<int> は引数なしコンストラクタを持つ
// var sb = Factory.Create<string>(); // ❌ string は引数なしコンストラクタを持たない
注意点
new()制約は 引数なし のコンストラクタしか作れません。引数付きで作りたい場合は ファクトリデリゲート(Func<T>)を渡す のが定石です。abstractクラスはnew()制約を満たしません。- 構造体は常に「引数なしコンストラクタを暗黙に持つ」とみなされるため、
new()を満たします。
他の制約との順序ルール
new() は 常に最後 に書く必要があります。
// ✅ OK
public class Repo<T> where T : DbEntity, IIdentifiable, new() { }
// ❌ コンパイルエラー: new() は最後に書く
public class Repo<T> where T : new(), DbEntity { }
where T : NameOfBaseClass — 特定の基底クラスを継承
T が 指定したクラス、またはそれを継承するクラス であることを要求します。
public abstract class Animal
{
public abstract string Cry();
}
public sealed class Dog : Animal
{
public override string Cry() => "ワン";
}
public static class Zoo
{
public static void Shout<T>(T animal) where T : Animal
{
// T は Animal なので、Animal のメンバーが直接呼べる
Console.WriteLine(animal.Cry());
}
}
Zoo.Shout(new Dog()); // ✅
// Zoo.Shout("string"); // ❌ string は Animal を継承していない
何が嬉しいのか
Tを 基底クラスとして安全に扱える(キャスト不要でメンバーにアクセス可能)- 受け取った
Tをそのまま返すような API で、呼び出し側の具体型を保ったまま ジェネリックに扱える
T で受ける vs. 基底クラスで受ける
「単に基底クラスのメンバーを使いたいだけ」なら、わざわざジェネリックにせず引数を Animal 型で受ければ十分です。ジェネリックにする意味は次のような場合です。
// 受け取った T をそのままの型で返したい
public static T Echo<T>(T animal) where T : Animal => animal;
Dog d = Echo(new Dog()); // ✅ Dog のまま返ってくる(Animal にダウングレードされない)
注意点
sealedクラスを基底制約に指定することは できません(継承できない型を制約にしても、Tがその型自身に固定されてしまうため意味がない)。System.Object、System.Array、System.Delegateなどの特殊な基底型は制約に使えません(一部は新しい C# で緩和されています)。
where T : NameOfInterface — 特定のインターフェースを実装
T が 指定したインターフェースを実装する ことを要求します。基底クラス制約と並んで、もっとも実用頻度が高い制約です。
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}
int m = Max(3, 5); // ✅ int は IComparable<int> を実装
string s = Max("apple", "pear"); // ✅ string も IComparable<string> を実装
何が嬉しいのか
Tが持つインターフェースのメンバーを キャストや動的ディスパッチなしで 呼べる- 値型を渡しても ボックス化が起きない(インターフェース変数経由で呼ぶより高速)
典型的な使いどころ
- 比較・順序付け(
IComparable<T>、IEquatable<T>) - 列挙(
IEnumerable<T>) - リソース解放(
IDisposable) - C# 11 以降の 静的抽象メンバー(
INumber<T>、IAdditionOperators<T,T,T>など)
using System.Numerics;
public static T Sum<T>(IEnumerable<T> values) where T : INumber<T>
{
T total = T.Zero;
foreach (var v in values) total += v;
return total;
}
制約は組み合わせられる
複数の制約はカンマ区切りで列挙でき、順序にもルール があります。
public class Repository<T>
where T : class, // 1. 主要制約(class / struct / 基底クラス)は最初
IEntity, // 2. インターフェース制約は中間(複数可)
IComparable<T>,
new() // 3. new() は最後
{
// ...
}
ルールをまとめると次の通りです。
| 順序 | 制約の種類 | 個数 |
|---|---|---|
| 1 | class / struct / 基底クラス(いずれか 1 つ) |
0〜1 |
| 2 | インターフェース制約 | 0〜複数 |
| 3 | new() |
0〜1 |
class と struct は 同時に書けません(互いに排他)。struct を指定したときは new() を書く必要はありません(値型は常に引数なしコンストラクタを持つ)。
複数の型パラメーターに制約をかける
型パラメーターが複数あるときは、where 句を 型パラメーターごとに 1 つずつ 書きます。
public class Map<TKey, TValue>
where TKey : notnull
where TValue : class, new()
{
// ...
}
どの制約をいつ使うか — 早見表
| やりたいこと | 使う制約 |
|---|---|
T を null と比較したい / null を返したい |
where T : class |
default(T) を意味のあるゼロにしたい / Nullable<T> の中身にしたい |
where T : struct |
new T() でインスタンスを作りたい |
where T : new() |
T を特定の基底クラスのメンバーで操作したい |
where T : BaseClass |
T を比較・列挙・破棄など 能力単位 で扱いたい |
where T : IInterface |
T をそのままの具体型で受け渡ししたい |
ジェネリック化 + 基底クラス/インターフェース制約 |
まとめ
- 型制約は、ジェネリックの
Tに 「最低限これくらいの能力はある」 とコンパイラに伝える仕組み。 struct/classは 値型 / 参照型 の二者択一の主要制約。new()はnew T()を許可 する制約で、必ず最後に書く。- 基底クラス制約は 継承関係、インターフェース制約は 能力(できること) で
Tを絞り込む。 - 複数組み合わせるときは
class/struct/基底クラス → インターフェース →new()の順。
ジェネリックは「型に依存しない汎用コード」を書くための仕組みですが、何でも受け入れすぎると何もできない というジレンマがあります。型制約はそのバランスを取り、「必要な能力だけを要求して、それ以外は自由」 という設計を可能にする、ジェネリクスの要となる機能です。