C#ジェネリック型とは?Listの意味からwhere制約まで初心者にもわかる使い方入門

はじめに

C#を学び始めると、List<T>Dictionary<TKey, TValue>IEnumerable<T>のように、名前の後ろに<T>が付いた型をよく見かけます。この<T>を使った仕組みが「ジェネリック型」です。

最初は見た目が難しく感じるかもしれませんが、ジェネリック型はC#でとてもよく使われる基本機能です。特にList<T>は、配列より柔軟にデータを扱えるため、実務でも学習中のコードでも頻繁に登場します。

この記事では、C#のジェネリック型とは何か、List<T>の意味、where制約の使い方、実践的なサンプルまで、初心者にもわかりやすく解説します。

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

C#のジェネリック型とは、クラスやメソッドを定義するときに、扱うデータの型を固定せず、後から指定できる仕組みです。

たとえば、整数を扱うクラス、文字列を扱うクラス、ユーザー情報を扱うクラスをそれぞれ別々に作るのではなく、1つの汎用的なクラスを作って、使うときに型を指定できます。

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

この例では、List<T>という仕組みに対して、intstringという型を指定しています。

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

ジェネリック型を一言で表すなら、「型を後から決められる仕組み」です。

通常のクラスでは、プロパティやメソッドの引数の型をあらかじめ決めておきます。

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

このクラスはint専用です。文字列を入れたい場合は、別にStringBoxのようなクラスを作る必要があります。

しかし、ジェネリック型を使うと次のように書けます。

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

このTは、使うときに具体的な型へ置き換わります。

C#
Box<int> intBox = new Box<int>();
Box<string> stringBox = new Box<string>();

Box<int>ではTintになり、Box<string>ではTstringになります。

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

C#でジェネリック型が使われる主な理由は、型安全で再利用しやすいコードを書けるからです。

たとえば、複数の型に対応するためにobject型を使う方法もあります。

C#
object value = "Hello";

objectは多くの型を入れられますが、取り出すときにキャストが必要になります。

C#
string text = (string)value;

キャストを間違えると実行時エラーが発生します。一方、ジェネリック型を使えば、コンパイル時点で型の間違いを検出できます。

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

キャストが不要になり、コードも読みやすくなります。

1-3. ジェネリック型と通常のクラス・メソッドの違い

通常のクラスやメソッドは、扱う型が固定されています。

C#
public int Add(int a, int b)
{
return a + b;
}

このメソッドはint専用です。doubledecimalを扱いたい場合は、別のメソッドが必要になります。

一方、ジェネリックメソッドでは、型をパラメーターとして受け取れます。

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

このメソッドは、intstringbool、独自クラスなど、さまざまな型で使えます。

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

通常のクラスやメソッドは「特定の型専用」、ジェネリック型は「型を変えて使える汎用的な仕組み」と考えると理解しやすいです。

1-4. 「<T>」のTは何を表しているのか

<T>Tは、型を表す仮の名前です。一般的にTは「Type」の頭文字として使われます。

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

この時点では、Tintなのかstringなのかは決まっていません。使うときに次のように指定します。

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

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

なお、Tという名前は慣習的によく使われるだけで、必ずTでなければならないわけではありません。

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

意味がわかりやすくなるなら、TValueTItemのような名前を使っても問題ありません。

1-5. 型パラメーターと型引数の違い

ジェネリック型を理解するときは、「型パラメーター」と「型引数」の違いも押さえておくと便利です。

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

この定義に出てくるTが型パラメーターです。まだ具体的な型ではなく、仮の型名です。

一方、次のように使うときに指定するintstringが型引数です。

C#
Box<int> intBox = new Box<int>();
Box<string> stringBox = new Box<string>();

つまり、型パラメーターは「受け取る側の仮の型」、型引数は「実際に渡す具体的な型」です。

2. List<T>の意味を理解しよう

C#でジェネリック型を学ぶとき、最も身近な例がList<T>です。List<T>は、同じ型のデータを複数まとめて管理できるコレクションです。

配列と似ていますが、要素の追加や削除がしやすく、サイズを柔軟に変更できます。

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

この場合、List<string>は文字列だけを入れられるリストです。

2-1. List<T>とは何か

List<T>は、Tで指定した型のデータを順番に格納できるクラスです。

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

List<int>では、int型の値だけを入れられます。stringを入れようとするとコンパイルエラーになります。

C#
// numbers.Add("Hello"); // エラー

このように、List<T>は「何の型を入れるリストなのか」を明確にできる仕組みです。

2-2. List<int>・List<string>・List<Person>の違い

List<T>Tには、さまざまな型を指定できます。

C#
List<int> scores = new List<int>();
List<string> names = new List<string>();
List<Person> people = new List<Person>();

List<int>は整数のリスト、List<string>は文字列のリスト、List<Person>Personクラスのオブジェクトを入れるリストです。

C#
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
C#
List<Person> people = new List<Person>();

people.Add(new Person { Name = "田中", Age = 25 });
people.Add(new Person { Name = "佐藤", Age = 30 });

このように、組み込み型だけでなく、自分で作ったクラスもTに指定できます。

2-3. Tに入れられる型の具体例

Tには、基本的にC#で使えるさまざまな型を指定できます。

C#
List<int> intList = new List<int>();
List<double> doubleList = new List<double>();
List<string> stringList = new List<string>();
List<bool> boolList = new List<bool>();
List<DateTime> dateList = new List<DateTime>();
List<Person> personList = new List<Person>();

値型であるintbool、参照型であるstringや独自クラスも指定できます。

また、インターフェイスを指定することもあります。

C#
List<IDisposable> disposables = new List<IDisposable>();

この場合、IDisposableを実装したオブジェクトをリストに入れられます。

2-4. ArrayListではなくList<T>を使う理由

昔のC#では、ArrayListというコレクションもよく使われていました。ArrayListはさまざまな型を入れられますが、型安全ではありません。

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

このように、整数と文字列を同じリストに入れることもできてしまいます。

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

C#
int number = (int)list[0];

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

一方、List<T>では入れられる型が決まっています。

C#
List<int> numbers = new List<int>();
numbers.Add(10);
// numbers.Add("Hello"); // コンパイルエラー

型の間違いを早い段階で見つけられるため、通常はArrayListではなくList<T>を使います。

2-5. List<T>で型安全になるとはどういうことか

型安全とは、意図しない型のデータが混ざることを防げる性質です。

たとえば、数値だけを扱いたいリストに文字列が入ってしまうと、計算処理で問題が起きます。

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

このリストにはintしか追加できません。そのため、取り出した値も安心してintとして扱えます。

C#
int total = 0;

foreach (int number in numbers)
{
total += number;
}

キャストも不要で、型の間違いもコンパイル時に検出できます。これがList<T>による型安全の大きなメリットです。

3. C#でジェネリック型を使うメリット

C#でジェネリック型を使うメリットは、コードを安全に、わかりやすく、再利用しやすくできることです。

同じような処理を複数の型で使いたい場合、ジェネリック型を使うと重複したコードを減らせます。

3-1. 型変換やキャストを減らせる

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

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

ジェネリック型を使えば、型があらかじめ決まっているためキャストが不要です。

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

string text = box.Value;

キャストが減ることで、コードが読みやすくなり、実行時エラーの可能性も下げられます。

3-2. コンパイル時に型の間違いを検出できる

ジェネリック型では、指定した型と違う値を入れようとするとコンパイルエラーになります。

C#
List<int> numbers = new List<int>();
numbers.Add(100);
// numbers.Add("abc"); // コンパイルエラー

実行してからエラーに気づくのではなく、ビルド時点で問題を発見できるのが大きな利点です。

特に実務では、コンパイル時に間違いを発見できることは品質向上につながります。

3-3. コードの再利用性が高くなる

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

C#
public T GetFirst<T>(List<T> items)
{
return items[0];
}

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

C#
int firstNumber = GetFirst(new List<int> { 1, 2, 3 });
string firstName = GetFirst(new List<string> { "田中", "佐藤" });

処理の内容が同じで、扱う型だけが違う場合は、ジェネリック型がとても役立ちます。

3-4. パフォーマンス面で有利になるケース

ジェネリック型は、特に値型を扱う場合にパフォーマンス面で有利になることがあります。

ArrayListのようにobject型で値型を扱うと、ボックス化とアンボックス化が発生する場合があります。

C#
ArrayList list = new ArrayList();
list.Add(10); // intがobjectとして扱われる
int number = (int)list[0];

一方、List<int>ではintとして扱えるため、余計な変換を避けやすくなります。

C#
List<int> numbers = new List<int>();
numbers.Add(10);
int number = numbers[0];

大量のデータを扱う場合、この違いが効いてくることがあります。

3-5. 同じ処理を複数の型で使い回せる

ジェネリック型を使うと、「型は違うが処理の流れは同じ」という場面に対応しやすくなります。

C#
public void PrintItems<T>(List<T> items)
{
foreach (T item in items)
{
Console.WriteLine(item);
}
}

このメソッドは、整数リストにも文字列リストにも使えます。

C#
PrintItems(new List<int> { 1, 2, 3 });
PrintItems(new List<string> { "A", "B", "C" });

同じ処理を型ごとに書き直す必要がなくなるため、保守しやすいコードになります。

4. ジェネリック型の基本的な書き方

ジェネリック型は、クラス、メソッド、インターフェイス、デリゲートなどで使えます。初心者がまず覚えるべきなのは、ジェネリッククラスとジェネリックメソッドです。

基本形は、名前の後ろに<T>を付けます。

C#
public class ClassName<T>
{
}
C#
public T MethodName<T>(T value)
{
return value;
}

4-1. ジェネリッククラスの定義方法

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

C#
public class DataStore<T>
{
private T _data;

public void SetData(T data)
{
_data = data;
}

public T GetData()
{
return _data;
}
}

使うときは、Tに具体的な型を指定します。

C#
DataStore<string> store = new DataStore<string>();
store.SetData("Hello");

string value = store.GetData();

この場合、DataStore<string>の中ではTstringとして扱われます。

4-2. ジェネリックメソッドの定義方法

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

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

使うときは、型を明示できます。

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

ただし、多くの場合は引数から型を推論できるため、型指定を省略できます。

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

C#が自動的にTを判断してくれるため、コードを簡潔に書けます。

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

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

C#
public class Pair<TKey, TValue>
{
public TKey Key { get; set; }
public TValue Value { get; set; }
}

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

C#
Pair<int, string> item = new Pair<int, string>
{
Key = 1,
Value = "りんご"
};

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

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

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

ジェネリック型のTは、戻り値、引数、フィールド、プロパティなどに使えます。

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

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

public T GetValue()
{
return Value;
}
}

このクラスでは、コンストラクターの引数、プロパティ、戻り値にTを使っています。

C#
Holder<int> holder = new Holder<int>(100);
int value = holder.GetValue();

Tをどこに使うかによって、汎用的なクラスやメソッドを柔軟に作れます。

4-5. 実際に使える簡単なサンプルコード

次の例は、任意の型の値を一時的に保存するシンプルなジェネリッククラスです。

C#
public class SimpleCache<T>
{
private T _value;

public void Save(T value)
{
_value = value;
}

public T Load()
{
return _value;
}
}

使い方は次の通りです。

C#
SimpleCache<string> nameCache = new SimpleCache<string>();
nameCache.Save("田中");

string name = nameCache.Load();
Console.WriteLine(name);

int用にも同じクラスを使えます。

C#
SimpleCache<int> scoreCache = new SimpleCache<int>();
scoreCache.Save(95);

int score = scoreCache.Load();
Console.WriteLine(score);

同じSimpleCache<T>を使いながら、型だけを変えられるのがジェネリック型の便利な点です。

5. where制約とは?ジェネリック型に条件を付ける方法

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

たとえば、「Tは参照型でなければならない」「Tは引数なしコンストラクターを持っていなければならない」「Tは特定のインターフェイスを実装していなければならない」といった条件を指定できます。

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

この例では、Tにはクラスなどの参照型だけを指定できます。

5-1. where制約を使う理由

where制約を使う理由は、ジェネリック型の中で安全に使える機能を増やすためです。

制約がない場合、C#はTがどのような型かわかりません。そのため、Tに特定のメソッドやプロパティがあると仮定して呼び出すことはできません。

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

TNameプロパティがあるとは限らないためです。

そこで、インターフェイス制約を付けます。

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

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

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

5-2. where T : classの意味

where T : classは、Tに参照型だけを指定できるようにする制約です。

C#
public class Service<T> where T : class
{
public void Print(T item)
{
if (item == null)
{
Console.WriteLine("nullです");
}
}
}

class制約を付けると、Tにはクラス、インターフェイス、デリゲートなどの参照型を指定できます。

C#
Service<string> service1 = new Service<string>();
Service<Person> service2 = new Service<Person>();

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

C#
// Service<int> service = new Service<int>(); // エラー

5-3. where T : structの意味

where T : structは、Tに値型だけを指定できるようにする制約です。

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

この場合、intdoubleboolDateTimeなどの値型を指定できます。

C#
ValueStore<int> intStore = new ValueStore<int>();
ValueStore<DateTime> dateStore = new ValueStore<DateTime>();

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

C#
// ValueStore<string> stringStore = new ValueStore<string>(); // エラー

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

5-4. where T : new()の意味

where T : new()は、Tが引数なしコンストラクターを持っていることを保証する制約です。

この制約を付けると、ジェネリック型の中でnew T()を使えるようになります。

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

使う側は次のように書けます。

C#
Factory<Person> factory = new Factory<Person>();
Person person = factory.Create();

ただし、new()制約で指定できるのは、引数なしでインスタンス化できる型です。

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

引数付きコンストラクターしかないクラスの場合は、new()制約を満たせないことがあります。

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

where T : インターフェイス名と書くと、Tにそのインターフェイスを実装した型だけを指定できます。

C#
public interface IEntity
{
int Id { get; }
}
C#
public class Repository<T> where T : IEntity
{
public T FindById(List<T> items, int id)
{
return items.FirstOrDefault(item => item.Id == id);
}
}

この場合、Tは必ずIdプロパティを持つため、ジェネリッククラス内でitem.Idを安全に使えます。

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

実務では、共通のプロパティやメソッドを持つ型だけを対象にしたいときによく使います。

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

where T : 基底クラス名と書くと、Tにその基底クラスまたは派生クラスだけを指定できます。

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

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

Tは必ずAnimalを継承しているため、NameプロパティやEatメソッドを使えます。

C#
public class Dog : Animal
{
}
C#
AnimalService<Dog> service = new AnimalService<Dog>();

共通の基底クラスを持つ型に対して処理をまとめたい場合に便利です。

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

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

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

public int GetId(T item)
{
return item.Id;
}
}

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

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

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

C#
public class Mapper<TSource, TResult>
where TSource : class
where TResult : class, new()
{
}

条件が増えるほどコードの意味は明確になりますが、複雑になりすぎないように注意しましょう。

6. ジェネリック型でよく使う実践例

ジェネリック型は、コレクションだけでなく、検索処理、レスポンスクラス、Repositoryパターン、APIの戻り値など、実務のさまざまな場面で使われます。

ここでは、初心者が理解しやすい実践例を紹介します。

6-1. 共通の検索処理をジェネリックメソッドにする

複数のリストから条件に合う要素を探す処理は、ジェネリックメソッドにできます。

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

int result = FindFirst(numbers, n => n > 2);
Console.WriteLine(result);

文字列にも使えます。

C#
List<string> names = new List<string> { "田中", "佐藤", "鈴木" };

string name = FindFirst(names, n => n.StartsWith("佐"));
Console.WriteLine(name);

型は違っても「条件に合う最初の要素を探す」という処理は共通化できます。

6-2. 汎用的なレスポンスクラスを作る

APIやサービス処理では、成功・失敗の状態とデータをまとめて返したいことがあります。そのような場合、ジェネリック型のレスポンスクラスが便利です。

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

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

C#
ApiResponse<User> response = new ApiResponse<User>
{
Success = true,
Message = "取得成功",
Data = new User { Id = 1, Name = "田中" }
};

商品一覧を返す場合にも同じクラスを使えます。

C#
ApiResponse<List<Product>> productResponse = new ApiResponse<List<Product>>
{
Success = true,
Message = "商品一覧を取得しました",
Data = products
};

戻り値の型だけを変えながら、共通のレスポンス形式を使い回せます。

6-3. Repositoryパターンでジェネリック型を使う

Repositoryパターンでは、データの取得や保存処理を共通化するためにジェネリック型がよく使われます。

C#
public interface IRepository<T> where T : class
{
T FindById(int id);
List<T> GetAll();
void Add(T item);
void Delete(T item);
}

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

C#
public class UserRepository : IRepository<User>
{
public User FindById(int id)
{
return new User();
}

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

public void Add(User item)
{
}

public void Delete(User item)
{
}
}

データ操作の基本形が同じ場合、ジェネリック型によって設計を整理できます。

6-4. Dictionary<TKey, TValue>で複数の型を扱う

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

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

users.Add(1, "田中");
users.Add(2, "佐藤");

この場合、キーはint、値はstringです。

C#
string name = users[1];
Console.WriteLine(name);

独自クラスを値にすることもできます。

C#
Dictionary<int, User> userMap = new Dictionary<int, User>();

userMap.Add(1, new User { Id = 1, Name = "田中" });

Dictionary<TKey, TValue>では、キーの型と値の型を別々に指定できるため、柔軟にデータを管理できます。

6-5. UnityやASP.NETでよく見るジェネリック型の例

Unityでは、GetComponent<T>()のようなジェネリックメソッドをよく見かけます。

C#
Rigidbody rb = GetComponent<Rigidbody>();

このコードでは、Rigidbody型のコンポーネントを取得しています。Tに取得したい型を指定することで、戻り値もその型になります。

ASP.NETでは、Task<T>ActionResult<T>などがよく使われます。

C#
public Task<User> GetUserAsync()
{
return Task.FromResult(new User());
}
C#
public ActionResult<User> GetUser()
{
return new User { Id = 1, Name = "田中" };
}

このように、C#の実践的なフレームワークでもジェネリック型は頻繁に登場します。

7. 初心者がつまずきやすいポイントとエラー対策

ジェネリック型は便利ですが、初心者がつまずきやすいポイントもあります。特に、Tに何を指定すればよいのか、なぜメソッドを呼び出せないのか、なぜnew T()が使えないのかはよくある疑問です。

ここでは、代表的なつまずきポイントと対策を解説します。

7-1. Tに何を指定すればよいかわからない

Tには、そのクラスやメソッドで扱いたいデータの型を指定します。

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

数値を扱いたいならint、文字列を扱いたいならstring、ユーザー情報を扱いたいならUserを指定します。

C#
List<User> users = new List<User>();

迷ったときは、「このリストやクラスの中に何を入れたいのか」を考えると判断しやすくなります。

List<T>Tは、入れる要素の型です。ApiResponse<T>Tは、返したいデータの型です。役割に注目すると理解しやすくなります。

7-2. 型パラメーターに存在しないメソッドを呼び出せない

次のようなコードはエラーになります。

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

これで、Tは必ずNameを持つ型だとコンパイラに伝えられます。

7-3. new T()が使えない理由

制約なしでnew T()を書こうとするとエラーになります。

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

C#から見ると、Tに指定される型が引数なしコンストラクターを持っているとは限りません。そのため、そのままではインスタンスを作れません。

対策はnew()制約を付けることです。

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

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

7-4. staticクラスを型引数にできない理由

C#のstaticクラスはインスタンス化できない特別なクラスです。そのため、通常の型引数として使う場面には向いていません。

C#
public static class Utility
{
public static void Log(string message)
{
Console.WriteLine(message);
}
}

staticクラスは、オブジェクトを作って扱うものではなく、クラス名から直接メソッドを呼び出すものです。

C#
Utility.Log("Hello");

ジェネリック型では、Tを値やオブジェクトとして扱うことが多いため、インスタンス化できないstaticクラスは指定できません。

共通処理をジェネリック型で扱いたい場合は、staticクラスではなく、通常のクラスやインターフェイスを検討しましょう。

7-5. where制約を付けないと起きる問題

where制約を付けない場合、Tに対してできる操作は限られます。

C#
public void Save<T>(T item)
{
// item.Id は使えない
// item.Name も使えない
}

コンパイラは、Tがどのようなプロパティやメソッドを持つかわからないためです。

Idを使いたいなら、次のようにインターフェイス制約を付けます。

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

where制約は、ジェネリック型に「どのような型なら受け入れるか」を伝えるための重要な仕組みです。

7-6. ジェネリック型の可読性が下がるケース

ジェネリック型は便利ですが、使いすぎると可読性が下がることがあります。

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

型パラメーターが多すぎると、何を表しているのか理解しにくくなります。

また、制約が複雑になりすぎると、初心者だけでなく経験者にとっても読みづらいコードになります。

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

必要な場面では有効ですが、無理にジェネリック化しないことも大切です。型パラメーターの名前をわかりやすくし、責務を小さく保つと読みやすいコードになります。

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

ジェネリック型は便利ですが、すべての場面で使えばよいわけではありません。処理を複数の型で使い回したいときには有効ですが、特定の型専用の処理なら通常のクラスやメソッドの方がわかりやすいこともあります。

使うべき場面と使わない方がよい場面を整理しておきましょう。

8-1. ジェネリック型を使うべき代表的なケース

ジェネリック型を使うべき代表的なケースは、同じ処理を複数の型で使い回したい場合です。

たとえば、次のような場面です。

データを格納するクラス、リストや辞書などのコレクション、APIレスポンスの共通形式、検索処理や変換処理、Repositoryパターンなどです。

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

このように、処理の構造は同じで、中身の型だけが変わる場合はジェネリック型が向いています。

8-2. 無理にジェネリック化しない方がよいケース

特定の型にしか使わない処理を無理にジェネリック化すると、かえってわかりにくくなります。

C#
public void PrintUser<T>(T user)
{
}

このメソッドが実際にはUser専用なら、次のように書いた方が明確です。

C#
public void PrintUser(User user)
{
}

ジェネリック型は「複数の型で共通化する意味がある」ときに使うのが基本です。

将来使うかもしれないという理由だけでジェネリック化すると、設計が複雑になりやすいので注意しましょう。

8-3. object型で代用しない方がよい理由

object型を使えば、さまざまな型を受け取ることはできます。

C#
public void Print(object value)
{
Console.WriteLine(value);
}

しかし、object型では本来の型情報が失われやすく、取り出すときにキャストが必要になることがあります。

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

キャストを間違えると実行時エラーになります。

ジェネリック型を使えば、型を保ったまま汎用的な処理を書けます。

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

型安全性を保ちたいなら、安易にobject型で代用せず、ジェネリック型を検討しましょう。

8-4. インターフェイスや継承との使い分け

ジェネリック型、インターフェイス、継承はそれぞれ役割が異なります。

複数の型で同じ処理を使い回したい場合はジェネリック型が向いています。共通の機能を持つことを保証したい場合はインターフェイスが向いています。共通の基本機能や状態を親クラスにまとめたい場合は継承が向いています。

たとえば、Idを持つ型だけを扱いたいなら、インターフェイスとジェネリック型を組み合わせます。

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

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

ジェネリック型は単独で使うだけでなく、インターフェイスや継承と組み合わせることで実務的な設計になります。

8-5. 初心者が意識したい設計のポイント

初心者がジェネリック型を使うときは、まず「型だけが違って処理は同じか」を考えるのがおすすめです。

処理内容が型ごとに大きく違うなら、ジェネリック型にしない方がよい場合があります。逆に、保存する、取り出す、検索する、結果を返すなどの共通処理なら、ジェネリック型が役立ちます。

また、Tだけでは意味が伝わりにくい場合は、TItemTKeyTValueTResultのような名前を使うと読みやすくなります。

C#
public class Result<TResult>
{
public TResult Value { get; set; }
}

ジェネリック型は便利さだけでなく、読みやすさも意識して使うことが大切です。

9. C#ジェネリック型の理解を深める練習問題

ジェネリック型は、読むだけでなく実際に書いてみると理解が深まります。ここでは、初心者向けの練習問題を紹介します。

短いコードから始めて、List<T>、ジェネリックメソッド、where制約の順に練習していきましょう。

9-1. List<T>を使った基本問題

まずはList<T>を使って、整数のリストを作成してみましょう。

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

scores.Add(80);
scores.Add(90);
scores.Add(75);

foreach (int score in scores)
{
Console.WriteLine(score);
}

次に、List<string>を使って名前の一覧を表示してみます。

C#
List<string> names = new List<string>
{
"田中",
"佐藤",
"鈴木"
};

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

この練習では、List<T>Tに指定した型だけをリストに追加できることを確認しましょう。

9-2. ジェネリックメソッドを作る練習

次は、渡された値をそのまま表示するジェネリックメソッドを作ってみましょう。

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

使い方は次の通りです。

C#
PrintValue<int>(100);
PrintValue<string>("Hello");
PrintValue<bool>(true);

型推論により、次のように書くこともできます。

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

Tが引数の型に応じて変わることを確認しましょう。

9-3. where制約を追加する練習

次は、Idを持つ型だけを受け取るジェネリックメソッドを作ってみます。

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

Userクラスで試してみます。

C#
public class User : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
C#
User user = new User { Id = 1, Name = "田中" };
PrintId(user);

where T : IEntityがあることで、item.Idを安全に使えることを確認しましょう。

9-4. エラーの原因を考える練習

次のコードはなぜエラーになるでしょうか。

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

理由は、Tが引数なしコンストラクターを持っている保証がないからです。

正しく書くには、new()制約を追加します。

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

もう1つ考えてみましょう。

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

このコードがエラーになる理由は、TNameプロパティがあるとは限らないからです。

対策として、Nameを持つインターフェイスを定義し、where制約を付けます。

9-5. 実務で使える小さなジェネリッククラスを作る

最後に、成功・失敗の結果を表す小さなジェネリッククラスを作ってみましょう。

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

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

public static Result<T> Fail(string errorMessage)
{
return new Result<T>
{
Success = false,
ErrorMessage = errorMessage
};
}
}

使い方は次の通りです。

C#
Result<string> result = Result<string>.Ok("成功しました");

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

Result<int>Result<User>としても使えるため、実務でも応用しやすい形です。

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

ここでは、C#のジェネリック型について初心者が疑問に感じやすい点をQ&A形式で整理します。

<T>の意味、object型との違い、where制約の必要性などを確認しておきましょう。

10-1. <T>のTは必ずTでなければいけないのか

<T>Tは、必ずTでなければならないわけではありません。Tは慣習的に使われる名前です。

次のように書いても問題ありません。

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

型パラメーターが1つだけならTがよく使われます。複数ある場合は、TKeyTValueTItemTResultなど、役割がわかる名前にすると読みやすくなります。

C#
public class Pair<TKey, TValue>
{
public TKey Key { get; set; }
public TValue Value { get; set; }
}

10-2. ジェネリック型とobject型の違いは何か

object型は多くの型を受け取れますが、取り出すときにキャストが必要になることがあります。

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

一方、ジェネリック型は型情報を保ったまま扱えます。

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

string text = box.Value;

ジェネリック型の方が型安全で、キャストミスによる実行時エラーを避けやすくなります。

10-3. ジェネリッククラスとジェネリックメソッドの違いは何か

ジェネリッククラスは、クラス全体で型パラメーターを使います。

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

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

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

クラス全体で同じ型を扱いたい場合はジェネリッククラス、特定のメソッドだけで型を柔軟にしたい場合はジェネリックメソッドを使うと考えるとよいでしょう。

10-4. where制約は必ず必要なのか

where制約は必ず必要ではありません。Tをそのまま保存したり、取り出したり、表示したりするだけなら制約なしでも使えます。

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

しかし、Tの特定のメソッドやプロパティを使いたい場合は、where制約が必要になることがあります。

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

つまり、where制約は「Tに何らかの条件を求めたいとき」に使います。

10-5. ジェネリック型は初心者でも覚えるべきか

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

理由は、List<T>Dictionary<TKey, TValue>IEnumerable<T>Task<T>など、C#の基本的なライブラリやフレームワークで頻繁に使われるからです。

最初から高度な設計まで理解する必要はありません。まずは、List<int>は整数のリスト、List<string>は文字列のリスト、Tは後から指定する型、という感覚をつかむことが大切です。

慣れてきたら、ジェネリックメソッドやwhere制約を少しずつ学んでいきましょう。

まとめ

C#のジェネリック型は、型を後から指定できる仕組みです。List<T>のように、Tを使うことで、整数、文字列、独自クラスなど、さまざまな型に対応したコードを書けます。

ジェネリック型を使う主なメリットは、型安全に書けること、キャストを減らせること、コンパイル時に間違いを検出できること、コードの再利用性が高くなることです。

特にList<T>は、C#で最もよく使うジェネリック型の1つです。List<int>なら整数のリスト、List<string>なら文字列のリスト、List<Person>ならPersonオブジェクトのリストになります。

また、where制約を使うと、Tに指定できる型へ条件を付けられます。where T : classwhere T : structwhere T : new()where T : インターフェイスなどを使うことで、より安全で意図の明確なジェネリック型を作れます。

初心者のうちは、まずList<T>の意味を理解し、次に簡単なジェネリッククラスやジェネリックメソッドを書いてみるのがおすすめです。ジェネリック型を理解できると、C#のコードが読みやすくなり、実務で使われる設計パターンも理解しやすくなります。