C#のジェネリックとは?初心者にもわかる使い方・メリット・制約をサンプルコードで解説

はじめに

C#でプログラミングを学んでいると、List<T>Dictionary<TKey, TValue> のように、<T> が付いた書き方をよく見かけます。これが ジェネリック です。

ジェネリックは、C#で安全かつ再利用しやすいコードを書くために欠かせない仕組みです。最初は記号が多くて難しく見えますが、考え方はとてもシンプルです。

この記事では、C#のジェネリックについて、初心者にもわかるように基本の使い方、メリット、制約、実践的なサンプルコードまで順番に解説します。

1. C#のジェネリックとは?まず初心者向けにわかりやすく解説

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

C#のジェネリックとは、クラスやメソッドを作る時点では具体的な型を決めず、使うときに型を指定できる仕組みです。

たとえば、整数を扱う箱、文字列を扱う箱、ユーザー情報を扱う箱をそれぞれ別々に作ると、似たようなコードが増えてしまいます。

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

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

このような場合、ジェネリックを使うと1つのクラスでさまざまな型に対応できます。

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<T>int にも string にも対応できます。

1-2. なぜC#でジェネリックが使われるのか

C#でジェネリックがよく使われる理由は、主に次の3つです。

1つ目は、同じ処理を複数の型で使い回せることです。int 用、string 用、User 用のように似たコードを何度も書く必要がありません。

2つ目は、型安全性を保てることです。コンパイル時に型の間違いを検出できるため、実行してからエラーに気づくリスクを減らせます。

3つ目は、キャストを減らせることです。object 型を使った実装では取り出すときにキャストが必要ですが、ジェネリックなら指定した型としてそのまま扱えます。

C#では List<T>Dictionary<TKey, TValue>Nullable<T>IEnumerable<T> など、多くの標準ライブラリでジェネリックが使われています。

1-3. <T>の意味と読み方

<T> は、型パラメーターを表します。

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

この場合の T は、あとから指定される型の仮の名前です。読み方は「ティー」で問題ありません。

たとえば、次のように使います。

C#
Box<int> numberBox = new Box<int>();
Box<string> textBox = new Box<string>();

Box<int> と書いた場合、Tint として扱われます。
Box<string> と書いた場合、Tstring として扱われます。

なお、型パラメーター名は必ず T である必要はありません。ただし、慣習として1つの型を表す場合は T、キーと値を表す場合は TKeyTValue のような名前がよく使われます。

1-4. ジェネリックを使わないコードとの違い

ジェネリックを使わない場合、object 型で汎用的な処理を書くこともできます。

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

このクラスはどんな値でも入れられます。

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

しかし、取り出すときにはキャストが必要です。

C#
string text = (string)box.Value;

もし中身が string ではなかった場合、実行時にエラーになる可能性があります。

C#
box.Value = 123;

// 実行時エラーになる可能性がある
string text = (string)box.Value;

一方、ジェネリックを使うと型が固定されるため、間違った型を入れようとした時点でコンパイルエラーになります。

C#
var box = new Box<string>();
box.Value = "Hello";

// intは入れられない
// box.Value = 123;

このように、ジェネリックは安全で読みやすいコードを書くために役立ちます。

2. C#ジェネリックの基本的な使い方

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

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

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

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

使用例は次のとおりです。

C#
var intHolder = new DataHolder<int>();
intHolder.Data = 10;
intHolder.Show();

var stringHolder = new DataHolder<string>();
stringHolder.Data = "C# Generic";
stringHolder.Show();

DataHolder<int> では Dataint 型になり、DataHolder<string> では Datastring 型になります。

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

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

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

呼び出し例です。

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

このメソッドは、受け取った値の型に関係なく表示できます。

また、多くの場合は型推論が働くため、次のように型指定を省略できます。

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

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

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

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 = "Age";
pair.Second = 30;

Console.WriteLine($"{pair.First}: {pair.Second}");

Dictionary<TKey, TValue> も、キーの型と値の型を別々に指定する代表的なジェネリック型です。

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

この場合、キーは string、値は int として扱われます。

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

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

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

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

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

コンパイラが 10 を見て Tint"Hello" を見て Tstring と判断してくれます。

ただし、型を判断できない場合は明示的な指定が必要です。

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

int number = CreateDefault<int>();
string text = CreateDefault<string>();

この例では引数がないため、コンパイラは T を推測できません。そのため <int><string> を指定する必要があります。

2-5. サンプルコードで動作を確認する

次のコードは、ジェネリッククラスとジェネリックメソッドを組み合わせた簡単な例です。

C#
using System;

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

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

public void Show()
{
Console.WriteLine($"Value: {Value}");
}
}

public class Program
{
public static void PrintType<T>(T value)
{
Console.WriteLine($"Value: {value}, Type: {typeof(T)}");
}

public static void Main()
{
var intBox = new Box<int>(123);
intBox.Show();

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

PrintType(3.14);
PrintType("C#");
}
}

実行結果の例です。

Value: 123
Value: Hello
Value: 3.14, Type: System.Double
Value: C#, Type: System.String

ジェネリックを使うことで、intstringdouble など異なる型を同じ仕組みで扱えることがわかります。

3. ジェネリックを使うメリット

3-1. 型安全性が高まり実行時エラーを防ぎやすい

ジェネリックの大きなメリットは、型安全性が高まることです。

たとえば、List<string> には string しか追加できません。

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

names.Add("Alice");
names.Add("Bob");

// intは追加できない
// names.Add(100);

間違った型を追加しようとすると、コンパイル時点でエラーになります。実行してから不具合に気づくのではなく、コードを書く段階で問題を発見しやすくなります。

3-2. キャストが不要になりコードが読みやすくなる

object 型を使うと、値を取り出すたびにキャストが必要になります。

C#
var list = new ArrayList();
list.Add("Hello");

string text = (string)list[0];

一方、List<string> を使えばキャストは不要です。

C#
var list = new List<string>();
list.Add("Hello");

string text = list[0];

コードが短くなり、型の意図も明確になります。

3-3. 同じ処理をさまざまな型で再利用できる

ジェネリックを使うと、同じロジックをさまざまな型で再利用できます。

C#
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}

この Swap メソッドは、int にも string にも使えます。

C#
int x = 1;
int y = 2;
Swap(ref x, ref y);

string first = "A";
string second = "B";
Swap(ref first, ref second);

型ごとに同じメソッドを何度も作る必要がありません。

3-4. ボックス化・アンボックス化を避けてパフォーマンスを改善できる

値型を object として扱うと、ボックス化とアンボックス化が発生することがあります。

C#
object value = 10;      // ボックス化
int number = (int)value; // アンボックス化

大量のデータを扱う場合、この処理がパフォーマンスに影響することがあります。

List<int> のようなジェネリックコレクションを使えば、intobject に変換せずに扱えます。

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

int number = numbers[0];

型安全性だけでなく、パフォーマンス面でもジェネリックは有利です。

3-5. 保守しやすいコードを書ける

ジェネリックを使うと、似たようなコードの重複を減らせます。

たとえば、int 用、string 用、User 用の処理を別々に書くより、Repository<T> のような共通クラスにまとめたほうが保守しやすくなります。

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

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

public List<T> GetAll()
{
return _items;
}
}

型ごとに同じ修正を繰り返す必要がなくなり、変更に強いコードになります。

4. よく使うC#のジェネリック型

4-1. List<T>の使い方

List<T> は、C#で最もよく使われるジェネリックコレクションの1つです。複数のデータを順番に管理できます。

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

names.Add("Alice");
names.Add("Bob");
names.Add("Charlie");

foreach (var name in names)
{
Console.WriteLine(name);
}

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

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

int first = numbers[0];
Console.WriteLine(first);

List<T>T には、intstring、自作クラスなどさまざまな型を指定できます。

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

var users = new List<User>
{
new User { Name = "Alice" },
new User { Name = "Bob" }
};

4-2. Dictionary<TKey, TValue>の使い方

Dictionary<TKey, TValue> は、キーと値の組み合わせを管理するジェネリック型です。

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

ages["Alice"] = 25;
ages["Bob"] = 30;

Console.WriteLine(ages["Alice"]);

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

存在しないキーを直接参照すると例外が発生するため、TryGetValue を使うと安全です。

C#
if (ages.TryGetValue("Charlie", out int age))
{
Console.WriteLine(age);
}
else
{
Console.WriteLine("見つかりませんでした");
}

4-3. Queue<T>Stack<T>の使い方

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

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

queue.Enqueue("Task1");
queue.Enqueue("Task2");
queue.Enqueue("Task3");

Console.WriteLine(queue.Dequeue()); // Task1
Console.WriteLine(queue.Dequeue()); // Task2

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

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

stack.Push("Page1");
stack.Push("Page2");
stack.Push("Page3");

Console.WriteLine(stack.Pop()); // Page3
Console.WriteLine(stack.Pop()); // Page2

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

4-4. Nullable<T>の使い方

Nullable<T> は、通常は null を持てない値型に null を許可するためのジェネリック型です。

C#
Nullable<int> age = null;

C#では、より簡単に次のように書けます。

C#
int? age = null;

値があるかどうかは HasValue で確認できます。

C#
int? score = 80;

if (score.HasValue)
{
Console.WriteLine(score.Value);
}

int?DateTime?bool? などは、データベースのNULL値や未入力の値を扱うときによく使われます。

4-5. LINQとジェネリックの関係

LINQでよく使う IEnumerable<T> もジェネリックです。

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

var evenNumbers = numbers.Where(n => n % 2 == 0);

foreach (var number in evenNumbers)
{
Console.WriteLine(number);
}

WhereSelectOrderBy などのLINQメソッドは、ジェネリックによってさまざまな型に対応しています。

C#
var names = new List<string> { "Alice", "Bob", "Charlie" };

var shortNames = names.Where(name => name.Length <= 3);

List<int> でも List<string> でも同じようにLINQを使えるのは、ジェネリックの仕組みがあるからです。

5. ジェネリック制約とは?where句の使い方

5-1. ジェネリック制約が必要になる理由

ジェネリックの T は、どんな型にもなれる可能性があります。そのため、制約を付けない状態では、C#コンパイラは T にどのメンバーが存在するかわかりません。

たとえば、次のコードはエラーになります。

C#
public void ShowName<T>(T item)
{
// TにNameプロパティがあるとは限らない
// Console.WriteLine(item.Name);
}

TUser なら Name があるかもしれませんが、intDateTime には Name プロパティがありません。

そこで使うのが ジェネリック制約です。where 句を使うことで、T に指定できる型を制限できます。

5-2. where T : classで参照型に限定する

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

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

public bool IsNull()
{
return Value == null;
}
}

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

C#
var holder = new ReferenceHolder<string>();
holder.Value = "Hello";

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

C#
// エラー
// var holder = new ReferenceHolder<int>();

5-3. where T : structで値型に限定する

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

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

使用例です。

C#
var intHolder = new ValueHolder<int>();
var dateHolder = new ValueHolder<DateTime>();

string や自作クラスのような参照型は指定できません。

C#
// エラー
// var stringHolder = new ValueHolder<string>();

値型だけを扱いたい場合に便利です。

5-4. where T : new()でインスタンス生成を許可する

ジェネリック型 T のインスタンスを new T() で作りたい場合は、new() 制約が必要です。

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

使用例です。

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

var factory = new Factory<User>();
User user = factory.Create();

new() 制約を付けることで、引数なしコンストラクターを持つ型だけを指定できます。

5-5. where T : 基底クラスで継承関係を指定する

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

C#
public class Entity
{
public int Id { get; set; }
}

public class User : Entity
{
public string Name { get; set; }
}

TEntity の派生クラスに限定します。

C#
public class EntityRepository<T> where T : Entity
{
private readonly List<T> _items = new();

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

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

where T : Entity と書くことで、T は必ず Id プロパティを持つと判断できます。

C#
var repository = new EntityRepository<User>();
repository.Add(new User { Id = 1, Name = "Alice" });

var user = repository.FindById(1);

5-6. where T : インターフェイスで利用できるメンバーを制限する

インターフェイスを使った制約もよく使われます。

C#
public interface IHasName
{
string Name { get; }
}

このインターフェイスを実装した型だけを受け取るようにします。

C#
public class NamePrinter<T> where T : IHasName
{
public void Print(T item)
{
Console.WriteLine(item.Name);
}
}

使用例です。

C#
public class Product : IHasName
{
public string Name { get; set; }
}

var printer = new NamePrinter<Product>();
printer.Print(new Product { Name = "Laptop" });

where T : IHasName によって、T には Name プロパティがあると保証されます。

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

複数の制約を組み合わせることもできます。

C#
public interface IEntity
{
int Id { get; set; }
}

public class Repository<T> where T : class, IEntity, new()
{
private readonly List<T> _items = new();

public T Create()
{
return new T();
}

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

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

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

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

C#
public class Customer : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
}

var repository = new Repository<Customer>();
var customer = repository.Create();
customer.Id = 1;
customer.Name = "Alice";

repository.Add(customer);

複数の制約を使うと、ジェネリックでありながら利用できるメンバーを明確にできます。

6. ジェネリックの実践サンプルコード

6-1. 任意の型を扱える汎用的なリポジトリクラス

リポジトリクラスは、データの追加、取得、削除などをまとめるクラスです。ジェネリックを使うと、さまざまなエンティティに対応できます。

C#
public interface IEntity
{
int Id { get; set; }
}

public class Repository<T> where T : IEntity
{
private readonly List<T> _items = new();

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

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

public List<T> GetAll()
{
return _items;
}

public void Remove(int id)
{
var item = FindById(id);
if (item != null)
{
_items.Remove(item);
}
}
}

使用例です。

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

var userRepository = new Repository<User>();

userRepository.Add(new User { Id = 1, Name = "Alice" });
userRepository.Add(new User { Id = 2, Name = "Bob" });

var user = userRepository.FindById(1);
Console.WriteLine(user.Name);

User 以外にも、ProductOrder など IEntity を実装した型なら同じリポジトリを再利用できます。

6-2. 共通の戻り値を扱うResult<T>クラス

アプリケーション開発では、処理の成功・失敗と結果データをまとめて返したい場面があります。そこで便利なのが Result<T> のようなジェネリッククラスです。

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

private Result(bool isSuccess, T value, string errorMessage)
{
IsSuccess = isSuccess;
Value = value;
ErrorMessage = errorMessage;
}

public static Result<T> Success(T value)
{
return new Result<T>(true, value, null);
}

public static Result<T> Failure(string errorMessage)
{
return new Result<T>(false, default, errorMessage);
}
}

使用例です。

C#
public Result<User> FindUser(int id)
{
if (id == 1)
{
return Result<User>.Success(new User { Id = 1, Name = "Alice" });
}

return Result<User>.Failure("ユーザーが見つかりません");
}

呼び出し側では、成功したかどうかを確認してから値を使えます。

C#
var result = FindUser(1);

if (result.IsSuccess)
{
Console.WriteLine(result.Value.Name);
}
else
{
Console.WriteLine(result.ErrorMessage);
}

Result<T> を使うことで、UserProductintstring など、さまざまな戻り値に対応できます。

6-3. 最大値・最小値を返すジェネリックメソッド

大小比較を行うジェネリックメソッドでは、IComparable<T> 制約を使います。

C#
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}

public static T Min<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) <= 0 ? a : b;
}

使用例です。

C#
Console.WriteLine(Max(10, 20));          // 20
Console.WriteLine(Min(10, 20)); // 10
Console.WriteLine(Max("apple", "banana"));

where T : IComparable<T> を付けることで、CompareTo メソッドを安全に呼び出せます。

6-4. インターフェイス制約を使った実用例

次は、価格を持つ商品だけを対象に合計金額を計算する例です。

C#
public interface IPrice
{
decimal Price { get; }
}

public class Product : IPrice
{
public string Name { get; set; }
public decimal Price { get; set; }
}

public static class PriceCalculator
{
public static decimal Sum<T>(IEnumerable<T> items) where T : IPrice
{
decimal total = 0;

foreach (var item in items)
{
total += item.Price;
}

return total;
}
}

使用例です。

C#
var products = new List<Product>
{
new Product { Name = "Book", Price = 1200 },
new Product { Name = "Pen", Price = 150 }
};

decimal total = PriceCalculator.Sum(products);
Console.WriteLine(total);

IPrice を実装した型だけを受け取るため、Price プロパティを安全に使用できます。

6-5. よくあるエラーと修正例

ジェネリックでよくあるエラーの1つは、制約がない型で特定のメンバーを使おうとすることです。

C#
public void PrintName<T>(T item)
{
// エラー: TにNameがあるとは限らない
// Console.WriteLine(item.Name);
}

修正するには、インターフェイス制約を使います。

C#
public interface IHasName
{
string Name { get; }
}

public void PrintName<T>(T item) where T : IHasName
{
Console.WriteLine(item.Name);
}

もう1つの例は、new T() を使おうとしてエラーになるケースです。

C#
public T Create<T>()
{
// エラー: Tに引数なしコンストラクターがあるとは限らない
// return new T();
}

修正例です。

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

ジェネリックでは、コンパイラに対して「この型には何ができるのか」を制約で伝えることが重要です。

7. ジェネリックを使うときの注意点

7-1. 何でもジェネリック化すればよいわけではない

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

たとえば、特定の型だけを扱う処理なら、無理にジェネリック化する必要はありません。

C#
public void SendEmail(User user)
{
// User専用の処理
}

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

ジェネリックは、複数の型に対して同じ処理を行いたい場合に使うのが基本です。

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

型パラメーター名は T だけでなく、意味のある名前にできます。

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

1つだけなら T で十分なことが多いですが、複数ある場合は TKeyTValueTItemTResult など、役割がわかる名前にすると読みやすくなります。

C#
public class Converter<TSource, TResult>
{
public TResult Convert(TSource source)
{
// 変換処理
return default;
}
}

型パラメーター名がわかりやすいと、コードを読む人が意図を理解しやすくなります。

7-3. 制約がない型では使えるメンバーが限られる

制約がない T では、基本的に object 型が持つメンバーしか安全に使えません。

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

ToString は使えますが、NameId のような独自プロパティは使えません。

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

特定のメンバーを使いたい場合は、基底クラスやインターフェイスで制約を付けましょう。

7-4. 可読性を下げる複雑なジェネリックは避ける

ジェネリックを多用しすぎると、コードが読みにくくなる場合があります。

C#
public class Handler<TRequest, TResponse, TContext, TOptions>
{
}

このように型パラメーターが多すぎると、初心者だけでなく経験者にとっても理解しづらくなります。

型パラメーターが増えすぎる場合は、設計を見直すサインかもしれません。クラスを分割する、インターフェイスを整理する、責務を減らすなどの方法を検討しましょう。

7-5. 初心者がつまずきやすいポイント

初心者がつまずきやすいポイントは、T を特別な型だと思ってしまうことです。

T はあくまで型の仮の名前です。intstringUser など、実際に使うときに指定された型に置き換わると考えると理解しやすくなります。

また、List<T>T に何を入れればよいかわからない場合は、「このリストに何を入れたいのか」を考えましょう。

C#
List<string> names;  // 名前を入れる
List<int> scores; // 点数を入れる
List<User> users; // ユーザーを入れる

ジェネリックは、型を難しくする仕組みではなく、型を安全に扱うための仕組みです。

8. ジェネリックと似た仕組みとの違い

8-1. object型との違い

object 型は、C#のすべての型の基底型です。そのため、どんな値でも代入できます。

C#
object value = "Hello";
value = 123;

しかし、取り出して使うときにはキャストが必要になることがあります。

C#
object value = "Hello";
string text = (string)value;

ジェネリックでは、使う時点で型を指定するため、キャストを減らせます。

C#
var list = new List<string>();
list.Add("Hello");

string text = list[0];

object は柔軟ですが、型安全性が低くなりがちです。ジェネリックは柔軟性と型安全性を両立しやすい仕組みです。

8-2. 継承・ポリモーフィズムとの違い

継承やポリモーフィズムは、共通の基底クラスやインターフェイスを通じて複数の型を扱う仕組みです。

C#
public abstract class Animal
{
public abstract void Speak();
}

public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Woof");
}
}

public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Meow");
}
}

この場合、Animal 型として DogCat を扱えます。

C#
Animal animal = new Dog();
animal.Speak();

一方、ジェネリックは「型をパラメーターとして受け取る」仕組みです。

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

継承は「共通の親を持つ型をまとめる」考え方で、ジェネリックは「型をあとから指定して再利用する」考え方です。目的が異なるため、実際の開発では組み合わせて使うことも多いです。

8-3. インターフェイスとの違い

インターフェイスは、クラスが持つべきメンバーの契約を定義します。

C#
public interface ILogger
{
void Log(string message);
}

ジェネリックは、型をパラメーター化する仕組みです。

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

両者は別の概念ですが、組み合わせると強力です。

C#
public class LoggerService<TLogger> where TLogger : ILogger
{
private readonly TLogger _logger;

public LoggerService(TLogger logger)
{
_logger = logger;
}

public void Execute()
{
_logger.Log("処理を実行しました");
}
}

インターフェイス制約を使うことで、ジェネリック型に対して利用できるメンバーを保証できます。

8-4. 配列やコレクションとの関係

配列は、同じ型の値をまとめて扱う仕組みです。

C#
int[] numbers = { 1, 2, 3 };

一方、List<T> はジェネリックコレクションです。

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

配列はサイズが固定ですが、List<T> は要素を追加・削除しやすいという特徴があります。

C#
numbers.Add(4);
numbers.Remove(2);

C#では、配列よりも List<T>Dictionary<TKey, TValue> などのジェネリックコレクションを使う場面が多くあります。

8-5. ジェネリックを使うべき場面・使わなくてよい場面

ジェネリックを使うべき場面は、同じ処理を複数の型で使い回したい場合です。

C#
public class Repository<T>
{
}

値の型に関係なく共通処理を行いたい場合にも向いています。

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

一方、特定の型に強く依存する処理では、ジェネリックを使わないほうがわかりやすい場合があります。

C#
public void RegisterUser(User user)
{
// User専用の登録処理
}

「型が変わっても同じ処理ができるか」を基準に考えると、ジェネリックを使うべきか判断しやすくなります。

9. C#ジェネリックに関するよくある質問

9-1. T以外の名前は使える?

使えます。T は慣習的によく使われる名前ですが、必須ではありません。

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

複数の型パラメーターを使う場合は、役割がわかる名前にすると読みやすくなります。

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

ただし、あまり独自すぎる名前にすると読みにくくなるため、一般的な命名を使うのがおすすめです。

9-2. ジェネリックは初心者でも覚えるべき?

C#を学ぶなら、ジェネリックは早めに覚えるべき重要な機能です。

理由は、C#の標準ライブラリで頻繁に使われているからです。特に List<T>Dictionary<TKey, TValue>IEnumerable<T> は日常的に登場します。

最初から制約や高度な設計まで完璧に理解する必要はありません。まずは次のような基本を押さえれば十分です。

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

T には具体的な型が入る」と理解できれば、ジェネリックの第一歩はクリアです。

9-3. ジェネリックメソッドとジェネリッククラスはどう使い分ける?

クラス全体で同じ型を扱いたい場合は、ジェネリッククラスを使います。

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

一方、特定のメソッドだけで型を汎用化したい場合は、ジェネリックメソッドを使います。

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

たとえば、データを保持するクラスならジェネリッククラス、値を表示するだけの処理ならジェネリックメソッドが向いています。

9-4. List<T>Tには何を指定できる?

List<T>T には、ほとんどの型を指定できます。

C#
List<int> numbers = new List<int>();
List<string> names = new List<string>();
List<DateTime> dates = new List<DateTime>();
List<User> users = new List<User>();

値型、参照型、自作クラス、構造体などを指定できます。

ただし、List<string>int を追加することはできません。

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

names.Add("Alice");
// names.Add(100); // エラー

この制限があることで、型安全なコードを書けます。

9-5. ジェネリック制約を書かないとどうなる?

ジェネリック制約を書かない場合、T にはどんな型でも指定できる可能性があります。

そのため、T に対して使えるメンバーは限られます。

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

このような処理なら制約は不要です。

しかし、TIdName を使いたい場合は、そのメンバーが存在することを制約で保証する必要があります。

C#
public interface IEntity
{
int Id { get; set; }
}

public void ShowId<T>(T value) where T : IEntity
{
Console.WriteLine(value.Id);
}

制約を書かないと自由度は高くなりますが、使える機能は少なくなります。必要に応じて where 句を使いましょう。

まとめ

C#のジェネリックは、型をあとから指定できる便利な仕組みです。<T> を使うことで、1つのクラスやメソッドをさまざまな型に対応させることができます。

ジェネリックを使う主なメリットは、型安全性が高まること、キャストが不要になること、コードを再利用しやすくなること、保守性が向上することです。

特に List<T>Dictionary<TKey, TValue>Queue<T>Stack<T>Nullable<T>IEnumerable<T> などは、C#開発で頻繁に使います。

また、where 句によるジェネリック制約を使うと、指定できる型を制限しながら、安全にメンバーを利用できます。

最初は Twhere の書き方が難しく感じるかもしれません。しかし、基本は「型をあとから指定して、同じ処理を安全に再利用する仕組み」です。

まずは List<T> や簡単なジェネリックメソッドから使い始めると、C# generic の考え方を自然に理解できるようになります。