C#ジェネリクスとは?型安全に再利用できる書き方を初心者向けにわかりやすく解説

はじめに

C#ジェネリクスは、C#で再利用しやすく型安全なコードを書くために欠かせない仕組みです。英語では「C# generics」と呼ばれ、List<T>Dictionary<TKey, TValue>のような形で日常的によく使われます。

初心者のうちは、Twhereといった見慣れない記法が出てくるため難しく感じるかもしれません。しかし、考え方はシンプルです。ジェネリクスとは「扱う型を後から指定できるようにする仕組み」です。

たとえば、int用、string用、DateTime用に似たようなクラスやメソッドを何度も作るのではなく、型をパラメーターとして受け取れるようにすることで、1つのコードをさまざまな型で使い回せます。

この記事では、C#ジェネリクスの基本から、ジェネリッククラス、ジェネリックメソッド、型制約、実務でよく使うサンプルまで、初心者向けにわかりやすく解説します。

1. C#ジェネリクスとは?型をパラメーター化して使う仕組み

1-1. ジェネリクスの基本的な意味

C#ジェネリクスとは、クラスやメソッドを定義するときに、具体的な型を固定せず、後から型を指定できるようにする機能です。

たとえば、次のような書き方を見たことがあるかもしれません。

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

ここで使われているList<T>Tが、型パラメーターです。

List<string>ではTstringとして扱われ、List<int>ではTintとして扱われます。つまり、List<T>という1つの仕組みを、さまざまな型に対応させて使えるのです。

1-2. 「型安全に再利用できる」とはどういうことか

「型安全」とは、間違った型の値を扱うミスをコンパイル時に発見しやすいという意味です。

たとえば、List<string>には文字列だけを入れられます。

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

names.Add("Tanaka");
names.Add("Suzuki");
// names.Add(100); // コンパイルエラー

namesstring用のリストなので、int100を追加しようとするとコンパイルエラーになります。

このように、実行する前に型の間違いを見つけられるため、プログラムの安全性が高まります。

1-3. C#でジェネリクスが使われる代表的な場面

C#では、次のような場面でジェネリクスがよく使われます。

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

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

Task<string> task = GetNameAsync();

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

代表的なジェネリック型には、List<T>Dictionary<TKey, TValue>Nullable<T>Task<T>IEnumerable<T>などがあります。

特にコレクション、非同期処理、共通レスポンス、リポジトリ設計などでは、ジェネリクスが頻繁に使われます。

1-4. 初心者がまず押さえるべきポイント

初心者がまず押さえるべきポイントは、次の3つです。

1つ目は、Tは「後から決まる型」を表す名前だということです。

2つ目は、List<string>List<int>のように、実際に使うときは具体的な型を指定するということです。

3つ目は、ジェネリクスを使うと、同じ処理を複数の型で安全に使い回せるということです。

最初から難しい設計を覚える必要はありません。まずは「型を引数のように渡せる仕組み」と理解すると、ジェネリクスの全体像がつかみやすくなります。

2. C#でジェネリクスを使うメリット

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

ジェネリクスを使う最大のメリットは、型安全性が高まることです。

たとえば、List<int>にはint型の値だけを追加できます。

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

scores.Add(90);
scores.Add(75);
// scores.Add("high"); // コンパイルエラー

間違った型の値を入れようとすると、プログラムを実行する前にコンパイラーがエラーを出してくれます。

これにより、実行時に「想定していない型だった」というエラーを防ぎやすくなります。

2-2. 同じ処理を複数の型で再利用できる

ジェネリクスを使うと、同じ処理を複数の型で使い回せます。

たとえば、値をそのまま返すメソッドを型ごとに作ると、次のようになります。

C#
int EchoInt(int value)
{
return value;
}

string EchoString(string value)
{
return value;
}

ジェネリックメソッドを使えば、1つのメソッドで表現できます。

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

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

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

同じようなコードを何度も書かなくてよいため、コードの重複を減らせます。

2-3. object型やキャストを使う書き方との違い

ジェネリクスを使わずに、どんな型でも受け取れるようにしたい場合、object型を使う方法があります。

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

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

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

C#
object value = "Hello";

// 実行時エラー
int number = (int)value;

一方、ジェネリクスを使うと、最初から型が決まっているため、不要なキャストを減らせます。

C#
string value = Echo<string>("Hello");

型が明確なので、コードも読みやすくなります。

2-4. コードの可読性と保守性が上がる

ジェネリクスを使うと、コードを読む人が「このクラスやメソッドは何の型を扱うのか」を理解しやすくなります。

C#
List<Customer> customers = new List<Customer>();

このコードを見るだけで、customersにはCustomer型のデータが入ることが分かります。

object型を使った場合は、中に何が入っているか分かりにくくなりがちです。

C#
List<object> items = new List<object>();

itemsには文字列も数値も独自クラスも入れられるため、扱いが複雑になります。

型が明確なコードは、修正や機能追加もしやすくなります。

2-5. ボックス化・アンボックス化を避けてパフォーマンス面でも有利になる

intboolなどの値型をobjectとして扱うと、ボックス化が発生します。

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

ボックス化とは、値型を参照型のように扱うために包み込む処理です。アンボックス化とは、包まれた値を元の値型として取り出す処理です。

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

ジェネリクスを使えば、値型をそのまま扱えるため、不要なボックス化・アンボックス化を避けやすくなります。

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

numbers.Add(10);
int number = numbers[0];

3. ジェネリクスを使わない場合の問題点

3-1. object型で値を扱うコード例

ジェネリクスを使わず、どんな値でも保持できるクラスを作ると、次のようになります。

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

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

このクラスは、stringintも受け取れます。

C#
Box box1 = new Box("Hello");
Box box2 = new Box(100);

一見便利に見えますが、値を取り出すときに問題が出ます。

3-2. キャストが必要になりミスが起きやすい理由

object型で保持した値は、取り出すときに元の型へキャストする必要があります。

C#
Box box = new Box("Hello");

string text = (string)box.Value;

正しい型にキャストすれば問題ありません。しかし、間違った型にキャストすると実行時エラーになります。

C#
Box box = new Box("Hello");

int number = (int)box.Value; // 実行時エラー

このようなミスは、コードを書いている時点では気づきにくい場合があります。

3-3. 型の不一致が実行時まで分かりにくい問題

object型を使うと、コンパイラーは中に入っている実際の型まで厳密には判断できません。

そのため、型の不一致がプログラムを実行するまで分からないことがあります。

C#
Box box = new Box("123");

int number = (int)box.Value; // 実行して初めてエラーになる

小さなプログラムでは気づきやすいかもしれませんが、実務のようにコード量が増えると、こうしたミスはバグの原因になります。

3-4. ジェネリクスを使った改善例

同じBoxクラスをジェネリクスで書くと、次のようになります。

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

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

使うときに型を指定します。

C#
Box<string> stringBox = new Box<string>("Hello");
string text = stringBox.Value;

Box<int> intBox = new Box<int>(100);
int number = intBox.Value;

Box<string>Valuestring型として扱われ、Box<int>Valueint型として扱われます。

キャストが不要になり、型の間違いもコンパイル時に見つけやすくなります。

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

4-1. 型パラメーターTの意味

ジェネリクスでは、Tのような型パラメーターを使います。

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

このTは、実際の型そのものではなく、「後から指定される型」を表します。

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

この場合、Tstringとして扱われます。

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

この場合、Tintとして扱われます。

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

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

C#
public class Container<T>
{
public T Item { get; set; }

public void SetItem(T item)
{
Item = item;
}

public T GetItem()
{
return Item;
}
}

このクラスは、任意の型の値を保持できます。

C#
Container<string> textContainer = new Container<string>();
textContainer.SetItem("Hello");

Container<int> numberContainer = new Container<int>();
numberContainer.SetItem(123);

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

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

C#
public T GetFirst<T>(T first, T second)
{
return first;
}

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

C#
int number = GetFirst<int>(10, 20);
string text = GetFirst<string>("A", "B");

型推論が働く場合は、型指定を省略できます。

C#
int number = GetFirst(10, 20);
string text = GetFirst("A", "B");

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

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

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;
}
}

使うときは、複数の型を指定します。

C#
Pair<int, string> pair = new Pair<int, string>(1, "One");

int id = pair.First;
string name = pair.Second;

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

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

4-5. 型パラメーター名の一般的な命名ルール

型パラメーターには、慣習的な命名ルールがあります。

最もよく使われるのはTです。

C#
public class Box<T>
{
}

複数の型を扱う場合は、意味が分かる名前を使うことが多いです。

C#
public class Repository<TEntity>
{
}

public class Pair<TFirst, TSecond>
{
}

public class Dictionary<TKey, TValue>
{
}

TKeyはキーの型、TValueは値の型、TEntityはエンティティの型を表します。

必ずTでなければならないわけではありませんが、読みやすい名前を付けることが大切です。

5. ジェネリッククラスの使い方を初心者向けに解説

5-1. 値を保持するシンプルなジェネリッククラスの例

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

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

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

このHolder<T>は、Tの部分に指定された型の値を保持します。

5-2. int・stringなど異なる型で使い回す方法

Holder<T>は、さまざまな型で使えます。

C#
Holder<int> numberHolder = new Holder<int>(100);
int number = numberHolder.Value;

Holder<string> textHolder = new Holder<string>("Hello");
string text = textHolder.Value;

Holder<int>ではValueint型になり、Holder<string>ではValuestring型になります。

同じHolder<T>というクラス定義を使いながら、異なる型に対応できるのがジェネリッククラスの便利な点です。

5-3. コンストラクターとプロパティで型Tを使う方法

型パラメーターTは、プロパティ、フィールド、コンストラクター、メソッドの引数や戻り値に使えます。

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

public ItemStore(T item)
{
_item = item;
}

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

public T GetItem()
{
return _item;
}
}

このように、クラス内で通常の型と同じようにTを扱えます。

C#
ItemStore<string> store = new ItemStore<string>("Apple");

string item = store.GetItem();

5-4. ジェネリッククラスを使うときの注意点

ジェネリッククラスでは、Tがどの型になるかは使う側が決めます。

そのため、型制約がない状態では、すべての型に共通する操作しか安全に使えません。

たとえば、次のようなコードは基本的には書けません。

C#
public class Printer<T>
{
public void PrintName(T item)
{
// item.Name は使えない
// TにNameプロパティがあるとは限らないため
}
}

Tstringかもしれませんし、intかもしれません。Nameプロパティを持っているとは限らないため、コンパイラーは許可しません。

特定のメンバーを使いたい場合は、型制約を使う必要があります。

6. ジェネリックメソッドの使い方を初心者向けに解説

6-1. 引数と戻り値に型Tを使う方法

ジェネリックメソッドでは、引数や戻り値に型パラメーターを使えます。

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

このメソッドは、受け取った値をそのまま返します。

C#
int number = Identity<int>(10);
string text = Identity<string>("Hello");

Tintの場合はintを受け取り、intを返します。Tstringの場合はstringを受け取り、stringを返します。

6-2. 型推論により型指定を省略できるケース

C#では、メソッドの引数から型を推論できる場合、型指定を省略できます。

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

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

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

10を渡しているため、コンパイラーはTintだと判断します。

"Hello"を渡しているため、コンパイラーはTstringだと判断します。

6-3. 型を明示して呼び出すケース

型推論がうまく働かない場合や、意図を明確にしたい場合は、型を明示して呼び出します。

C#
string value = Echo<string>("Hello");

また、引数だけでは型を判断できないメソッドでは、型指定が必要になることがあります。

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

このメソッドは引数がないため、呼び出し時に型を明示します。

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

6-4. ジェネリックメソッドが便利な実践例

ジェネリックメソッドは、配列やリストの先頭要素を取得するような共通処理に便利です。

C#
public T GetFirst<T>(List<T> items)
{
if (items == null || items.Count == 0)
{
return default(T);
}

return items[0];
}

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

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

List<string> names = new List<string> { "Tanaka", "Suzuki" };
string firstName = GetFirst(names);

intのリストにもstringのリストにも同じメソッドを使えます。

7. C#でよく使うジェネリック型の代表例

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

List<T>は、同じ型の要素を複数保持するためのコレクションです。

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

names.Add("Tanaka");
names.Add("Suzuki");
names.Add("Sato");

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

List<string>にはstring型の値を入れます。

int型のリストを作る場合は、次のように書きます。

C#
List<int> scores = new List<int> { 80, 90, 75 };

List<T>は、C#ジェネリクスの中でも特によく使われる型です。

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

Dictionary<TKey, TValue>は、キーと値のペアを管理するコレクションです。

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

users.Add(1, "Tanaka");
users.Add(2, "Suzuki");

string userName = users[1];

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

文字列のキーを使いたい場合は、次のように書けます。

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

scores.Add("Tanaka", 90);
scores.Add("Suzuki", 85);

TKeyはキーの型、TValueは値の型を表します。

7-3. Nullable<T>の使い方

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

C#
Nullable<int> age = null;

C#では、短縮形としてint?と書くこともできます。

C#
int? age = null;

if (age.HasValue)
{
Console.WriteLine(age.Value);
}
else
{
Console.WriteLine("年齢は未設定です");
}

intDateTimeのような値型で、「値がない状態」を表したいときに使います。

7-4. Task<T>の使い方

Task<T>は、非同期処理の結果を表すジェネリック型です。

C#
public async Task<string> GetMessageAsync()
{
await Task.Delay(1000);
return "Hello";
}

このメソッドは、非同期処理の結果としてstringを返します。

呼び出す側では、awaitを使って結果を受け取ります。

C#
string message = await GetMessageAsync();

Task<int>なら非同期処理の結果がintTask<Customer>なら結果がCustomer型という意味になります。

7-5. IEnumerable<T>の使い方

IEnumerable<T>は、順番に取り出せるデータの集まりを表すインターフェイスです。

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

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

List<T>や配列など、多くのコレクションはIEnumerable<T>として扱えます。

メソッドの戻り値として使うこともよくあります。

C#
public IEnumerable<int> GetNumbers()
{
return new List<int> { 1, 2, 3 };
}

コレクションの具体的な種類に依存しすぎず、柔軟にデータを扱えるのが特徴です。

8. ジェネリック型制約とは?where句の基本

8-1. 型制約を使う理由

ジェネリック型制約とは、型パラメーターに指定できる型を制限する仕組みです。

型制約がない場合、Tにはどんな型でも指定できるため、特定のメンバーを安全に呼び出せません。

たとえば、Nameプロパティを使いたい場合、Tが必ずNameを持っている保証が必要です。

そのようなときにwhere句を使います。

C#
public class Repository<T> where T : IEntity
{
}

この例では、Tに指定できるのはIEntityを実装した型だけです。

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

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

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

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

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

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

C#
// ReferenceBox<int> box = new ReferenceBox<int>(); // コンパイルエラー

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

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

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

intdoubleDateTimeなどを指定できます。

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

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

C#
// ValueBox<string> box = new ValueBox<string>(); // コンパイルエラー

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

where T : new()は、引数なしのコンストラクターを持つ型に限定する制約です。

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

通常、型パラメーターTに対してnew T()は使えません。なぜなら、Tに引数なしコンストラクターがあるとは限らないからです。

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

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

Factory<Customer> factory = new Factory<Customer>();
Customer customer = factory.Create();

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

インターフェイスを使って型制約を付けると、Tが特定の機能を持つことを保証できます。

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

このインターフェイスを制約に使います。

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

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

C#
public class User : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
C#
Repository<User> repository = new Repository<User>();
repository.PrintId(new User { Id = 1, Name = "Tanaka" });

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

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

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

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

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

複数の制約を使うことで、ジェネリックなコードでありながら、必要な操作を安全に行えるようになります。

9. ジェネリックインターフェイス・デリゲートの基本

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

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

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

このインターフェイスは、Tの型に応じて保存する対象を変えられます。

C#
public class UserStore : IStore<User>
{
public void Save(User item)
{
Console.WriteLine(item.Name);
}

public User FindById(int id)
{
return new User { Id = id, Name = "Tanaka" };
}
}

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

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

C#
public interface IRepository<T>
{
T GetById(int id);
void Add(T entity);
void Remove(T entity);
}

User用のリポジトリなら、次のように使えます。

C#
public class UserRepository : IRepository<User>
{
public User GetById(int id)
{
return new User { Id = id, Name = "Tanaka" };
}

public void Add(User entity)
{
Console.WriteLine("User added");
}

public void Remove(User entity)
{
Console.WriteLine("User removed");
}
}

Product用にも同じインターフェイスを使えます。

C#
public class ProductRepository : IRepository<Product>
{
public Product GetById(int id)
{
return new Product { Id = id, Name = "Book" };
}

public void Add(Product entity)
{
Console.WriteLine("Product added");
}

public void Remove(Product entity)
{
Console.WriteLine("Product removed");
}
}

共通の操作を型ごとに整理できるため、設計が分かりやすくなります。

9-3. Func<T>やAction<T>などのジェネリックデリゲート

C#には、よく使われるジェネリックデリゲートがあります。

Func<T>は、戻り値を持つ処理を表します。

C#
Func<int, int, int> add = (a, b) => a + b;

int result = add(3, 5);

この例では、Func<int, int, int>は「intを2つ受け取り、intを返す処理」を表します。

Action<T>は、戻り値を持たない処理を表します。

C#
Action<string> print = message => Console.WriteLine(message);

print("Hello");

この例では、Action<string>は「stringを受け取り、戻り値を返さない処理」を表します。

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

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

共通レスポンスを表すResult<T>、データアクセスを共通化するRepository<T>、処理結果を包むResponse<T>、イベントやメッセージを扱うHandler<T>などです。

C#
public interface IHandler<T>
{
void Handle(T message);
}
C#
public class UserCreatedHandler : IHandler<UserCreatedMessage>
{
public void Handle(UserCreatedMessage message)
{
Console.WriteLine(message.UserId);
}
}

ジェネリクスを使うことで、型ごとに安全性を保ちながら、共通の設計ルールを作れます。

10. C#ジェネリクスで初心者がつまずきやすいポイント

10-1. Tは実際の型ではなく型パラメーターである

Tは特別な型名ではありません。実際の型を入れるための仮の名前です。

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

Box<string>として使えば、Tstringになります。

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

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

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

つまり、Tそのものが何かの固定された型というわけではありません。

10-2. 型制約がないと使えないメンバーがある

型制約がない場合、Tに対して使える操作は限られます。

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

ToString()はすべての型が持っているため使えます。

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

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

TIdプロパティを持つとは限らないためです。

この場合は、インターフェイス制約を使います。

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

10-3. List<int>とList<string>は別の型として扱われる

List<int>List<string>は、同じList<T>から作られていますが、実際には別の型として扱われます。

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

List<int>にはintだけ、List<string>にはstringだけを入れられます。

C#
numbers.Add(10);
// numbers.Add("Hello"); // コンパイルエラー

names.Add("Hello");
// names.Add(10); // コンパイルエラー

この違いによって、型安全性が保たれます。

10-4. ジェネリクスと継承の関係で混乱しやすい点

初心者が混乱しやすいのが、ジェネリクスと継承の関係です。

たとえば、DogAnimalを継承しているとします。

C#
public class Animal
{
}

public class Dog : Animal
{
}

DogAnimalとして扱えます。

C#
Animal animal = new Dog();

しかし、通常のList<Dog>をそのままList<Animal>として扱うことはできません。

C#
List<Dog> dogs = new List<Dog>();

// List<Animal> animals = dogs; // コンパイルエラー

これは、もし許可されるとList<Dog>Catのような別のAnimalを追加できてしまい、型安全性が崩れる可能性があるためです。

10-5. 共変性・反変性は必要になった段階で学べばよい

ジェネリクスには、共変性や反変性という高度な概念があります。

IEnumerable<Dog>IEnumerable<Animal>として扱えるような仕組みが関係します。

C#
IEnumerable<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs;

ただし、初心者の段階で最初から深く理解する必要はありません。

まずは、List<T>Dictionary<TKey, TValue>、ジェネリッククラス、ジェネリックメソッド、型制約を理解することが大切です。

共変性・反変性は、インターフェイスやデリゲートをより深く使う段階で学べば十分です。

11. ジェネリクスを使うべき場面・使わない方がよい場面

11-1. 同じ処理を複数の型で使うならジェネリクスが向いている

同じ処理を複数の型で使う場合、ジェネリクスが向いています。

たとえば、値を保持するクラス、処理結果を表すクラス、共通のデータ操作を行うクラスなどです。

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

このResult<T>は、Result<User>にもResult<Product>にも使えます。

C#
Result<User> userResult = new Result<User>();
Result<Product> productResult = new Result<Product>();

型だけが違い、処理の考え方が同じであれば、ジェネリクスを使うとコードをきれいにまとめられます。

11-2. 型ごとに処理内容が大きく違う場合は無理に使わない

一方で、型ごとに処理内容が大きく違う場合は、無理にジェネリクスを使わない方がよいこともあります。

たとえば、UserOrderで処理内容がまったく違うのに、無理にService<T>として共通化すると、かえって分かりにくいコードになります。

C#
public class Service<T>
{
public void Execute(T item)
{
// 型ごとの分岐が大量に増えると読みにくい
}
}

ジェネリクスは、共通化できる部分があるときに使うものです。

型ごとにロジックが大きく異なる場合は、個別のクラスやメソッドに分けた方が読みやすくなります。

11-3. 可読性が下がるほど複雑なジェネリクスは避ける

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

C#
public class Processor<TInput, TOutput, TContext, TOption>
{
}

型パラメーターが増えすぎると、クラスの役割が分かりにくくなります。

初心者のうちは、まずTTKey, TValueのような分かりやすい使い方から始めましょう。

複雑なジェネリクスを使うよりも、読みやすく保守しやすいコードを書くことが重要です。

11-4. 初心者が実務で判断するための目安

実務でジェネリクスを使うか迷ったら、次のように考えると判断しやすくなります。

同じ処理を型違いで何度も書いているなら、ジェネリクスを検討します。

キャストが多くなっているなら、ジェネリクスで型安全にできないか考えます。

object型で何でも受け取る設計になっているなら、本当にそれでよいか見直します。

ただし、共通化によってコードが分かりにくくなる場合は、無理にジェネリクスを使う必要はありません。

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

12-1. 汎用的な結果クラスResult<T>を作る

実務でよく使われる例として、処理結果を表すResult<T>クラスがあります。

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

public Result(bool isSuccess, string message, T data)
{
IsSuccess = isSuccess;
Message = message;
Data = data;
}
}

このクラスを使うと、さまざまな型の結果を同じ形で表現できます。

C#
Result<string> result = new Result<string>(
true,
"取得に成功しました",
"Hello"
);

Dataの型はstringです。

C#
string data = result.Data;

12-2. 共通レスポンスをジェネリクスで表現する

APIのレスポンスを表すクラスにもジェネリクスはよく使われます。

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

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

C#
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
C#
ApiResponse<User> response = new ApiResponse<User>
{
Success = true,
Data = new User { Id = 1, Name = "Tanaka" },
ErrorMessage = null
};

商品一覧を返す場合は、List<Product>を指定できます。

C#
ApiResponse<List<Product>> response = new ApiResponse<List<Product>>
{
Success = true,
Data = new List<Product>(),
ErrorMessage = null
};

このように、レスポンスの形は共通にしながら、中身の型だけを変えられます。

12-3. リポジトリクラスをジェネリクスで共通化する

データアクセスの処理では、リポジトリをジェネリクスで共通化することがあります。

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

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

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

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

Userクラスを作ります。

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

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

C#
Repository<User> userRepository = new Repository<User>();

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

User user = userRepository.GetById(1);

where T : IEntityを使っているため、Repository<T>の中でIdプロパティを安全に使えます。

12-4. サンプルコードから学ぶ設計の考え方

ジェネリクスを使った設計では、「何を共通化し、何を型ごとに変えるか」を考えることが重要です。

Result<T>では、成功・失敗やメッセージの構造は共通です。一方で、実際に返すデータの型は処理によって変わります。

Repository<T>では、追加、取得、一覧取得といった基本操作は共通です。一方で、扱うエンティティの型はUserProductなどに変わります。

このように、共通部分と変化する部分を分けることで、ジェネリクスのメリットを活かせます。

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

13-1. ジェネリクスと総称型は同じ意味?

はい、ほぼ同じ意味で使われます。

C#の「ジェネリクス」は、日本語では「総称型」と呼ばれることがあります。

ただし、実務や学習記事では「ジェネリクス」や「Generics」という表現の方がよく使われます。

13-2. T以外の名前を使ってもよい?

はい、T以外の名前を使っても問題ありません。

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

ただし、型パラメーターであることが分かりやすい名前にするのが一般的です。

よく使われる名前には、TTKeyTValueTEntityTResultなどがあります。

13-3. ジェネリクスとobject型はどちらを使うべき?

基本的には、型が決まっているならジェネリクスを使う方が安全です。

object型は何でも入れられるため柔軟ですが、キャストが必要になり、型の間違いが実行時まで分かりにくいという問題があります。

一方、ジェネリクスは型を指定して使うため、コンパイル時に型チェックができます。

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

このように書けば、文字列のリストであることが明確です。

本当にさまざまな型を混在させたい場合を除き、初心者はまずジェネリクスを使う方がよいでしょう。

13-4. ジェネリクスと配列・コレクションの関係は?

配列は、同じ型の値を複数扱うための基本的な仕組みです。

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

一方、List<T>Dictionary<TKey, TValue>などのコレクションは、ジェネリクスを使って型安全にデータを管理します。

C#
List<int> numbers = new List<int> { 1, 2, 3 };
Dictionary<int, string> users = new Dictionary<int, string>();

配列よりも要素の追加や削除がしやすいため、実務ではList<T>などのジェネリックコレクションがよく使われます。

13-5. JavaのGenericsやC++のテンプレートとの違いは?

C#のジェネリクス、JavaのGenerics、C++のテンプレートは、いずれも型をパラメーター化して再利用しやすいコードを書くための仕組みです。

ただし、内部の仕組みやコンパイル時の扱いには違いがあります。

C#のジェネリクスは、.NETの型システムと統合されており、実行時にも型情報を利用しやすい特徴があります。

JavaのGenericsは、型消去という仕組みにより、コンパイル後に型情報の扱いがC#とは異なります。

C++のテンプレートは、コンパイル時に型ごとのコードを生成する仕組みで、非常に強力ですが複雑になりやすい面もあります。

初心者の段階では、まずC#のジェネリクスをC#の文法として理解すれば十分です。

まとめ

C#ジェネリクスとは、型をパラメーター化して、型安全に再利用できるコードを書くための仕組みです。

List<T>Dictionary<TKey, TValue>Task<T>IEnumerable<T>など、C#では多くの場面でジェネリクスが使われています。

ジェネリクスを使うと、object型やキャストに頼るコードよりも安全で読みやすくなります。また、同じ処理を複数の型で使い回せるため、コードの重複を減らし、保守性も高められます。

基本となる書き方は、クラスやメソッドに<T>を付けるだけです。

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

さらに、where句を使えば、型パラメーターに制約を付けて、より安全で実用的なジェネリックコードを書けます。

初心者はまず、List<T>Dictionary<TKey, TValue>の使い方から慣れていき、次にジェネリッククラス、ジェネリックメソッド、型制約の順に学ぶと理解しやすいです。

C# genericsを理解できるようになると、C#の標準ライブラリや実務コードが読みやすくなり、再利用性の高い設計もしやすくなります。