C#ジェネリクスとは?基本構文から型制約・使い方まで初心者向けにわかりやすく解説

はじめに

C#を学び始めると、List<int>Dictionary<string, int>のように、山かっこの中に型を書く構文をよく見かけます。これが「C#ジェネリクス」です。

ジェネリクスを理解すると、同じような処理をint用、string用、独自クラス用に何度も書く必要がなくなります。型安全で再利用しやすいコードを書けるようになるため、C#の実務開発でも非常によく使われます。

この記事では、C#ジェネリクスとは何か、基本構文、ジェネリッククラス、ジェネリックメソッド、型制約where、コレクションとの関係、実践サンプルまで初心者向けにわかりやすく解説します。

1. C#ジェネリクスとは?

1-1. ジェネリクスは「型をあとから指定できる仕組み」

C#ジェネリクスとは、クラスやメソッドを作るときに、具体的な型をあとから指定できる仕組みです。

たとえば、次のようなクラスがあるとします。

C#
public class Box<T>
{
public T Value { get; set; }
}

ここで使われているTは、あとから指定される型を表します。

C#
var intBox = new Box<int>();
intBox.Value = 100;

var stringBox = new Box<string>();
stringBox.Value = "Hello";

Box<int>ならTintとして扱われ、Box<string>ならTstringとして扱われます。

つまり、ジェネリクスを使うと「型はまだ決めないけれど、あとで指定された型として安全に扱う」ことができます。

1-2. なぜジェネリクスが必要なのか

ジェネリクスがない場合、型ごとに似たようなクラスやメソッドを作る必要があります。

C#
public class IntBox
{
public int Value { get; set; }
}

public class StringBox
{
public string Value { get; set; }
}

このように、int用、string用、DateTime用などを個別に作ると、同じようなコードが増えてしまいます。

ジェネリクスを使えば、次のように1つのクラスで複数の型に対応できます。

C#
public class Box<T>
{
public T Value { get; set; }
}

これにより、コードの重複を減らし、保守しやすいプログラムを書けます。

1-3. List<T>やDictionary<TKey, TValue>で使われている身近な例

C#ジェネリクスは、実は普段からよく使っているコレクションにも登場します。

代表的なのがList<T>です。

C#
List<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);

この場合、Tintです。つまり、List<int>intだけを入れられるリストになります。

C#
List<string> names = new List<string>();
names.Add("田中");
names.Add("佐藤");

こちらはTstringなので、文字列だけを入れられるリストです。

また、Dictionary<TKey, TValue>もジェネリクスです。

C#
Dictionary<string, int> scores = new Dictionary<string, int>();
scores["Alice"] = 90;
scores["Bob"] = 80;

TKeyはキーの型、TValueは値の型を表します。この例では、キーがstring、値がintです。

1-4. object型との違い

ジェネリクスと似たような目的で、object型を使う方法もあります。objectはすべての型の基底型なので、どんな値でも入れられます。

C#
public class ObjectBox
{
public object Value { get; set; }
}

一見便利そうですが、取り出すときにキャストが必要になります。

C#
var box = new ObjectBox();
box.Value = 100;

int number = (int)box.Value;

さらに、間違った型にキャストすると実行時エラーになります。

C#
string text = (string)box.Value; // 実行時エラー

一方、ジェネリクスを使うと、型がコンパイル時に決まります。

C#
var box = new Box<int>();
box.Value = 100;

int number = box.Value;

キャストが不要で、間違った型を入れようとするとコンパイルエラーになります。

C#
box.Value = "Hello"; // コンパイルエラー

このように、ジェネリクスはobjectよりも型安全にコードを書けます。

1-5. ジェネリクスを使うメリット

C#ジェネリクスを使う主なメリットは、次のとおりです。

1つ目は、型安全なコードを書けることです。指定した型以外の値を入れられないため、実行時エラーを減らせます。

2つ目は、キャストが不要になることです。object型を使う場合と違い、値を取り出すたびにキャストする必要がありません。

3つ目は、コードの再利用性が高まることです。1つのクラスやメソッドで複数の型に対応できます。

4つ目は、可読性が上がることです。List<string>と書かれていれば、「文字列のリスト」であることがすぐにわかります。

5つ目は、パフォーマンス面でも有利な場合があることです。特に値型をobjectとして扱う場合に発生するボックス化を避けやすくなります。

2. C#ジェネリクスの基本構文

2-1. 型パラメーターTとは

ジェネリクスでよく使われるTは、型パラメーターと呼ばれます。

C#
public class Box<T>
{
public T Value { get; set; }
}

Tは「Type」の頭文字として使われることが多く、あとから指定される型の仮の名前です。

Box<int>と書いた場合、Tintになります。

C#
Box<int> box = new Box<int>();

Box<string>と書いた場合、Tstringになります。

C#
Box<string> box = new Box<string>();

型パラメーター名は必ずTである必要はありません。ただし、1つだけ型を受け取る場合はTがよく使われます。

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

ジェネリッククラスは、クラス名の後ろに<T>を付けて定義します。

C#
public class DataHolder<T>
{
public T Data { get; set; }

public void Show()
{
Console.WriteLine(Data);
}
}

使う側では、具体的な型を指定します。

C#
var holder = new DataHolder<string>();
holder.Data = "C#ジェネリクス";
holder.Show();

この例では、Tstringとして扱われます。

2-3. ジェネリックメソッドの書き方

クラス全体ではなく、メソッドだけをジェネリックにすることもできます。

C#
public void ShowValue<T>(T value)
{
Console.WriteLine(value);
}

呼び出すときは、型を指定できます。

C#
ShowValue<int>(100);
ShowValue<string>("Hello");

多くの場合、C#が引数から型を推論してくれるため、型指定を省略できます。

C#
ShowValue(100);
ShowValue("Hello");

2-4. 複数の型パラメーターを使う方法

ジェネリクスでは、複数の型パラメーターを使うこともできます。

C#
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
}

使うときは、それぞれの型を指定します。

C#
var pair = new Pair<string, int>();
pair.First = "年齢";
pair.Second = 30;

Dictionary<TKey, TValue>も、複数の型パラメーターを使う代表例です。

C#
Dictionary<string, int> scores = new Dictionary<string, int>();

型パラメーターが複数ある場合は、TKeyTValueTItemのように意味がわかる名前を付けると読みやすくなります。

2-5. 型推論で型指定を省略できるケース

ジェネリックメソッドでは、引数から型がわかる場合、型指定を省略できます。

C#
public T Echo<T>(T value)
{
return value;
}

本来は次のように呼び出せます。

C#
int number = Echo<int>(10);

しかし、引数10からTintだと判断できるため、次のように書けます。

C#
int number = Echo(10);

文字列の場合も同じです。

C#
string text = Echo("Hello");

ただし、戻り値だけでは型推論できないケースがあります。

C#
public T CreateDefault<T>()
{
return default;
}

このようなメソッドは引数から型を判断できないため、型を明示する必要があります。

C#
int number = CreateDefault<int>();

3. ジェネリッククラスの使い方

3-1. 基本的なジェネリッククラスの例

まずは、値を1つ保持するシンプルなジェネリッククラスを作ってみましょう。

C#
public class Box<T>
{
public T Value { get; set; }

public Box(T value)
{
Value = value;
}

public void Print()
{
Console.WriteLine(Value);
}
}

このクラスは、Tに指定された型の値を保持できます。

C#
var box = new Box<int>(123);
box.Print();

Box<int>として使えば、Valueint型になります。

3-2. int・string・独自クラスを指定する例

同じBox<T>クラスを、さまざまな型で使ってみます。

C#
var intBox = new Box<int>(100);
var stringBox = new Box<string>("Hello");

独自クラスにも使えます。

C#
public class User
{
public string Name { get; set; }
}
C#
var user = new User { Name = "山田" };
var userBox = new Box<User>(user);

Console.WriteLine(userBox.Value.Name);

このように、ジェネリッククラスは組み込み型だけでなく、自分で作ったクラスにも利用できます。

3-3. プロパティやフィールドでTを使う方法

型パラメーターTは、プロパティやフィールドの型として使えます。

C#
public class ItemStore<T>
{
private T _item;

public T Item
{
get { return _item; }
set { _item = value; }
}
}

ItemStore<string>なら、_itemItemstring型です。

C#
var store = new ItemStore<string>();
store.Item = "商品A";

ItemStore<int>なら、int型として扱われます。

C#
var numberStore = new ItemStore<int>();
numberStore.Item = 10;

3-4. 戻り値や引数にTを使う方法

Tは、メソッドの引数や戻り値にも使えます。

C#
public class Repository<T>
{
private readonly List<T> _items = new List<T>();

public void Add(T item)
{
_items.Add(item);
}

public T GetFirst()
{
return _items[0];
}
}

使う側では、指定した型に応じてメソッドの引数や戻り値の型が決まります。

C#
var repository = new Repository<string>();
repository.Add("Apple");

string first = repository.GetFirst();

この場合、Addの引数はstringGetFirstの戻り値もstringになります。

3-5. ジェネリッククラスを使うと便利な場面

ジェネリッククラスは、特定の型に依存しない共通処理を作りたいときに便利です。

たとえば、次のような場面でよく使われます。

データを一時的に保持するクラス、一覧を管理するクラス、検索や保存を行うリポジトリクラス、APIレスポンスを表すクラス、処理結果を表すクラスなどです。

C#
public class ApiResponse<T>
{
public bool Success { get; set; }
public string Message { get; set; }
public T Data { get; set; }
}

このようにしておけば、ユーザー情報を返すAPIにも、商品情報を返すAPIにも使えます。

C#
ApiResponse<User> userResponse = new ApiResponse<User>();
ApiResponse<List<string>> namesResponse = new ApiResponse<List<string>>();

4. ジェネリックメソッドの使い方

4-1. メソッドだけをジェネリックにする基本例

クラス全体をジェネリックにしなくても、メソッドだけをジェネリックにできます。

C#
public class Printer
{
public void Print<T>(T value)
{
Console.WriteLine(value);
}
}

呼び出し例は次のとおりです。

C#
var printer = new Printer();

printer.Print<int>(100);
printer.Print<string>("Hello");
printer.Print<DateTime>(DateTime.Now);

このように、同じPrintメソッドでさまざまな型を扱えます。

4-2. 引数と戻り値にTを使う例

ジェネリックメソッドでは、引数と戻り値の両方にTを使えます。

C#
public T GetSameValue<T>(T value)
{
return value;
}

呼び出し例です。

C#
int number = GetSameValue(10);
string text = GetSameValue("C#");

Tintなら戻り値もintTstringなら戻り値もstringになります。

また、2つの値を入れ替えるメソッドもジェネリックで書けます。

C#
public void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
C#
int x = 1;
int y = 2;

Swap(ref x, ref y);

Console.WriteLine(x); // 2
Console.WriteLine(y); // 1

4-3. 型を明示して呼び出す方法

ジェネリックメソッドは、次のように型を明示して呼び出せます。

C#
Print<int>(100);
Print<string>("Hello");

型を明示すると、どの型として扱われるかがはっきりします。

特に、引数だけでは型推論できない場合は、型指定が必要です。

C#
public T CreateDefault<T>()
{
return default;
}
C#
int number = CreateDefault<int>();
string text = CreateDefault<string>();

4-4. 型推論で呼び出す方法

多くの場合、C#は引数から型を推論してくれます。

C#
public void Print<T>(T value)
{
Console.WriteLine(value);
}

このメソッドは、次のように型指定なしで呼び出せます。

C#
Print(100);
Print("Hello");
Print(true);

100ならTint"Hello"ならTstringtrueならTboolと判断されます。

初心者のうちは、型推論できる場面では省略し、型がわかりにくい場面では明示する、という考え方で問題ありません。

4-5. ジェネリックメソッドでよくあるエラー

ジェネリックメソッドでよくあるエラーの1つは、Tに存在するとは限らないメンバーを呼び出そうとすることです。

C#
public void PrintName<T>(T item)
{
Console.WriteLine(item.Name); // エラー
}

Tがどんな型になるかわからないため、Nameプロパティがあるとは限りません。そのため、コンパイルエラーになります。

このような場合は、型制約を使います。

C#
public interface IHasName
{
string Name { get; }
}
C#
public void PrintName<T>(T item) where T : IHasName
{
Console.WriteLine(item.Name);
}

where T : IHasNameと書くことで、Tは必ずIHasNameを実装している型だと保証できます。

5. 型制約whereの基本

5-1. 型制約とは何か

型制約とは、ジェネリクスで指定できる型に条件を付ける仕組みです。

通常、Tはどんな型にもなれます。

C#
public class Box<T>
{
}

しかし、場合によっては「参照型だけにしたい」「値型だけにしたい」「特定のインターフェイスを実装している型だけにしたい」といった条件を付けたいことがあります。

そのときに使うのがwhereです。

C#
public class Sample<T> where T : class
{
}

この例では、Tに指定できるのは参照型だけになります。

5-2. where T : classの使い方

where T : classは、Tを参照型に限定する制約です。

C#
public class ReferenceBox<T> where T : class
{
public T Value { get; set; }
}

このクラスには、stringや独自クラスなどの参照型を指定できます。

C#
var box = new ReferenceBox<string>();

一方、intなどの値型は指定できません。

C#
// var box = new ReferenceBox<int>(); // エラー

参照型に限定したい場合や、nullを扱う前提の設計にしたい場合に使います。

5-3. where T : structの使い方

where T : structは、Tを値型に限定する制約です。

C#
public class ValueBox<T> where T : struct
{
public T Value { get; set; }
}

intdoubleDateTimeなどの値型を指定できます。

C#
var intBox = new ValueBox<int>();
var dateBox = new ValueBox<DateTime>();

一方、stringや独自クラスなどの参照型は指定できません。

C#
// var stringBox = new ValueBox<string>(); // エラー

数値や日付など、値型だけを扱いたい場合に使います。

5-4. where T : new()の使い方

where T : new()は、Tに引数なしコンストラクターがあることを条件にする制約です。

C#
public class Factory<T> where T : new()
{
public T Create()
{
return new T();
}
}

通常、ジェネリクスではnew T()は使えません。なぜなら、Tに引数なしコンストラクターがあるかどうかわからないからです。

しかし、where T : new()を付けると、new T()を使えるようになります。

C#
public class User
{
public string Name { get; set; }
}
C#
var factory = new Factory<User>();
User user = factory.Create();

new()制約は、複数の制約と組み合わせる場合、基本的に最後に書きます。

C#
public class Service<T> where T : class, new()
{
}

5-5. where T : 基底クラスの使い方

特定の基底クラスを継承している型だけに限定することもできます。

C#
public class Animal
{
public string Name { get; set; }

public void Eat()
{
Console.WriteLine("食べます");
}
}
C#
public class Dog : Animal
{
}
C#
public class AnimalService<T> where T : Animal
{
public void ShowName(T animal)
{
Console.WriteLine(animal.Name);
animal.Eat();
}
}

where T : Animalと書くことで、TAnimalまたはAnimalを継承したクラスに限定されます。そのため、NameプロパティやEatメソッドを安全に使えます。

5-6. where T : インターフェイスの使い方

インターフェイスを型制約に使うこともよくあります。

C#
public interface IEntity
{
int Id { get; }
}
C#
public class User : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
C#
public class Repository<T> where T : IEntity
{
public void PrintId(T item)
{
Console.WriteLine(item.Id);
}
}

where T : IEntityにより、Tは必ずIdプロパティを持つことが保証されます。

実務では、インターフェイス制約は非常によく使われます。共通の振る舞いを持つ型だけを対象にしたい場合に便利です。

5-7. 複数の型制約を組み合わせる方法

型制約は複数組み合わせることができます。

C#
public class Service<T> where T : class, IEntity, new()
{
public T Create()
{
T item = new T();
Console.WriteLine(item.Id);
return item;
}
}

この例では、Tに対して次の条件を指定しています。

classにより参照型であること、IEntityを実装していること、new()により引数なしコンストラクターを持つことです。

複数の型パラメーターがある場合は、それぞれに制約を付けられます。

C#
public class PairService<TKey, TValue>
where TKey : notnull
where TValue : class
{
}

5-8. 型制約を使うべきタイミング

型制約は、Tに対して特定のメンバーを使いたいときに使います。

たとえば、TIdを使いたいなら、Idを持つインターフェイスで制約します。

C#
public void PrintId<T>(T item) where T : IEntity
{
Console.WriteLine(item.Id);
}

また、new T()を使いたいならnew()制約が必要です。

C#
public T Create<T>() where T : new()
{
return new T();
}

一方で、必要がないのに型制約を付けすぎると、使える型が制限されてしまいます。型制約は「その制約がないと実現できない処理があるか」を考えて付けるのがポイントです。

6. ジェネリクスとコレクションの関係

6-1. List<T>の基本

List<T>は、C#ジェネリクスを使った代表的なコレクションです。複数の値を順番に管理できます。

C#
List<string> names = new List<string>();

names.Add("田中");
names.Add("佐藤");
names.Add("鈴木");

List<string>は文字列だけを格納できます。

C#
foreach (string name in names)
{
Console.WriteLine(name);
}

List<int>なら整数のリストになります。

C#
List<int> numbers = new List<int> { 10, 20, 30 };

foreach (int number in numbers)
{
Console.WriteLine(number);
}

List<T>は、要素数が変わる一覧を扱うときによく使います。

6-2. Dictionary<TKey, TValue>の基本

Dictionary<TKey, TValue>は、キーと値の組み合わせを管理するコレクションです。

C#
Dictionary<string, int> scores = new Dictionary<string, int>();

scores["Alice"] = 90;
scores["Bob"] = 80;

この例では、キーがstring、値がintです。

C#
Console.WriteLine(scores["Alice"]);

キーを使って値をすばやく取り出せるため、IDとデータ、名前と点数、コードと名称などを管理するときに便利です。

C#
Dictionary<int, string> users = new Dictionary<int, string>();

users[1] = "山田";
users[2] = "佐藤";

6-3. Queue<T>・Stack<T>の基本

Queue<T>は、先に入れたものを先に取り出すコレクションです。順番待ちの処理に向いています。

C#
Queue<string> queue = new Queue<string>();

queue.Enqueue("A");
queue.Enqueue("B");
queue.Enqueue("C");

Console.WriteLine(queue.Dequeue()); // A

Stack<T>は、あとに入れたものを先に取り出すコレクションです。

C#
Stack<string> stack = new Stack<string>();

stack.Push("A");
stack.Push("B");
stack.Push("C");

Console.WriteLine(stack.Pop()); // C

Queue<T>は待ち行列、Stack<T>は履歴や戻る処理のような場面で使われます。

6-4. 配列とジェネリックコレクションの違い

配列は、要素数が固定です。

C#
int[] numbers = new int[3];

numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;

一方、List<T>は要素数をあとから増やせます。

C#
List<int> numbers = new List<int>();

numbers.Add(10);
numbers.Add(20);
numbers.Add(30);
numbers.Add(40);

配列はサイズが決まっているデータに向いています。List<T>などのジェネリックコレクションは、要素数が変わるデータに向いています。

初心者は、まずList<T>をよく使うと考えて問題ありません。

6-5. 初心者がまず覚えるべきコレクション

C#ジェネリクスを学ぶ初心者がまず覚えるべきコレクションは、List<T>Dictionary<TKey, TValue>です。

List<T>は、複数のデータを順番に扱いたいときに使います。

C#
List<string> fruits = new List<string>
{
"Apple",
"Banana",
"Orange"
};

Dictionary<TKey, TValue>は、キーを使って値を管理したいときに使います。

C#
Dictionary<string, string> capitals = new Dictionary<string, string>
{
{ "Japan", "Tokyo" },
{ "France", "Paris" }
};

この2つを理解すると、C#の多くのコードが読みやすくなります。

7. ジェネリックインターフェイスの使い方

7-1. ジェネリックインターフェイスとは

ジェネリックインターフェイスとは、型パラメーターを持つインターフェイスのことです。

C#
public interface IStorage<T>
{
void Save(T item);
T Get(int id);
}

このインターフェイスは、Tに指定された型のデータを保存したり取得したりする機能を表します。

C#
public class UserStorage : IStorage<User>
{
public void Save(User item)
{
Console.WriteLine(item.Name + "を保存しました");
}

public User Get(int id)
{
return new User { Name = "山田" };
}
}

IStorage<User>として実装すると、Saveの引数やGetの戻り値はUserになります。

7-2. IRepository<T>のような設計例

実務では、IRepository<T>のようなジェネリックインターフェイスがよく使われます。

C#
public interface IRepository<T>
{
void Add(T item);
T GetById(int id);
List<T> GetAll();
}

ユーザー用のリポジトリを作る場合は、次のように実装できます。

C#
public class UserRepository : IRepository<User>
{
private readonly List<User> _users = new List<User>();

public void Add(User item)
{
_users.Add(item);
}

public User GetById(int id)
{
return _users.FirstOrDefault(user => user.Id == id);
}

public List<User> GetAll()
{
return _users;
}
}

商品用にも同じ設計を使えます。

C#
public class ProductRepository : IRepository<Product>
{
private readonly List<Product> _products = new List<Product>();

public void Add(Product item)
{
_products.Add(item);
}

public Product GetById(int id)
{
return _products.FirstOrDefault(product => product.Id == id);
}

public List<Product> GetAll()
{
return _products;
}
}

このように、共通の操作を型ごとに実装したいときにジェネリックインターフェイスは便利です。

7-3. インターフェイスを型制約に使う方法

ジェネリックインターフェイスや通常のインターフェイスは、型制約として使えます。

C#
public interface IEntity
{
int Id { get; }
}
C#
public class Repository<T> where T : IEntity
{
private readonly List<T> _items = new List<T>();

public T FindById(int id)
{
return _items.FirstOrDefault(item => item.Id == id);
}
}

where T : IEntityにより、Tには必ずIdがあると保証されます。

そのため、item.Idのようなコードを安全に書けます。

7-4. 実務でよく使われる設計パターン

ジェネリクスは、実務で次のような設計によく使われます。

データアクセスを共通化するリポジトリパターン、処理結果を共通化するレスポンスクラス、バリデーション処理、ファクトリー処理、イベントやメッセージ処理などです。

たとえば、処理結果を表すクラスは次のように書けます。

C#
public class Result<T>
{
public bool IsSuccess { get; set; }
public string ErrorMessage { get; set; }
public T Value { get; set; }
}

ユーザー取得処理なら次のように使えます。

C#
Result<User> result = new Result<User>
{
IsSuccess = true,
Value = new User { Name = "山田" }
};

商品取得処理にも同じResult<T>を使えます。

C#
Result<Product> productResult = new Result<Product>
{
IsSuccess = true,
Value = new Product { Name = "ノートPC" }
};

7-5. 使いすぎると読みにくくなるケース

ジェネリクスは便利ですが、使いすぎるとコードが読みにくくなることがあります。

たとえば、型パラメーターが多すぎるクラスは、初心者だけでなく経験者にとっても理解しづらくなります。

C#
public class ComplexService<TUser, TOrder, TResult, TKey>
{
}

本当に複数の型を抽象化する必要があるなら問題ありませんが、単純な処理まで無理にジェネリックにする必要はありません。

ジェネリクスは「複数の型で同じ処理を使い回したい」ときに使うのが基本です。1つの型でしか使わない処理なら、通常のクラスやメソッドで十分なこともあります。

8. ジェネリクスでよくある疑問とつまずき

8-1. Tにはどんな名前を付ければよいか

型パラメーター名には、慣習的にTを使うことが多いです。

C#
public class Box<T>
{
}

ただし、意味がある場合は、より具体的な名前を使うと読みやすくなります。

C#
public class Dictionary<TKey, TValue>
{
}

キーを表すならTKey、値を表すならTValue、要素を表すならTItemのように命名すると、役割がわかりやすくなります。

型パラメーターが1つだけで意味が明確な場合はTで問題ありません。

8-2. Tとobjectはどちらを使うべきか

基本的には、型が決まるならTを使うべきです。

objectは何でも入れられますが、取り出すときにキャストが必要です。また、間違った型にキャストすると実行時エラーになります。

C#
object value = 100;
string text = (string)value; // 実行時エラー

ジェネリクスなら、コンパイル時に型をチェックできます。

C#
Box<int> box = new Box<int>();
box.Value = 100;

型安全性を保ちたい場合は、objectよりもジェネリクスを使う方が適しています。

ただし、本当にあらゆる型を受け取りたいだけで、型固有の処理をしない場合はobjectを使うこともあります。

8-3. Tで四則演算できない理由

初心者がつまずきやすい点として、Tに対してそのまま四則演算できないことがあります。

C#
public T Add<T>(T a, T b)
{
return a + b; // エラーになることが多い
}

なぜなら、Tintとは限らないからです。stringUserが指定される可能性もあります。

C#では、Tに「必ず+演算子が使える」と通常の型制約だけで簡単に伝えることはできません。

数値を汎用的に扱いたい場合は、C#のバージョンや.NETの機能に応じて、ジェネリック数値演算用のインターフェイスを使う方法もあります。ただし、初心者のうちは「Tには何の型が来るかわからないため、四則演算はそのまま書けない」と覚えておくとよいでしょう。

8-4. nullを扱うときの注意点

ジェネリクスでは、Tが参照型か値型かによってnullの扱いが変わります。

参照型ならnullを代入できます。

C#
string text = null;

しかし、通常の値型にはnullを代入できません。

C#
// int number = null; // エラー

ジェネリクスでdefaultを使うと、型によって結果が変わります。

C#
public T GetDefault<T>()
{
return default;
}

Tintなら0boolならfalse、参照型ならnullになります。

null許容参照型を有効にしているプロジェクトでは、T?where T : classwhere T : structなどの扱いにも注意が必要です。

8-5. new T()が使えない理由

次のようなコードは、そのままでは書けません。

C#
public T Create<T>()
{
return new T(); // エラー
}

理由は、Tに引数なしコンストラクターがあるとは限らないからです。

たとえば、次のようなクラスは引数なしでは作れません。

C#
public class User
{
public User(string name)
{
Name = name;
}

public string Name { get; }
}

そのため、new T()を使いたい場合はnew()制約を付けます。

C#
public T Create<T>() where T : new()
{
return new T();
}

これにより、Tは引数なしコンストラクターを持つ型に限定されます。

8-6. 型制約を書かないと使えないメンバーがある理由

型制約を書かない場合、Tはどんな型にもなれます。

そのため、特定のプロパティやメソッドを呼び出すことはできません。

C#
public void PrintId<T>(T item)
{
Console.WriteLine(item.Id); // エラー
}

TUserならIdがあるかもしれませんが、stringintにはIdがありません。

そこで、Idを持つインターフェイスを作り、型制約を付けます。

C#
public interface IEntity
{
int Id { get; }
}
C#
public void PrintId<T>(T item) where T : IEntity
{
Console.WriteLine(item.Id);
}

型制約を書くことで、コンパイラに「Tにはこのメンバーがある」と伝えられます。

9. C#ジェネリクスの実践サンプル

9-1. 汎用的なデータ保持クラスを作る

まずは、処理結果を保持する汎用的なクラスを作ってみます。

C#
public class Result<T>
{
public bool Success { get; set; }
public string Message { get; set; }
public T Data { get; set; }
}

ユーザー情報を返す場合は、次のように使えます。

C#
public class User
{
public string Name { get; set; }
}
C#
Result<User> result = new Result<User>
{
Success = true,
Message = "取得に成功しました",
Data = new User { Name = "山田" }
};

Console.WriteLine(result.Data.Name);

文字列の結果にも使えます。

C#
Result<string> textResult = new Result<string>
{
Success = true,
Message = "成功",
Data = "完了しました"
};

このように、Result<T>を作っておくと、さまざまな型の結果を同じ形で扱えます。

9-2. 汎用的な検索メソッドを作る

次に、リストの中から条件に合う要素を探すメソッドを作ります。

C#
public T FindFirst<T>(List<T> items, Func<T, bool> predicate)
{
foreach (T item in items)
{
if (predicate(item))
{
return item;
}
}

return default;
}

使い方は次のとおりです。

C#
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

int result = FindFirst(numbers, x => x > 3);

Console.WriteLine(result); // 4

文字列にも使えます。

C#
List<string> names = new List<string> { "Tanaka", "Sato", "Suzuki" };

string name = FindFirst(names, x => x.StartsWith("S"));

Console.WriteLine(name); // Sato

型に依存しない検索処理を、1つのメソッドで実現できます。

9-3. 型制約付きのFactoryメソッドを作る

new()制約を使って、汎用的なFactoryメソッドを作ります。

C#
public T CreateInstance<T>() where T : new()
{
return new T();
}

引数なしコンストラクターを持つクラスなら、このメソッドで生成できます。

C#
public class Product
{
public string Name { get; set; }
}
C#
Product product = CreateInstance<Product>();
product.Name = "ノートPC";

Console.WriteLine(product.Name);

where T : new()があるため、new T()を安全に使えます。

9-4. インターフェイス制約を使って共通処理を書く

インターフェイス制約を使うと、共通のプロパティやメソッドを使った処理を書けます。

C#
public interface IEntity
{
int Id { get; }
}
C#
public class User : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
C#
public class Product : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
}

IEntityを実装している型だけを対象にしたメソッドを作ります。

C#
public void PrintEntityId<T>(T entity) where T : IEntity
{
Console.WriteLine($"ID: {entity.Id}");
}

呼び出し例です。

C#
var user = new User { Id = 1, Name = "山田" };
var product = new Product { Id = 100, Name = "マウス" };

PrintEntityId(user);
PrintEntityId(product);

UserでもProductでも、Idを持つ型として共通処理できます。

9-5. 初心者向けミニ演習

最後に、C#ジェネリクスの理解を深めるためのミニ演習です。

次のような、2つの値を保持するPair<TFirst, TSecond>クラスを作ってみましょう。

C#
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }

public Pair(TFirst first, TSecond second)
{
First = first;
Second = second;
}

public void Print()
{
Console.WriteLine($"{First}: {Second}");
}
}

使い方です。

C#
var pair1 = new Pair<string, int>("年齢", 30);
pair1.Print();

var pair2 = new Pair<string, string>("名前", "山田");
pair2.Print();

この演習では、複数の型パラメーターを使う感覚をつかめます。

次に、Box<T>を自分で作り、intstring、独自クラスで使ってみると、C#ジェネリクスの基本がより理解しやすくなります。

10. ジェネリクスを使うときの注意点とベストプラクティス

10-1. 何でもジェネリックにしない

ジェネリクスは便利ですが、すべてをジェネリックにすればよいわけではありません。

たとえば、特定の型でしか使わない処理なら、通常のクラスやメソッドで十分です。

C#
public class UserService
{
public void Register(User user)
{
// ユーザー登録処理
}
}

このような処理を無理にService<T>のようにすると、かえって目的がわかりにくくなることがあります。

ジェネリクスは、複数の型で同じ構造や処理を使い回したいときに使いましょう。

10-2. 型パラメーター名はわかりやすくする

型パラメーターが1つだけならTで問題ありません。

C#
public class Box<T>
{
}

しかし、複数ある場合は、意味がわかる名前にした方が読みやすくなります。

C#
public class Pair<TFirst, TSecond>
{
}
C#
public class Repository<TEntity, TKey>
{
}

T1T2のような名前でも動作しますが、何を表しているのかわかりにくくなります。

読みやすい命名を意識しましょう。

10-3. 必要に応じて型制約を付ける

Tに対して特定のメンバーを使うなら、型制約を付けましょう。

C#
public interface IEntity
{
int Id { get; }
}
C#
public class Repository<T> where T : IEntity
{
public void PrintId(T item)
{
Console.WriteLine(item.Id);
}
}

型制約がないままitem.Idを使おうとすると、コンパイルエラーになります。

一方で、不要な型制約を付けすぎると、使える型が減ってしまいます。必要なときだけ付けるのが基本です。

10-4. 可読性と再利用性のバランスを取る

ジェネリクスを使うと再利用性は高まりますが、複雑にしすぎると可読性が下がります。

たとえば、型パラメーターが多く、型制約も多いクラスは理解に時間がかかります。

C#
public class ComplexService<TEntity, TKey, TResult>
where TEntity : class, IEntity, new()
where TKey : notnull
{
}

このような設計が必要な場合もありますが、初心者のうちはシンプルなジェネリクスから始めるのがおすすめです。

「本当に汎用化する必要があるか」「通常のクラスで書いた方がわかりやすくないか」を考えましょう。

10-5. 初心者が覚えるべき使いどころ

初心者がまず覚えるべきC#ジェネリクスの使いどころは、次の3つです。

1つ目は、List<T>Dictionary<TKey, TValue>などのコレクションです。これは日常的に使います。

2つ目は、Result<T>ApiResponse<T>のような汎用的なデータ保持クラスです。Web APIやアプリケーション開発でよく登場します。

3つ目は、Repository<T>IRepository<T>のような共通処理の設計です。実務的な設計を学ぶときに重要になります。

最初から難しい設計を目指す必要はありません。まずはList<int>List<string>Box<T>のような基本から慣れていきましょう。

まとめ

C#ジェネリクスとは、型をあとから指定できる仕組みです。クラスやメソッドを特定の型に固定せず、さまざまな型で再利用できるようにします。

List<T>Dictionary<TKey, TValue>は、C#ジェネリクスの代表的な使用例です。普段何気なく使っているコレクションも、ジェネリクスによって型安全で使いやすくなっています。

ジェネリッククラスでは、クラス全体で型パラメーターを使えます。ジェネリックメソッドでは、メソッド単位で型パラメーターを使えます。

また、whereを使った型制約を理解すると、Tに対して参照型だけ、値型だけ、特定の基底クラスだけ、特定のインターフェイスを実装した型だけ、といった条件を付けられます。

初心者のうちは、まず次のポイントを押さえましょう。

Tはあとから指定される型を表すこと、List<T>のようなコレクションでよく使われること、objectよりも型安全に扱えること、特定のメンバーを使いたい場合は型制約が必要なことです。

C#ジェネリクスを使いこなせるようになると、コードの重複を減らし、読みやすく、保守しやすいプログラムを書けるようになります。まずは小さなBox<T>Result<T>を作るところから練習してみましょう。