みなさんこんにちは!ヒロポンです!
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本構成。
忙しい人向けに最初にまとめ
- 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仲間いたら、どんどんシェア待ってるぜ!!
執筆者
バイブス父さん — 業務 SE 7 年 (正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。
🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


コメント