C# のジェネリクス(ジェネリック)とは、型をあとから指定できるようにして、1つのメソッドやクラスをいろいろな型で安全に使い回すしくみです。 List<int> や Dictionary<string, int> の山かっこの中に書く型がそれで、定義側では T という「型の入れ物(型パラメーター)」として書きます。T は特別なキーワードではなく、Type の頭文字を取った慣習的な名前で、使うときに int や string などの具体的な型に置き換わります。

この記事では、「T とは何か」を object 版・型ごとの重複版・ジェネリック版の3つのコードを並べて確かめるところから始め、メソッド → クラス → インターフェイスの順に積み上げます。where 制約は全種類を一覧表にし、制約が足りないときに出るコンパイルエラー(CS0304、CS0019、CS0311 など)は番号から引けるようにしました。コードと実行結果・エラー文は、すべて .NET SDK 10.0.112(ランタイム 10.0.12、C# 14.0)で実際にビルド・実行したものです。

C#ジェネリクスの早見表

書きたいもの 書き方 使う側
ジェネリックメソッド static T First<T>(T[] items) First<int>(arr) または First(arr)
型パラメーター2つ TOut Convert<TIn, TOut>(TIn v, Func<TIn, TOut> f) Convert(42, v => "No." + v)
ジェネリッククラス class SimpleStack<T> { ... } new SimpleStack<string>()
ジェネリックレコード record Pair<TKey, TValue>(TKey Key, TValue Value); new Pair<string, int>("pen", 3)
ジェネリックインターフェイス interface IRepository<T> where T : IEntity IRepository<Item> repo = ...
制約を付ける where T : IComparable<T> 制約を満たす型だけ渡せる
複数の制約 where T : Animal, new() 基底クラス → インターフェイス → new() の順
既定値 default(T) または default int なら 0、参照型なら null
数値ならどれでも where T : INumber<T> int・double・decimal などで共通に計算
where 制約 渡せる型 T に対してできるようになること
where T : class 参照型 null との比較、as 演算子
where T : struct null 非許容の値型 T? が Nullable<T> になる
where T : new() 引数なしの public コンストラクターを持つ型 new T()
where T : notnull null 非許容の型 辞書のキーなどに使える
where T : unmanaged アンマネージド型(int、double、それらだけの構造体など) sizeof(T)、ポインター
where T : 基底クラス名 そのクラスか派生クラス 基底クラスのメンバー呼び出し
where T : インターフェイス名 そのインターフェイスを実装した型 インターフェイスのメンバー呼び出し
where T : struct, Enum 列挙型 Enum.GetNames<T>() など
where T : allows ref struct Span<T> などの ref struct も渡せる (できることを減らし、渡せる型を広げる)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「C#ジェネリクスの使い方|Tとは・メソッド・クラス・where制約一覧」

Tとは何かを3つのコードで比べる

「配列の先頭を返す」という同じ処理を、3通りの書き方で用意します。次のコードは Program.cs にそのまま貼って dotnet run で動かせます。

// 1) object版:何でも受け取れるが、取り出すときにキャストが必要
object first = Box.FirstObj(new object[] { 10, 20 });
int n1 = (int)first;
Console.WriteLine(n1);
try
{
    string s = (string)Box.FirstObj(new object[] { 10, 20 }); // コンパイルは通る
}
catch (InvalidCastException e)
{
    Console.WriteLine(e.GetType().Name + ": " + e.Message);
}

// 2) 型ごとの重複版:安全だが、型の数だけ同じ処理が増える
Console.WriteLine(Box.FirstInt(new[] { 10, 20 }));
Console.WriteLine(Box.FirstString(new[] { "a", "b" }));

// 3) ジェネリック版:1つの定義で、型ごとに安全
int n3 = Box.First<int>(new[] { 10, 20 });
string s3 = Box.First<string>(new[] { "a", "b" });
Console.WriteLine($"{n3} {s3}");
Console.WriteLine(Box.TypeName<int>() + " / " + Box.TypeName<string>());

static class Box
{
    public static object FirstObj(object[] items) => items[0];

    public static int FirstInt(int[] items) => items[0];
    public static string FirstString(string[] items) => items[0];

    public static T First<T>(T[] items) => items[0];
    public static string TypeName<T>() => typeof(T).Name;
}

実行結果:

10
InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.String'.
10
a
10 a
Int32 / String
書き方 よい点 気になる点
object 版 定義が1つで済む 取り出すたびにキャストが必要。型を取り違えてもコンパイルは通り、実行時に InvalidCastException になる。値型はボックス化(箱詰め)される
型ごとの重複版 型が合わなければコンパイルで気づける 型の数だけ同じ処理を書くことになる
ジェネリック版 定義が1つで、型の取り違えにコンパイルで気づける。キャスト不要 山かっこの書き方に慣れが必要

ジェネリック版の First<T> の T は、呼び出すときに決まります。First<int> と書けば「T を int に置き換えた First」、First<string> と書けば「T を string に置き換えた First」として動きます。最後の行の出力 Int32 / String は、typeof(T) が呼び出しごとに別の型になっていることを示しています。

型パラメーターの名前の付け方

名前 使う場面 例
T 型パラメーターが1つで、意味が明らかなとき List<T>
TKey、TValue 役割が分かれているとき Dictionary<TKey, TValue>
TIn、TOut / TResult 入力と出力 Func<T, TResult>
TEntity、TItem など 何を入れるかを名前で伝えたいとき IRepository<TEntity>

T で始まる名前にするのが .NET の慣習です。1文字の T でも、TKey のような説明的な名前でも、コンパイラにとっての意味は同じです。

ジェネリックメソッドの書き方

メソッド名のすぐあとに山かっこで型パラメーターを書くと、ジェネリックメソッドになります。型パラメーターは、引数・戻り値・メソッド本体のどこでも型として使えます。

int x = 1, y = 2;
Util.Swap<int>(ref x, ref y);
Console.WriteLine($"x={x}, y={y}");

string p = "left", q = "right";
Util.Swap(ref p, ref q);            // 型引数を省略(型推論)
Console.WriteLine($"p={p}, q={q}");

Console.WriteLine(Util.Describe("pen", 3));
Console.WriteLine(Util.Convert(42, v => "No." + v));

static class Util
{
    // 型パラメーターが1つ
    public static void Swap<T>(ref T a, ref T b)
    {
        T tmp = a;
        a = b;
        b = tmp;
    }

    // 型パラメーターが2つ
    public static string Describe<TKey, TValue>(TKey key, TValue value)
        => $"{typeof(TKey).Name}:{key} / {typeof(TValue).Name}:{value}";

    // 引数と戻り値で別の型パラメーターを使う
    public static TOut Convert<TIn, TOut>(TIn value, Func<TIn, TOut> f) => f(value);
}

実行結果:

x=2, y=1
p=right, q=left
String:pen / Int32:3
No.42
書く場所 例 意味
メソッド名の直後 Swap<T> 「このメソッドは型パラメーター T を持つ」という宣言
引数 ref T a T 型の引数
戻り値 TOut Convert<TIn, TOut>(...) TOut 型を返す
本体 T tmp = a; T 型のローカル変数

Swap(ref p, ref q) のように山かっこを省略できるのは、引数の型から T が string だとコンパイラが判断できるためです(型推論。くわしくは後述します)。

ジェネリッククラスの書き方

クラス名のあとに型パラメーターを書くと、クラス全体で T を使えます。フィールド、プロパティ、メソッドの引数・戻り値に同じ T を使うことで、「string を入れたら string が出てくる」ことをコンパイラが保証します。

var stack = new SimpleStack<string>();
stack.Push("A");
stack.Push("B");
Console.WriteLine($"{stack.Pop()} / 残り {stack.Count}");

var numbers = new SimpleStack<int>();
numbers.Push(10);
// numbers.Push("C");  // error CS1503: cannot convert from 'string' to 'int'
Console.WriteLine(numbers.Pop() + 1);

var pair = new Pair<string, int>("pen", 3);
Console.WriteLine(pair);

Console.WriteLine(typeof(SimpleStack<int>) == typeof(SimpleStack<string>));
Console.WriteLine(typeof(SimpleStack<>).Name + " / " + typeof(Pair<string, int>));

Counter<int>.Count++;
Counter<int>.Count++;
Counter<string>.Count++;
Console.WriteLine($"Counter<int>={Counter<int>.Count}, Counter<string>={Counter<string>.Count}");

class SimpleStack<T>
{
    private readonly List<T> _items = new();
    public int Count => _items.Count;
    public void Push(T item) => _items.Add(item);
    public T Pop()
    {
        T last = _items[^1];
        _items.RemoveAt(_items.Count - 1);
        return last;
    }
}

record Pair<TKey, TValue>(TKey Key, TValue Value);

static class Counter<T>
{
    public static int Count;
}

実行結果:

B / 残り 1
11
Pair { Key = pen, Value = 3 }
False
SimpleStack`1 / Pair`2[System.String,System.Int32]
Counter<int>=2, Counter<string>=1

この結果から、3つのことがわかります。

  • SimpleStack<int> と SimpleStack<string> は別の型です(typeof の比較が False)。SimpleStack<int> に文字列を Push しようとすると、error CS1503: Argument 1: cannot convert from 'string' to 'int' でビルドが止まります。
  • 型名の `1 や `2 は、型パラメーターの数を表す .NET 内部の表記です。
  • static フィールドは、型引数ごとに別々に持たれます。 Counter<int>.Count を2回、Counter<string>.Count を1回増やすと、それぞれ 2 と 1 になりました。「全体で1つのカウンター」にしたいときは、ジェネリックでないクラスに static フィールドを置きます。

なお、ジェネリック型を型引数なしで List a = null; のように書くと、error CS0305(型引数が1つ必要)になります。

ジェネリックインターフェイスの書き方

インターフェイスにも型パラメーターを付けられます。「データを保存・検索する入れ物」の約束事を1つ定義して、Item 用にも Member 用にも使う例です。

IRepository<Item> repo = new MemoryRepository<Item>();
repo.Add(new Item(1, "pen"));
repo.Add(new Item(2, "note"));
Console.WriteLine(repo.Find(2));
Console.WriteLine(repo.Find(9) is null ? "null" : "found");

IRepository<Member> members = new MemoryRepository<Member>();
members.Add(new Member(7, "Sato"));
Console.WriteLine(members.Find(7));

interface IEntity
{
    int Id { get; }
}

interface IRepository<T> where T : IEntity
{
    void Add(T item);
    T? Find(int id);
}

class MemoryRepository<T> : IRepository<T> where T : class, IEntity
{
    private readonly List<T> _list = new();
    public void Add(T item) => _list.Add(item);
    public T? Find(int id) => _list.FirstOrDefault(e => e.Id == id);
}

record Item(int Id, string Name) : IEntity;
record Member(int Id, string Name) : IEntity;

実行結果:

Item { Id = 2, Name = note }
null
Member { Id = 7, Name = Sato }

IRepository<T> に where T : IEntity という制約を付けたので、Find の中で e.Id を使えます。制約は「T に何ができるか」をコンパイラに伝える役割を持ちます。実装クラス側では class, IEntity と制約を足して、T? を「null かもしれない参照型」として扱えるようにしています。

型推論が効く場合と効かない場合・default(T)

// 型推論が効く:引数から T が決まる
Console.WriteLine(Util.Max(3, 7));
Console.WriteLine(Util.Max("apple", "banana"));

// 型引数を自分で書くこともできる
Console.WriteLine(Util.Max<double>(3, 7.5));

// 型推論が効かない:T が引数に現れない
List<Item> list = Util.Create<Item>();
Console.WriteLine(list.GetType().Name + " " + list.Count);
Console.WriteLine(Util.Parse<int>("123") + 1);

// default(T)
Console.WriteLine($"int:{Util.Def<int>()} bool:{Util.Def<bool>()} string:{Util.Def<string>() ?? "null"} DateTime:{Util.Def<DateTime>():yyyy-MM-dd}");
Console.WriteLine(Util.FirstOrDef(new int[0]) + " / " + (Util.FirstOrDef(new string[0]) ?? "null"));
Console.WriteLine(Util.IsDefault(0) + " " + Util.IsDefault(5) + " " + Util.IsDefault<string?>(null));

record Item(int Id, string Name);

static class Util
{
    public static T Max<T>(T a, T b) where T : IComparable<T>
        => a.CompareTo(b) >= 0 ? a : b;

    public static List<T> Create<T>() => new List<T>();

    public static T Parse<T>(string s) where T : IParsable<T> => T.Parse(s, null);

    public static T? Def<T>() => default;

    public static T? FirstOrDef<T>(T[] items) => items.Length > 0 ? items[0] : default(T);

    public static bool IsDefault<T>(T value)
        => EqualityComparer<T>.Default.Equals(value, default);
}

実行結果:

7
banana
7.5
List`1 0
124
int:0 bool:False string:null DateTime:0001-01-01
0 / null
True False True

型推論が効く条件

型推論は「引数の型」から T を決めるしくみです。戻り値の型や、代入先の変数の型は手がかりに使われません。

呼び出し 推論 理由
Util.Max(3, 7) 効く(T = int) 引数が int
Util.Max("apple", "banana") 効く(T = string) 引数が string
Util.Max(3, 7.5) 効く(T = double) int は double に暗黙変換できるため。ビルドが通ることを確認
Util.Create() 効かない T が引数に現れない
Util.Parse("123") 効かない 引数は string で、T は戻り値にしか現れない

効かない場合は、次のエラーになります(Create の例。Parse でも同じ CS0411 でした)。

error CS0411: The type arguments for method 'E.Create<T>()' cannot be inferred from the usage. Try specifying the type arguments explicitly.

CS0411 が出たら、Util.Create<Item>() のように型引数を自分で書きます。 List<Item> list = Util.Create(); のように代入先で型を示しても推論されない点が、つまずきやすいところです。

default(T)は型ごとの既定値を返す

T が何の型かわからない段階では、return null; とは書けません。T が int かもしれないからです。実際に書くと次のエラーになります。

error CS0403: Cannot convert null to type parameter 'T' because it could be a non-nullable value type. Consider using 'default(T)' instead.

エラー文が案内しているとおり、default(T)(または型を省略した default)を使うと、値型なら 0 や false、参照型なら null が返ります。

T default(T)
int、double など数値 0
bool False
DateTime 0001-01-01
string、クラス null
構造体 全フィールドが既定値

「値が既定値かどうか」を調べるときは、value == default とは書けません(T には == が定義されているとは限らず、CS0019 になります)。上のコードの IsDefault のように、EqualityComparer<T>.Default.Equals(value, default) を使うと、どの型でも比較できます。

where制約の全種類一覧

制約を付けると、渡せる型が絞られる代わりに、T に対してできることが増えます。次の一覧は、Microsoft Learn の「型パラメーターの制約」(英語版)を今回開いて確認した内容をもとにしています。

制約 渡せる型 組み合わせのルール
where T : struct null 非許容の値型(record struct を含む) new() を含むので new() とは併記しない。unmanaged とも併記しない
where T : class 参照型(クラス、インターフェイス、デリゲート、配列)。null 許容コンテキストでは null 非許容の参照型 先頭に書く
where T : class? 参照型(null 許容・非許容のどちらも) 先頭に書く
where T : notnull null 非許容の型(参照型・値型のどちらも) 先頭に書く
where T : unmanaged null 非許容のアンマネージド型 struct を含む。struct や new() とは併記しない
where T : new() 引数なしの public コンストラクターを持つ型 ほかの制約のあと、最後に書く
where T : 基底クラス名 そのクラスか、その派生クラス struct / class / notnull / unmanaged とは併記しない。インターフェイスより前
where T : 基底クラス名? 上と同じで、null 許容も可 同上
where T : インターフェイス名 そのインターフェイスを実装した型 複数書ける。ジェネリックなインターフェイスも可
where T : インターフェイス名? 上と同じで、null 許容の参照型も可 同上
where T : U 別の型パラメーター U と同じか、U から派生した型 ―
where T : default オーバーライドや明示的なインターフェイス実装で、制約なしの T であることを示す そうした場面専用
where T : allows ref struct ref struct も渡せる(制約を緩める「反制約」) すべての制約のあとに書く。class / class? とは併記しない
where T : System.Enum 列挙型 struct と組み合わせて struct, Enum と書くことが多い
where T : System.Delegate デリゲート型 ―

並べる順番は「struct・class・notnull・unmanaged のどれか1つ(または基底クラス)→ インターフェイス → new() → allows ref struct」です。順番を変えると、次のエラーになります。

error CS0401: The new() constraint must be the last restrictive constraint specified
error CS0449: The 'class', 'struct', 'unmanaged', 'notnull', and 'default' constraints cannot be combined or duplicated, and must be specified first in the constraints list.

(where T : new(), class と書いてビルドした結果です。)

制約ごとの使用例と実行結果

主な制約を1つずつ使ったコードです。sizeof(T) を使うため、プロジェクトファイル(.csproj)の PropertyGroup に AllowUnsafeBlocks を true で追加してからビルドしています。

Console.WriteLine(W.MakeNew<Item>().Name);                // new()
Console.WriteLine(W.Larger(3, 9) + " " + W.Larger("b", "a")); // インターフェイス
Console.WriteLine(string.Join(",", W.Names<Color>()));    // struct, Enum
Console.WriteLine(W.SizeOf<int>() + " " + W.SizeOf<double>() + " " + W.SizeOf<Point>()); // unmanaged
Console.WriteLine(W.OrNull<int>(false) is null ? "null" : "value"); // struct
Console.WriteLine(W.Describe(new Dog()));                 // 基底クラス
Console.WriteLine(W.Upcast<Dog, Animal>(new Dog()).GetType().Name); // T : U
Console.WriteLine(W.Key("pen", 3));                       // notnull
Console.WriteLine(W.Show<Span<int>>(stackalloc int[3]));  // allows ref struct
Console.WriteLine(W.Create<Dog>().Cry());                 // 複数の制約

class Item { public string Name { get; set; } = "new item"; }
enum Color { Red, Green, Blue }
struct Point { public int X; public int Y; }
class Animal { public virtual string Cry() => "..."; }
class Dog : Animal { public override string Cry() => "Wan"; }

static class W
{
    public static T MakeNew<T>() where T : new() => new T();

    public static T Larger<T>(T a, T b) where T : IComparable<T>
        => a.CompareTo(b) > 0 ? a : b;

    public static string[] Names<T>() where T : struct, Enum => Enum.GetNames<T>();

    public static unsafe int SizeOf<T>() where T : unmanaged => sizeof(T);

    public static T? OrNull<T>(bool has) where T : struct => has ? default(T) : null;

    public static string Describe<T>(T a) where T : Animal => typeof(T).Name + ":" + a.Cry();

    public static TBase Upcast<TDerived, TBase>(TDerived d) where TDerived : TBase => d;

    public static string Key<TKey, TValue>(TKey key, TValue value) where TKey : notnull
        => new Dictionary<TKey, TValue> { [key] = value }.Keys.First().ToString()!;

    public static string Show<T>(T value) where T : allows ref struct => typeof(T).Name;

    public static T Create<T>() where T : Animal, new() => new T();
}

実行結果:

new item
9 b
Red,Green,Blue
4 8 8
null
Dog:Wan
Dog
pen
Span`1
Wan
行 制約 制約があるからできること
MakeNew new() new T() でインスタンスを作る
Larger IComparable<T> a.CompareTo(b) を呼ぶ
Names struct, Enum Enum.GetNames<T>() に渡す
SizeOf unmanaged sizeof(T) でバイト数を得る(int は 4、double は 8、int 2つの構造体は 8)
OrNull struct T? が Nullable<T> になり、null を返せる
Describe 基底クラス Animal a.Cry() を呼ぶ
Upcast TDerived : TBase TDerived を TBase として返す
Key notnull Dictionary<TKey, TValue> のキーに使う
Show allows ref struct Span<int> を型引数として渡す
Create Animal, new() 作ってから基底クラスのメソッドを呼ぶ

allows ref structとnotnullの補足

Span<T> のような ref struct は、スタック上にしか置けない特別な構造体で、通常は型引数に渡せません。制約なしのメソッドに渡すと、次のエラーになります。

error CS9244: The type 'Span<int>' may not be a ref struct or a type parameter allowing ref structs in order to use it as parameter 'T' in the generic type or method 'E.Plain<T>(T)'

C# 13 で追加された allows ref struct を付けると、ref struct も受け取れるようになります。 代わりに、メソッドの中で T を object に変換する(ボックス化する)といった、ref struct にできない操作は書けなくなります。ほかの制約が「渡せる型を絞る」のに対し、これだけは「渡せる型を広げる」ので、反制約(anti-constraint)と呼ばれます。

notnull 制約に null 許容の型を渡した場合は、エラーではなく警告になります。

warning CS8714: The type 'string?' cannot be used as type parameter 'TKey' in the generic type or method 'E.Key<TKey, TValue>(TKey, TValue)'. Nullability of type argument 'string?' doesn't match 'notnull' constraint.

コンパイルエラーを番号から引く

制約が足りないとき・合わないときに出るエラーを、番号順にまとめました。メッセージはすべて .NET SDK 10.0.112 の dotnet build が出力した原文です(Visual Studio の日本語環境では、同じ番号で日本語のメッセージが表示されます)。

定義側(メソッドやクラスを書く側)で出るエラー

番号 書いたコード メッセージ(原文) 直し方
CS0019 a > b Operator '>' cannot be applied to operands of type 'T' and 'T' where T : IComparable<T> を付けて a.CompareTo(b) > 0。数値なら INumber<T>
CS0019 a + b Operator '+' cannot be applied to operands of type 'T' and 'T' where T : INumber<T> を付ける
CS0019 a == b Operator '==' cannot be applied to operands of type 'T' and 'T' EqualityComparer<T>.Default.Equals(a, b) を使う
CS0304 new T() Cannot create an instance of the variable type 'T' because it does not have the new() constraint where T : new() を付ける
CS0403 return null; Cannot convert null to type parameter 'T' because it could be a non-nullable value type. Consider using 'default(T)' instead. default を返す。または where T : class を付ける
CS1061 a.Cry() 'T' does not contain a definition for 'Cry' and no accessible extension method 'Cry' accepting a first argument of type 'T' could be found where T : Animal など、Cry を持つ型で制約する
CS0401 where T : new(), class The new() constraint must be the last restrictive constraint specified new() を最後にする
CS0449 同上 The 'class', 'struct', 'unmanaged', 'notnull', and 'default' constraints cannot be combined or duplicated, and must be specified first in the constraints list. class を先頭にする
CS0233 unsafe なしで sizeof(T) 'T' does not have a predefined size, therefore sizeof can only be used in an unsafe context メソッドに unsafe を付け、AllowUnsafeBlocks を有効にする
CS1961 interface IBad<out T> { void Take(T item); } Invalid variance: The type parameter 'T' must be contravariantly valid on 'IBad<T>.Take(T)'. 'T' is covariant. out の T は戻り値だけに使う。引数に使うなら in

制約のない T は「object にできることしかできない型」として扱われる、と覚えると、どのエラーも同じ理由だとわかります。演算子・new・メンバー呼び出しは、どれも「その型にそれがある」とコンパイラに伝える制約が必要です。

使う側(呼び出す側)で出るエラー

番号 書いたコード メッセージ(原文) 直し方
CS0305 List a = null; Using the generic type 'List<T>' requires 1 type arguments List<int> のように型引数を書く
CS0310 MakeNew<NoCtor>()(引数ありのコンストラクターだけの型) 'NoCtor' must be a non-abstract type with a public parameterless constructor in order to use it as parameter 'T' in the generic type or method 'E.MakeNew<T>()' 引数なしの public コンストラクターを用意する
CS0311 Larger(new Animal(), new Animal()) The type 'Animal' cannot be used as type parameter 'T' in the generic type or method 'E.Larger<T>(T, T)'. There is no implicit reference conversion from 'Animal' to 'System.IComparable<Animal>'. Animal に IComparable<Animal> を実装する
CS0315 Larger(new Point(), new Point())(構造体) The type 'Point' cannot be used as type parameter 'T' in the generic type or method 'E.Larger<T>(T, T)'. There is no boxing conversion from 'Point' to 'System.IComparable<Point>'. Point に IComparable<Point> を実装する
CS0315 Names<int>()(Enum 制約に int) The type 'int' cannot be used as type parameter 'T' in the generic type or method 'E.Names<T>()'. There is no boxing conversion from 'int' to 'System.Enum'. 列挙型を渡す
CS0411 Create() The type arguments for method 'E.Create<T>()' cannot be inferred from the usage. Try specifying the type arguments explicitly. Create<Item>() と型引数を書く
CS0452 Ref<int>(1)(class 制約に int) The type 'int' must be a reference type in order to use it as parameter 'T' in the generic type or method 'E.Ref<T>(T)' 参照型を渡す。または制約を見直す
CS0453 Val<string>("a")(struct 制約に string) The type 'string' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'E.Val<T>(T)' 値型を渡す。または制約を見直す
CS8377 Um<string>("a")(unmanaged 制約に string) The type 'string' must be a non-nullable value type, along with all fields at any level of nesting, in order to use it as parameter 'T' in the generic type or method 'E.Um<T>(T)' アンマネージド型を渡す
CS9244 Plain<Span<int>>(default) The type 'Span<int>' may not be a ref struct or a type parameter allowing ref structs in order to use it as parameter 'T'(後半省略) 定義側に allows ref struct を付ける
CS0029 List<object> a = new List<string>(); Cannot implicitly convert type 'System.Collections.Generic.List<string>' to 'System.Collections.Generic.List<object>' IEnumerable<object> で受ける(共変)
CS0266 IEnumerable<object> a = new List<int>(); Cannot implicitly convert type 'System.Collections.Generic.List<int>' to 'System.Collections.Generic.IEnumerable<object>'. An explicit conversion exists (are you missing a cast?) 共変は参照型どうしだけ。Cast<object>() などで変換する

CS0311 と CS0315 は同じ原因(制約のインターフェイスや基底クラスを満たしていない)で、渡した型が参照型なら CS0311、値型なら CS0315 になります。

INumberで数値ならどれでも使えるメソッドを書く

CS0019 の「T に + が使えない」を解決するのが、.NET 7 / C# 11 で追加されたジェネリック数学です。System.Numerics.INumber<T> を制約にすると、int・double・decimal などの数値型に共通で、+ や / 、T.Zero、T.One が使えます。

using System.Globalization;
using System.Numerics;

Console.WriteLine(M.Sum(new[] { 1, 2, 3 }));
Console.WriteLine(M.Sum(new[] { 1.5, 2.25 }));
Console.WriteLine(M.Sum(new[] { 0.1m, 0.2m }));
Console.WriteLine(M.Average(new[] { 1, 2, 4 }));
Console.WriteLine(M.Average(new[] { 1.0, 2.0, 4.0 }));
Console.WriteLine(M.Clamp01(1.7) + " " + M.Clamp01(-3));
Console.WriteLine(M.ParseSum<int>("1,2,3") + " " + M.ParseSum<double>("1.5,2.5"));

static class M
{
    public static T Sum<T>(IEnumerable<T> values) where T : INumber<T>
    {
        T total = T.Zero;
        foreach (T v in values)
        {
            total += v;
        }
        return total;
    }

    public static T Average<T>(T[] values) where T : INumber<T>
        => Sum(values) / T.CreateChecked(values.Length);

    public static T Clamp01<T>(T value) where T : INumber<T>
        => T.Clamp(value, T.Zero, T.One);

    public static T ParseSum<T>(string csv) where T : INumber<T>
        => Sum(csv.Split(',').Select(s => T.Parse(s, CultureInfo.InvariantCulture)));
}

実行結果:

6
3.75
0.3
2
2.3333333333333335
1 0
6 4
書き方 意味
where T : INumber<T> T は数値型。制約の中に T 自身が出てくる書き方がポイント
T.Zero、T.One その型の 0 と 1(静的メンバーを型パラメーターから呼ぶ)
total += v、a / b 演算子がそのまま使える
T.CreateChecked(values.Length) int などの別の数値型から T に変換する(範囲外なら例外)
T.Clamp(value, min, max) 範囲内に収める
T.Parse(s, provider) 文字列から T に変換する

Average の結果が、int の配列では 2(整数の割り算)、double の配列では 2.3333333333333335 になっている点に注目してください。ジェネリック数学でも、演算の意味は渡した型のルールに従います。 平均を小数で得たいときは、double に変換してから割るか、double の配列を渡します。

共変(out)と反変(in)

ジェネリック型は、型引数が違えば別の型です。string は object に代入できますが、List<string> を List<object> に代入することはできません(先ほどの CS0029)。もし代入できると、List<object> として数値を追加でき、中身が string だけという約束が崩れてしまうためです。

一方、「取り出すだけ」のインターフェイスや「受け取るだけ」のデリゲートは、安全な方向に限って代入できます。 これが共変と反変です。

// 共変(out):IEnumerable<string> を IEnumerable<object> として扱える
IEnumerable<string> strs = new List<string> { "a", "b" };
IEnumerable<object> objs = strs;
Console.WriteLine(string.Join("", objs));

// 反変(in):Action<object> を Action<string> として扱える
Action<object> printObj = v => Console.WriteLine("obj:" + v);
Action<string> printStr = printObj;
printStr("hello");

// Func の戻り値は共変
Func<string> getStr = () => "s";
Func<object> getObj = getStr;
Console.WriteLine(getObj());

// 自作インターフェイス
IProducer<Animal> producer = new DogProducer();
Console.WriteLine(producer.Produce().GetType().Name);
IConsumer<Dog> consumer = new AnimalConsumer();
consumer.Consume(new Dog());

class Animal { }
class Dog : Animal { }

interface IProducer<out T> { T Produce(); }
interface IConsumer<in T> { void Consume(T item); }

class DogProducer : IProducer<Dog>
{
    public Dog Produce() => new Dog();
}
class AnimalConsumer : IConsumer<Animal>
{
    public void Consume(Animal a) => Console.WriteLine("consume " + a.GetType().Name);
}

実行結果:

ab
obj:hello
s
Dog
consume Dog
種類 キーワード T の使い道 代入できる方向 標準ライブラリの例
共変 out 戻り値だけ 派生 → 基底(IEnumerable<string> → IEnumerable<object>) IEnumerable<out T>、IReadOnlyList<out T>、Func<out TResult>
反変 in 引数だけ 基底 → 派生(Action<object> → Action<string>) Action<in T>、IComparer<in T>
不変 なし 両方 同じ型だけ List<T>、IList<T>、Dictionary<TKey, TValue>

覚え方は、「出す(out)だけなら大きな型として見てよい、受け取る(in)だけなら小さな型専用として見てよい」です。Dog を作る係は「Animal を作る係」として通用し、Animal なら何でも受け取れる係は「Dog を受け取る係」として通用します。

  • out・in を付けられるのは、インターフェイスとデリゲートの型パラメーターです。
  • 共変・反変が働くのは参照型どうしだけです。IEnumerable<object> a = new List<int>(); は CS0266 になります(値型はボックス化が必要なため)。
  • out を付けた T を引数に使うと、CS1961 でビルドが止まります。
  • 逆方向(Action<string> 型の変数を Action<object> 型の変数へ代入)も CS0266 になることを確認しました。

つまずきやすい点と直し方

起きること 原因 直し方
T に対して + や > が書けない(CS0019) 制約なしの T は object と同じことしかできない INumber<T> や IComparable<T> で制約する
new T() が書けない(CS0304) コンストラクターがあるとは限らない where T : new()
return null が書けない(CS0403) T が値型かもしれない default を返す
型引数を省略したらエラー(CS0411) T が引数に現れず推論できない 山かっこで明示する
List<Dog> を List<Animal> に渡せない(CS0029) List<T> は不変 引数を IEnumerable<Animal> や IReadOnlyList<Animal> にする
static フィールドが共有されない 型引数ごとに別の型になる ジェネリックでないクラスに置く
制約の順番でエラー(CS0401・CS0449) 並べる順が決まっている class / struct → 基底クラス → インターフェイス → new()
int の平均が整数になる 演算は渡した型のルールに従う double で計算する

直す前と直した後

// 直す前:CS0019(T に > が使えない)
static T Larger<T>(T a, T b) => a > b ? a : b;

// 直した後:比較できる型だけに絞る
static T Larger<T>(T a, T b) where T : IComparable<T>
    => a.CompareTo(b) > 0 ? a : b;

直した後の Larger は、上の「制約ごとの使用例」で W.Larger(3, 9) が 9、W.Larger("b", "a") が b を返すことを確認しています。

よくある質問

Q. ジェネリクスとジェネリックはどちらが正しい呼び方ですか? A. どちらも同じものを指します。英語の generics(総称型のしくみ全体)をそのまま読んだのが「ジェネリクス」、Microsoft の日本語ドキュメントで使われているのが「ジェネリック」です。「ジェネリック メソッド」「ジェネリック クラス」のように個々の要素を指すときは後者がよく使われます。

Q. T 以外の文字を使ってもよいですか? A. 使えます。T はキーワードではなく、ただの名前です。型パラメーターが複数あるときや、役割を伝えたいときは TKey・TValue・TResult のように T で始まる説明的な名前にすると読みやすくなります。

Q. object を使う書き方と比べて、速度に違いはありますか? A. int などの値型を object として扱うと、ボックス化(ヒープ上に箱を作って値を入れる処理)と取り出し時のキャストが発生します。ジェネリクスでは型ごとに専用のコードが用意されるため、これらが発生しません。今回は速度の計測は行っていませんが、しくみとして余分な処理が減ることと、型の取り違えにコンパイルで気づけることが利点です。

Q. where 制約はいつ付ければよいですか? A. T に対して「メンバーを呼びたい」「new したい」「演算したい」「null を扱いたい」と思ったときです。制約を付けずに書き始めて、CS0019・CS0304・CS1061 などが出たら、そのエラーを解消する制約を足す、という順番で進めると、必要以上に絞らずに済みます。

まとめ

  • ジェネリクスは、型をあとから指定できるようにするしくみ。T は Type の頭文字を取った慣習的な名前で、使うときに具体的な型へ置き換わる
  • object 版は実行時に InvalidCastException、ジェネリック版はコンパイル時に気づける。定義は1つで済む
  • メソッドは名前の直後、クラス・インターフェイスは型名の直後に型パラメーターを書く。型引数が違えば別の型になり、static フィールドも別々
  • 型推論は引数の型だけを手がかりにする。T が引数に現れないときは CS0411 になるので、山かっこで明示する
  • 制約のない T は object と同じことしかできない。new は new()、比較は IComparable<T>、四則演算は INumber<T> で制約する
  • 制約の順番は「class・struct など → 基底クラス → インターフェイス → new() → allows ref struct」
  • out は戻り値だけ(共変)、in は引数だけ(反変)。List<T> は不変なので、受け取る側は IEnumerable<T> にすると柔軟になる

参考・出典

  • Microsoft Learn「Constraints on type parameters」(英語版):where 制約の一覧と組み合わせ・順番のルール、Enum・Delegate・unmanaged・allows ref struct の説明
  • Microsoft Learn「ジェネリック クラス」(日本語版):ジェネリッククラスの定義と継承のルール
  • 実行結果とエラー文は、.NET SDK 10.0.112(ランタイム 10.0.12、コンパイラが報告した言語バージョン 14.0、Linux)で取得

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「C#ジェネリクスの使い方|Tとは・メソッド・クラス・where制約一覧」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?