みなさんこんにちは!ヒロポンです!
C# でジェネリック使ってて、こんな経験ないですか??
Repository<T>を書こうとしてnew T()でエラー出てきて詰まったT item = null;で「型 'T' を 'null' に変換できません」って怒られたwhere T : class, new()の 順序 をネットで調べても「とりあえずコピペ」で終わってる
俺も最初の業務系プロジェクトで DataAccess の共通化やろうとした時に、Repository<T> の new T() で MissingMethodException が出て、半日溶かしたんですよね。.NET Framework 4.7.2 / VS2019 / C# 7.3 の構成だったんですけど、当時はジェネリック制約の意味分かってなくて「なんで実行時に落ちるんや??」って状態でした。
ん?普通に new T() で動くんじゃないの??って思いますよね。
結論先出すと、ジェネリック制約は 5パターン に整理できて、書く 順序 も決まってます。今回は業務SE が Repository<T> 書ける状態にゴールを置いて、5パターン全部コード付きで解説します。コード6本構成。
TL;DR
- C# Generic 制約は5パターン:
where T : class/where T : struct/where T : new()/where T : IDisposable(Interface)/where T : EntityBase(基底クラス) - 制約の 書く順序:
classまたはstruct→ 基底クラス → Interface →new() - 業務SE 必修2パターン:
Repository<T> where T : class, new()とDisposable<T> where T : class, IDisposable T item = null;のエラーはwhere T : classで解決、default(T)でも代用可
なぜジェネリック制約が必要なのか — T item = null; がコンパイルエラーになる理由
ジェネリックメソッド書いてて、こういう書き方で詰まった人多いと思います。
// ❌ コンパイルエラー: CS0403
public static T GetDefault<T>()
{
T item = null; // 'T' を 'null' に変換できません
return item;
}
このエラー、ヒロポン的に「ん?普通に null 入れるだけやろ??」って最初思ったんですけど、コンパイラからすると T が値型(int / DateTime / struct)だった時に困る のが理由です。
値型に null は代入できないので、「T が値型でも参照型でも安全に書ける形」をコンパイラが要求してくる。対処は2択あります。
// ✅ 対処1: where T : class で参照型に絞る
public static T GetDefault<T>() where T : class
{
T item = null; // OK
return item;
}
// ✅ 対処2: default(T) を使う(制約なしでもOK)
public static T GetDefault2<T>()
{
T item = default(T); // 参照型なら null、値型なら 0 / false / DateTime.MinValue
return item;
}
業務系の Repository<T> で扱うのは大体エンティティクラス(参照型)なので、対処1の class 制約寄せが現実解。値型も含めて汎用的に書く場合は対処2の default(T) を使う、こんな感じで覚えておくと迷わなくなります!!
パターン1: where T : class — 参照型のみ許容
最頻出パターン。T を参照型(クラス / インターフェース / デリゲート / 配列)に絞ります。
// ✅ T は参照型のみ
public class Cache<T> where T : class
{
private readonly Dictionary<string, T> _store = new Dictionary<string, T>();
public T Get(string key)
{
if (_store.TryGetValue(key, out var value))
return value;
return null; // 参照型なので null 返せる
}
public void Set(string key, T value)
{
_store[key] = value;
}
}
// 使い方
var userCache = new Cache<User>(); // OK: User はクラス
var stringCache = new Cache<string>(); // OK: string も参照型
// var intCache = new Cache<int>(); // ❌ コンパイルエラー: int は値型
where T : class が付くと null 比較が自由にできるので、「キャッシュにヒットしなければ null」とか「DB に存在しなければ null」とかの業務系パターンが書きやすくなります。
俺の現場でも Cache<User> Cache<Order> みたいな共通基盤をこんな感じで書く時に多用するパターン。業務系の DataAccess 系コードはほぼこれです。
パターン2: where T : struct — 値型のみ許容
値型(int / DateTime / 構造体 / enum)のみを受け入れる制約。Nullable<T> のラッパーを書く時とかで使います。
// ✅ T は値型のみ(int / DateTime / struct / enum)
public class NullableWrapper<T> where T : struct
{
private T? _value; // Nullable<T> として宣言
public void Set(T value)
{
_value = value;
}
public bool HasValue => _value.HasValue;
public T GetValueOrDefault(T defaultValue)
{
return _value ?? defaultValue;
}
}
// 使い方
var intWrapper = new NullableWrapper<int>(); // OK: int は値型
var dateWrapper = new NullableWrapper<DateTime>(); // OK: DateTime は値型
// var strWrapper = new NullableWrapper<string>(); // ❌ string は参照型
ここでちょっとハマりやすいのが、where T : struct を付けると Nullable<T> 自体は除外される という挙動。Nullable
// ❌ Nullable<int> は Nullable<T> where T : struct の制約を満たさない扱い
// var wrapper = new NullableWrapper<int?>(); // CS0453: 値型でない
業務系で値型のラッパー書きたい時は where T : struct を使って、内部で T? を保持 するのが定石。Nullable
パターン3: where T : new() — 引数なしコンストラクタを持つ型のみ
これが業務SE のハマりポイント No.1。new T() で実行時例外が出る原因のやつです。
// ❌ 制約なしだと実行時 MissingMethodException リスクあり
public class FactoryBroken<T>
{
public T Create()
{
return new T(); // T が引数必須コンストラクタの型だと実行時エラー
}
}
// ✅ new() 制約でコンパイル時に保証
public class Factory<T> where T : new()
{
public T Create()
{
return new T(); // OK: T は引数なしコンストラクタを持つことが保証されてる
}
}
// 使い方
var factory = new Factory<User>(); // User クラスに引数なしコンストラクタが必要
var user = factory.Create(); // new User() 相当
new() 制約がないと、T が new T() できない型(引数必須のクラス)だった時に実行時 MissingMethodException で落ちます。これコンパイル時に検出できないので、new() 制約を付けてコンパイラに保証させるのが業務SE 鉄則。
俺も最初の業務系プロジェクトでこれやらかしました。Repository<T> の new T() で アプリ起動直後に死んで 半日デバッグ、原因が「Entity クラスに引数なしコンストラクタなかった」っていう。new() 制約を後付けでコンパイル時検出に切り替えたら、即気付ける構造になって学習体験としては大きかったです。
パターン4: where T : IDisposable — Interface 実装制約
Interface を制約に使うパターン。using で安全に使える T だけを許容します。
// ✅ T は IDisposable 実装のみ
public class DisposableScope<T> where T : IDisposable
{
private readonly T _resource;
public DisposableScope(T resource)
{
_resource = resource;
}
public void Use(Action<T> action)
{
using (_resource) // T が IDisposable なので using で囲める
{
action(_resource);
}
}
}
// 使い方
using (var conn = new SqlConnection("..."))
{
var scope = new DisposableScope<SqlConnection>(conn);
scope.Use(c => c.Open());
}
SqlConnection SqlCommand FileStream などの IDisposable 実装型を扱うラッパーを書く時に頻出。Interface 制約は 複数指定もできる ので、where T : IDisposable, IComparable<T> みたいに連ねるパターンもあります。
ハマりポイント1つ。as キャスト と組み合わせる時、where T : class 制約も同時に必要になります。is/as の挙動については C# Interface と is/as の暗黙的キャスト で詳しく書いてます。
パターン5: where T : EntityBase — 基底クラス制約
特定クラスの派生型のみを許容するパターン。業務系の Entity 階層を表現する時に使います。
// 基底クラス
public abstract class EntityBase
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
}
public class User : EntityBase
{
public string Name { get; set; }
}
public class Order : EntityBase
{
public decimal Amount { get; set; }
}
// ✅ T は EntityBase の派生のみ
public class Repository<T> where T : EntityBase, new()
{
public T FindById(int id)
{
var entity = new T(); // new() 制約で保証
entity.Id = id; // EntityBase 制約で Id プロパティ参照可能
// ... 実際は DB から取得して詰める
return entity;
}
}
// 使い方
var userRepo = new Repository<User>(); // OK: User は EntityBase 派生
var orderRepo = new Repository<Order>(); // OK: Order も派生
// var strRepo = new Repository<string>(); // ❌ string は EntityBase 派生じゃない
これが業務系 Repository<T> パターンの完成形。基底クラス制約 + new() 制約 で、「EntityBase 派生 + new T() できる型」だけを受け入れる Repository が書けます。
俺の現場でも DataAccess 共通化する時にこのパターンよく使うんですけど、最初は where T : EntityBase だけ書いて new() を忘れて MissingMethodException 出してた、っていう経験あります。基底クラス制約と new() 制約はセットで覚える のが現実解です。
複数制約の組み合わせ — 順序ルール
制約は複数組み合わせられますが、書く順序が決まってる のでそこだけ注意。
// 順序ルール: class または struct → 基底クラス → Interface → new()
// ✅ 正しい順序
public class FullRepo<T> where T : class, EntityBase, IDisposable, new()
{
// ...
}
// ❌ 順序が違うとコンパイルエラー (CS0449 等)
// public class WrongRepo<T> where T : new(), class, IDisposable // NG
業務SE が覚えておくべき組み合わせは、こんな感じで2つだけ。
// 組み合わせ1: 業務系 Repository<T>
public class Repository<T> where T : class, new() { /* ... */ }
// 組み合わせ2: IDisposable + using で安全に使える参照型
public class SafeScope<T> where T : class, IDisposable { /* ... */ }
この2パターン覚えておけば、業務系の DataAccess / Service 層の共通基盤は大体書けます。
ハマりポイント整理 — default(T) vs null の使い分け
ジェネリックメソッドで「未設定」を表現する時、null と default(T) のどっち使うか迷う場面があります。
public class GenericContainer<T>
{
private T _value;
// ❌ 制約なしで null 比較すると CS0019 など
// public bool IsEmpty() => _value == null; // T が値型だと比較できない
// ✅ default(T) を使えば制約不要
public bool IsEmpty() => EqualityComparer<T>.Default.Equals(_value, default(T));
// ✅ where T : class なら null 比較で OK
}
public class RefContainer<T> where T : class
{
private T _value;
public bool IsEmpty() => _value == null; // class 制約で null 比較可能
}
EqualityComparer<T>.Default を使うパターンは 制約なしで書ける汎用ヘルパー で、業務系では null 比較と default(T) 比較を意識せず書けるので便利。.NET Framework 4.7.2 でも標準で使えます。
まとめ
C# Generic 制約 5パターンの整理:
| 制約 | 意味 | 業務SE 使用頻度 |
|---|---|---|
where T : class |
参照型のみ | ★★★(Cache / Repository) |
where T : struct |
値型のみ | ★(Nullable ラッパー) |
where T : new() |
引数なしコンストラクタ持ち | ★★★(Repository とセット) |
where T : IDisposable |
Interface 実装 | ★★(using で安全に使う型) |
where T : BaseClass |
基底クラス派生 | ★★(Entity 階層) |
業務SE 必修パターン2つ:
Repository<T> where T : class, new()— DataAccess 共通化Disposable<T> where T : class, IDisposable— using で安全に扱う参照型
複数制約の 書く順序: class または struct → 基底クラス → Interface → new()。順序間違えるとコンパイルエラーになるので、ここだけ気を付けてください。
ジェネリック制約は最初取っ付きにくいけど、5パターンに整理してしまえば業務系で詰まる場面は減ります。Repository<T> で new T() 落ちた経験ある人は、where T : class, new() の組み合わせをコピペできる状態にしておくと、いい感じに防御できます。
よくある質問
Q1. T item = null; がコンパイルエラーになるのはなぜ?
A. ジェネリック T は値型(struct)である可能性があるためです。値型に null は代入できないので、コンパイラがエラーにします。対処は2択: (1)where T : class 制約を付けて T を参照型に絞る、または(2)default(T) を使う(参照型なら null、値型なら既定値)。業務系の Repository
Q2. new T() で MissingMethodException が出るのを防ぐには?
A. where T : new() 制約を付けることで防げます。この制約がないと、T が引数なしコンストラクタを持たない型だった時に実行時 MissingMethodException が出ます。new() 制約を付けるとコンパイル時に「T は new T() できる型のみ」と保証されるので、実行時例外が消えます。
Q3. 複数制約を書く時の順序は?
A. 順序ルールは「class または struct → 基底クラス → Interface → new()」。例えば where T : class, EntityBase, IDisposable, new() の順。順序を間違えるとコンパイルエラー(CS0449 等)になります。
Q4. where T : struct と Nullable<T> の関係は?
A. where T : struct は値型のみ許容しますが、Nullablewhere T : struct と書いて、内部で T? を使う、というのが定石です。
Q5. ジェネリック制約は実行時パフォーマンスに影響する?
A. 影響しません。ジェネリック制約はコンパイル時のみの検証で、ランタイムには影響しないので、ジェネリックメソッドの実行コストは制約あり / なしで同じです。ただし IDisposable 制約付きの T を仮想呼び出しすると JIT の挙動が変わる微細な差はありますが、業務系で気にするレベルじゃないので「制約は安心して付けてOK」と覚えておけば十分です。
関連記事
- C# Interface と is/as の暗黙的キャスト — 業務SE が型判定で詰まる時の整理 — Interface 制約と
is/asキャストの違いを補完 - C# のコールバックとデリゲートの違いはなんなのか — 業務SE が関数を引数で渡す時の整理 — ジェネリックメソッド + デリゲートの隣接トピック
- C# 例外処理のベストプラクティス — 業務SE が try-catch-finally と IDisposable で詰まる時の整理 —
where T : IDisposable+ using の隣接トピック
以上!
同じ罠でハマってる業務SE仲間いたら、どんどんシェア待ってるぜ!!


コメント