C#ジェネリックとは?初心者がつまずく型パラメーター・制約・使い方を実例で完全解説
はじめに
C#を学び始めると、List<T>、Dictionary<TKey, TValue>、Task<T>のように、山かっこを使った書き方をよく見かけます。この<T>の部分が、C#ジェネリックを理解するうえで重要なポイントです。
C#ジェネリックは、簡単にいうと「型を後から指定できる仕組み」です。int用、string用、独自クラス用に似たような処理を何度も書くのではなく、1つのクラスやメソッドをさまざまな型で使い回せます。
この記事では、C# genericの基本である型パラメーター、ジェネリッククラス、ジェネリックメソッド、where制約、よくあるエラーまで、初心者がつまずきやすいポイントを実例付きで解説します。
1. C#ジェネリックとは?初心者向けにわかりやすく解説
1-1. ジェネリックの意味と役割
ジェネリックとは、クラスやメソッドを特定の型に固定せず、使うときに型を指定できる仕組みです。
たとえば、List<int>は整数のリスト、List<string>は文字列のリストです。どちらも同じList<T>という仕組みを使っていますが、Tに入る型が異なります。
C#List<int> numbers = new List<int>();
numbers.Add(10);
List<string> names = new List<string>();
names.Add("Alice");
List<T>のTは、後から具体的な型に置き換わる「型パラメーター」です。MicrosoftのC#ドキュメントでも、ジェネリックは型安全を維持しながら任意の型で動作するコードを書く仕組みとして説明されています。
1-2. 型を後から決められる仕組み
通常のクラスでは、扱う型をあらかじめ決めます。
C#public class IntBox
{
public int Value { get; set; }
}
このクラスはint専用です。stringを入れたい場合は、別のクラスを作る必要があります。
C#public class StringBox
{
public string Value { get; set; }
}
ジェネリックを使うと、次のように1つのクラスで対応できます。
C#public class Box<T>
{
public T Value { get; set; }
}
使うときに型を指定します。
C#Box<int> intBox = new Box<int>();
intBox.Value = 100;
Box<string> stringBox = new Box<string>();
stringBox.Value = "Hello";
このように、Tの具体的な型を使用時に決められるのがジェネリックの特徴です。
1-3. ジェネリックを使うと何が便利になるのか
ジェネリックを使うと、主に次のようなメリットがあります。
同じようなコードを何度も書かなくて済みます。int用、string用、User用のクラスを別々に作る必要がなくなります。
型安全になります。List<int>にはintしか入れられないため、間違って文字列を追加しようとするとコンパイル時にエラーになります。
キャストが減ります。object型を使った場合のように、取り出すたびに(int)や(string)へ変換する必要がありません。
1-4. int・string・独自クラスを同じ処理で扱える理由
ジェネリックでは、型そのものをパラメーターとして扱います。
C#public class Repository<T>
{
private List<T> items = new List<T>();
public void Add(T item)
{
items.Add(item);
}
public T GetFirst()
{
return items[0];
}
}
このRepository<T>は、intにもstringにも独自クラスにも使えます。
C#Repository<int> numberRepository = new Repository<int>();
numberRepository.Add(1);
Repository<string> nameRepository = new Repository<string>();
nameRepository.Add("Taro");
Repository<User> userRepository = new Repository<User>();
userRepository.Add(new User { Name = "Hanako" });
TがintならRepository<int>、stringならRepository<string>、UserならRepository<User>としてコンパイラが扱ってくれます。
2. C#でジェネリックが必要になる場面
2-1. 同じようなクラスやメソッドを何度も書いてしまう問題
ジェネリックを使わない場合、型ごとに似たようなクラスを作りがちです。
C#public class IntResult
{
public int Value { get; set; }
}
public class StringResult
{
public string Value { get; set; }
}
構造は同じなのに、型が違うだけでクラスが増えていきます。これでは修正箇所が増え、保守が大変になります。
ジェネリックを使えば、次のように1つにまとめられます。
C#public class Result<T>
{
public T Value { get; set; }
}
C#Result<int> score = new Result<int> { Value = 90 };
Result<string> message = new Result<string> { Value = "成功" };
2-2. object型で代用したときのデメリット
ジェネリックを知らないと、object型で何でも受け取ろうとすることがあります。
C#public class ObjectBox
{
public object Value { get; set; }
}
objectはすべての型を入れられるため、一見便利に見えます。
C#ObjectBox box = new ObjectBox();
box.Value = 123;
しかし、取り出すときにはキャストが必要です。
C#int number = (int)box.Value;
もし中身がstringだった場合、実行時エラーになります。
C#box.Value = "abc";
int number = (int)box.Value; // 実行時エラー
2-3. キャストミスや実行時エラーを防げる理由
ジェネリックでは、型の間違いをコンパイル時に検出できます。
C#Box<int> box = new Box<int>();
box.Value = 123;
// box.Value = "abc"; // コンパイルエラー
Box<int>と指定した時点で、Valueはintとして扱われます。そのため、stringを代入しようとするとコンパイラがエラーを出します。
実行してから失敗するのではなく、コードを書いている段階で間違いに気づけるのが大きな利点です。
2-4. 型安全性とコードの再利用性を両立できる仕組み
ジェネリックの強みは、「型安全」と「再利用性」を同時に満たせることです。
objectを使えば再利用性は上がりますが、型安全性が下がります。型ごとにクラスを作れば型安全ですが、再利用性が下がります。
ジェネリックを使うと、1つのコードを複数の型で使い回しながら、型のチェックはコンパイラに任せられます。
C#public T Echo<T>(T value)
{
return value;
}
C#int a = Echo<int>(10);
string b = Echo<string>("hello");
戻り値の型も呼び出し時の型に合わせて決まるため、安全で読みやすいコードになります。
3. ジェネリックの基本構文と読み方
3-1. 型パラメーターTとは何か
Tは、具体的な型が入る場所を表す仮の名前です。
C#public class Box<T>
{
public T Value { get; set; }
}
この場合、Tは変数ではありません。int、string、Userなどの型が入るためのプレースホルダーです。
C#Box<int> box = new Box<int>();
このコードでは、Tがintに置き換わったように考えると理解しやすいです。
3-2. ジェネリッククラスの書き方
ジェネリッククラスは、クラス名の後ろに<T>を付けて定義します。
C#public class Storage<T>
{
private T value;
public void Save(T item)
{
value = item;
}
public T Load()
{
return value;
}
}
使うときは、クラス名の後ろに具体的な型を指定します。
C#Storage<string> storage = new Storage<string>();
storage.Save("データ");
string data = storage.Load();
Storage<string>では、Saveの引数もLoadの戻り値もstringになります。
3-3. ジェネリックメソッドの書き方
ジェネリックメソッドは、メソッド名の後ろに<T>を付けて定義します。
C#public static T GetFirst<T>(T first, T second)
{
return first;
}
呼び出し時には型を明示できます。
C#int number = GetFirst<int>(1, 2);
string text = GetFirst<string>("A", "B");
ただし、多くの場合はコンパイラが引数から型を推論できます。
C#int number = GetFirst(1, 2);
string text = GetFirst("A", "B");
C#のジェネリックメソッドでは、渡された値から型引数を推論できる場合があり、明示的な型指定を省略できます。
3-4. 複数の型パラメーターを使う書き方
型パラメーターは1つだけではありません。複数使うこともできます。
C#public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
}
使うときは、型を順番に指定します。
C#Pair<string, int> pair = new Pair<string, int>
{
First = "Age",
Second = 30
};
Dictionary<TKey, TValue>も、キーの型と値の型という2つの型パラメーターを使う代表例です。
C#Dictionary<string, int> scores = new Dictionary<string, int>();
scores["Alice"] = 90;
3-5. T・TKey・TValueなど命名ルールの考え方
型パラメーター名には、慣習的な付け方があります。
1つだけならTがよく使われます。
C#public class Box<T>
{
}
キーと値を扱う場合は、TKeyとTValueがよく使われます。
C#public class Cache<TKey, TValue>
{
}
要素を表す場合はTItem、結果を表す場合はTResultなども使われます。
C#public TResult Convert<TSource, TResult>(TSource source)
{
// 変換処理
return default(TResult);
}
大切なのは、型パラメーターが何を表しているのか名前から分かるようにすることです。
4. ジェネリックの使い方を実例で理解する
4-1. List<T>で学ぶジェネリックの基本
List<T>は、C#ジェネリックを学ぶうえで最も身近な例です。
C#List<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
numbers.Add(30);
List<int>では、要素の型はintです。そのため、取り出した値もintとして扱えます。
C#int first = numbers[0];
キャストは不要です。
C#// int first = (int)numbers[0]; // 不要
List<string>にすれば、文字列専用のリストになります。
C#List<string> names = new List<string>();
names.Add("Alice");
names.Add("Bob");
string name = names[0];
List<T>などのジェネリックコレクションは、型安全なコレクションクラスとしてC#で日常的に使われます。
4-2. Dictionary<TKey, TValue>の仕組み
Dictionary<TKey, TValue>は、キーと値の組み合わせを管理するコレクションです。
C#Dictionary<string, int> ages = new Dictionary<string, int>();
ages["Taro"] = 25;
ages["Hanako"] = 30;
この場合、TKeyはstring、TValueはintです。
C#int taroAge = ages["Taro"];
キーにstring以外を使おうとするとエラーになります。
C#// ages[100] = 40; // コンパイルエラー
値にもint以外は入れられません。
C#// ages["Jiro"] = "40"; // コンパイルエラー
このように、キーと値の型を明確に決められるため、安全にデータを扱えます。
4-3. 自作ジェネリッククラスの実装例
次は、成功・失敗の結果を表すジェネリッククラスの例です。
C#public class ApiResponse<T>
{
public bool IsSuccess { get; set; }
public T Data { get; set; }
public string ErrorMessage { get; set; }
}
文字列を返すAPIなら、次のように使えます。
C#ApiResponse<string> response = new ApiResponse<string>
{
IsSuccess = true,
Data = "取得成功"
};
ユーザー情報を返すAPIなら、独自クラスを指定できます。
C#public class User
{
public string Name { get; set; }
}
C#ApiResponse<User> userResponse = new ApiResponse<User>
{
IsSuccess = true,
Data = new User { Name = "Alice" }
};
ApiResponse<T>という1つのクラスで、さまざまな戻り値の型に対応できます。
4-4. 自作ジェネリックメソッドの実装例
次は、配列の先頭要素を返すジェネリックメソッドです。
C#public static T FirstOrDefaultValue<T>(T[] items)
{
if (items.Length == 0)
{
return default(T);
}
return items[0];
}
int配列にも使えます。
C#int[] numbers = { 10, 20, 30 };
int firstNumber = FirstOrDefaultValue(numbers);
string配列にも使えます。
C#string[] names = { "Alice", "Bob" };
string firstName = FirstOrDefaultValue(names);
同じ処理を、型に依存せず使い回せるのがジェネリックメソッドの便利な点です。
4-5. 戻り値や引数に型パラメーターを使う例
型パラメーターは、引数にも戻り値にも使えます。
C#public static T Choose<T>(bool condition, T value1, T value2)
{
return condition ? value1 : value2;
}
C#int number = Choose(true, 1, 2);
string text = Choose(false, "A", "B");
この場合、value1、value2、戻り値はすべて同じTです。
異なる型を扱いたい場合は、複数の型パラメーターを使います。
C#public static string FormatPair<TKey, TValue>(TKey key, TValue value)
{
return $"{key}: {value}";
}
C#string result = FormatPair("Age", 30);
5. 型パラメーターで初心者がつまずくポイント
5-1. Tは変数ではなく型を表す
初心者がよく混乱するのが、Tを普通の変数だと思ってしまうことです。
C#public class Box<T>
{
public T Value { get; set; }
}
このTは値を入れる変数ではなく、型を表します。
C#Box<int> box = new Box<int>();
この時点で、Tはintとして扱われます。
つまり、Tは「何かの値」ではなく、「何かの型」です。
5-2. 型引数と型パラメーターの違い
型パラメーターと型引数は似ていますが、意味が異なります。
定義側に書くTが型パラメーターです。
C#public class Box<T>
{
}
使用側に書くintやstringが型引数です。
C#Box<int> intBox = new Box<int>();
Box<string> stringBox = new Box<string>();
つまり、Tは受け取る側の仮の名前、intやstringは実際に渡す型です。
5-3. コンパイラが型を推論できる場合とできない場合
ジェネリックメソッドでは、引数から型を推論できる場合があります。
C#public static void Print<T>(T value)
{
Console.WriteLine(value);
}
C#Print(123); // Tはint
Print("hello"); // Tはstring
しかし、引数から型が分からない場合は推論できません。
C#public static T Create<T>()
{
return default(T);
}
このメソッドは引数がないため、次のように型を明示する必要があります。
C#int number = Create<int>();
string text = Create<string>();
戻り値の代入先だけでは、型推論できない場面がある点に注意しましょう。
5-4. Tに対して使えるメンバーが限られる理由
制約のないTに対しては、どんな型が来るか分かりません。
C#public static void ShowName<T>(T item)
{
// Console.WriteLine(item.Name); // コンパイルエラー
}
TがUserならNameプロパティがあるかもしれません。しかし、intやstringが来る可能性もあります。
そのため、コンパイラはTにNameが存在するとは判断できません。
この問題を解決するには、インターフェース制約や基底クラス制約を使います。
C#public interface IHasName
{
string Name { get; }
}
C#public static void ShowName<T>(T item) where T : IHasName
{
Console.WriteLine(item.Name);
}
5-5. default(T)とnullの違い
default(T)は、型Tの既定値を返します。
C#default(int) // 0
default(bool) // false
default(string) // null
Tが値型なら、その型の初期値になります。intなら0、boolならfalseです。
Tが参照型なら、基本的にはnullになります。
C#public static T GetDefault<T>()
{
return default(T);
}
C#int number = GetDefault<int>(); // 0
string text = GetDefault<string>(); // null
default(T)は必ずnullになるわけではありません。型によって結果が変わる点を覚えておきましょう。
6. ジェネリック制約whereの使い方
6-1. ジェネリック制約とは何か
ジェネリック制約とは、型パラメーターTに指定できる型を制限する仕組みです。
C#public static void Method<T>(T value) where T : class
{
}
この例では、Tには参照型だけを指定できます。
制約を付けることで、コンパイラに「この型はこういう特徴を持っている」と伝えられます。その結果、制約なしでは使えなかったメソッドやプロパティを呼び出せるようになります。C#の公式ドキュメントでも、制約はジェネリック型やメソッドが受け入れる型引数を制限し、型パラメーターに対して特定の操作を可能にするものとして説明されています。
6-2. where T : class の使い方
where T : classは、Tを参照型に制限します。
C#public class ReferenceBox<T> where T : class
{
public T Value { get; set; }
}
次のようにstringや独自クラスを指定できます。
C#ReferenceBox<string> box1 = new ReferenceBox<string>();
ReferenceBox<User> box2 = new ReferenceBox<User>();
一方、intのような値型は指定できません。
C#// ReferenceBox<int> box = new ReferenceBox<int>(); // コンパイルエラー
参照型だけを扱いたい場合に使います。
6-3. where T : struct の使い方
where T : structは、Tを値型に制限します。
C#public class ValueBox<T> where T : struct
{
public T Value { get; set; }
}
int、double、DateTimeなどを指定できます。
C#ValueBox<int> intBox = new ValueBox<int>();
ValueBox<DateTime> dateBox = new ValueBox<DateTime>();
stringや独自クラスなどの参照型は指定できません。
C#// ValueBox<string> box = new ValueBox<string>(); // コンパイルエラー
6-4. where T : new() の使い方
where T : new()は、Tが引数なしのpublicコンストラクターを持つことを要求します。
C#public static T CreateInstance<T>() where T : new()
{
return new T();
}
この制約があるため、メソッド内でnew T()を書けます。
C#User user = CreateInstance<User>();
C#public class User
{
public string Name { get; set; }
}
new()制約がない場合、new T()はコンパイルエラーになります。
6-5. where T : インターフェース の使い方
インターフェース制約を使うと、Tが特定のインターフェースを実装していることを保証できます。
C#public interface IPrintable
{
void Print();
}
C#public static void PrintItem<T>(T item) where T : IPrintable
{
item.Print();
}
where T : IPrintableがあるため、item.Print()を安全に呼び出せます。
C#public class Report : IPrintable
{
public void Print()
{
Console.WriteLine("レポートを印刷します");
}
}
C#PrintItem(new Report());
6-6. where T : 基底クラス の使い方
基底クラス制約を使うと、Tが特定のクラスを継承していることを保証できます。
C#public class Entity
{
public int Id { get; set; }
}
C#public static void ShowId<T>(T entity) where T : Entity
{
Console.WriteLine(entity.Id);
}
TはEntityまたはEntityを継承したクラスに限定されるため、Idプロパティを呼び出せます。
C#public class Product : Entity
{
public string Name { get; set; }
}
C#ShowId(new Product { Id = 1, Name = "PC" });
6-7. 複数の制約を組み合わせる方法
制約は複数組み合わせられます。
C#public static T CreatePrintable<T>() where T : class, IPrintable, new()
{
T item = new T();
item.Print();
return item;
}
この場合、Tには次の条件があります。
classなので参照型であること。
IPrintableを実装していること。
new()なので引数なしのpublicコンストラクターを持つこと。
複数制約を使うと、型を柔軟にしながら、必要な機能を安全に呼び出せます。
7. ジェネリック制約を使う実践例
7-1. new()制約でインスタンスを生成する
ジェネリックでインスタンスを作りたい場合は、new()制約が必要です。
C#public static T Create<T>() where T : new()
{
return new T();
}
C#public class Customer
{
public string Name { get; set; }
}
C#Customer customer = Create<Customer>();
制約がない場合、Tに引数なしコンストラクターがあるか分からないため、new T()は使えません。
C#public static T CreateWithoutConstraint<T>()
{
// return new T(); // コンパイルエラー
return default(T);
}
7-2. インターフェース制約で特定のメソッドを呼び出す
次のような保存処理を考えます。
C#public interface ISavable
{
void Save();
}
C#public static void SaveAll<T>(List<T> items) where T : ISavable
{
foreach (T item in items)
{
item.Save();
}
}
where T : ISavableがあるため、item.Save()を呼び出せます。
C#public class Document : ISavable
{
public void Save()
{
Console.WriteLine("ドキュメントを保存しました");
}
}
C#List<Document> documents = new List<Document>
{
new Document(),
new Document()
};
SaveAll(documents);
7-3. 基底クラス制約で共通プロパティを扱う
共通の基底クラスを持つデータを扱う場合にも制約が便利です。
C#public abstract class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
}
C#public static void PrintCreatedAt<T>(T entity) where T : BaseEntity
{
Console.WriteLine(entity.CreatedAt);
}
C#public class Order : BaseEntity
{
public decimal Amount { get; set; }
}
C#Order order = new Order
{
Id = 1,
CreatedAt = DateTime.Now,
Amount = 5000
};
PrintCreatedAt(order);
BaseEntityを継承していることが保証されるため、IdやCreatedAtを安全に使えます。
7-4. 制約なしではコンパイルエラーになるコード例
次のコードは、一見動きそうに見えます。
C#public static void PrintName<T>(T item)
{
Console.WriteLine(item.Name);
}
しかし、これはコンパイルエラーになります。TにNameプロパティがあるとは限らないからです。
解決するには、共通インターフェースを作ります。
C#public interface IHasName
{
string Name { get; }
}
C#public static void PrintName<T>(T item) where T : IHasName
{
Console.WriteLine(item.Name);
}
C#public class Employee : IHasName
{
public string Name { get; set; }
}
これでNameプロパティを安全に呼び出せます。
7-5. 制約を付けるべきケースと付けすぎに注意するケース
制約は、Tに対して特定のメソッドやプロパティを使いたいときに付けるべきです。
たとえば、CompareToを呼びたいならIComparable<T>制約が有効です。
C#public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}
一方で、必要のない制約を付けすぎると、使える型が減ります。
C#public class Box<T> where T : class, new()
{
}
単に値を保持するだけなら、classやnew()は不要かもしれません。
制約は「その制約がないと実装できないか」を基準に考えると、過剰設計を避けやすくなります。
8. C#ジェネリックのメリット・デメリット
8-1. メリット1:型安全にコードを再利用できる
ジェネリックの最大のメリットは、型安全なままコードを再利用できることです。
C#public class Holder<T>
{
public T Value { get; set; }
}
C#Holder<int> intHolder = new Holder<int>();
intHolder.Value = 10;
Holder<string> stringHolder = new Holder<string>();
stringHolder.Value = "hello";
型ごとに別々のクラスを作らなくても、コンパイラが型をチェックしてくれます。
8-2. メリット2:キャスト処理を減らせる
object型を使うと、取り出すたびにキャストが必要です。
C#object value = 100;
int number = (int)value;
ジェネリックなら、最初から型が決まっています。
C#Box<int> box = new Box<int>();
box.Value = 100;
int number = box.Value;
キャストが減ることで、コードが読みやすくなり、実行時エラーのリスクも下がります。
8-3. メリット3:コレクションや共通処理を柔軟に作れる
ジェネリックは、コレクションや共通処理と相性が良いです。
C#public class PagedResult<T>
{
public List<T> Items { get; set; }
public int TotalCount { get; set; }
}
商品一覧にも使えます。
C#PagedResult<Product> products = new PagedResult<Product>();
ユーザー一覧にも使えます。
C#PagedResult<User> users = new PagedResult<User>();
「ページングされた結果」という共通構造を、さまざまなデータ型に再利用できます。
8-4. デメリット1:初心者には構文が読みにくい
ジェネリックは、慣れるまで構文が読みにくく感じます。
C#Dictionary<string, List<User>> usersByGroup;
このように入れ子になると、初心者には難しく見えます。
読み方のコツは、外側から順に分解することです。
Dictionary<string, List<User>>は、「キーがstring、値がList<User>の辞書」です。
複雑な型は、型エイリアスやクラス設計を見直すことで読みやすくできます。
8-5. デメリット2:抽象化しすぎると保守しづらくなる
ジェネリックは便利ですが、何でもジェネリックにすればよいわけではありません。
C#public class Processor<TInput, TOutput, TContext, TOption>
{
}
型パラメーターが増えすぎると、使う側が理解しづらくなります。
実務では、「本当に複数の型で使い回す必要があるか」を考えることが大切です。1つの型でしか使わない処理なら、通常のクラスやメソッドのほうが分かりやすい場合もあります。
9. ジェネリックでよくあるエラーと解決方法
9-1. 型Tに特定のメソッドやプロパティが存在しないと言われる
よくあるエラーは、Tに対して存在が保証されていないメンバーを呼ぼうとするケースです。
C#public static void Show<T>(T item)
{
// Console.WriteLine(item.Id); // エラー
}
解決策は、インターフェースや基底クラスで制約を付けることです。
C#public interface IHasId
{
int Id { get; }
}
C#public static void Show<T>(T item) where T : IHasId
{
Console.WriteLine(item.Id);
}
9-2. new T()が使えないときの原因
new T()を使うには、new()制約が必要です。
C#public static T Create<T>()
{
// return new T(); // エラー
return default(T);
}
修正後は次のようになります。
C#public static T Create<T>() where T : new()
{
return new T();
}
where T : new()は、型Tが引数なしのpublicコンストラクターを持つことを要求します。C#の制約一覧でも、new()制約はパラメーターなしのpublicコンストラクターを必要とする制約として定義されています。
9-3. nullを代入できないときの原因
Tが値型の可能性がある場合、単純にnullを代入できないことがあります。
C#public static T GetNull<T>()
{
// return null; // Tがintなどの場合に問題
return default(T);
}
Tがintならnullは代入できません。型に応じた既定値を返したいならdefault(T)を使います。
参照型だけに限定したい場合は、class制約を使います。
C#public static T GetNull<T>() where T : class
{
return null;
}
ただし、nullable reference typesを有効にしているプロジェクトでは、T?などの書き方も考慮する必要があります。
9-4. 型推論できないときの対処法
次のようなメソッドは、引数がないため型推論できません。
C#public static T GetDefault<T>()
{
return default(T);
}
呼び出し時には型を明示します。
C#int number = GetDefault<int>();
string text = GetDefault<string>();
引数から型が分かる設計に変更する方法もあります。
C#public static T GetOrDefault<T>(T value)
{
return value;
}
C#int number = GetOrDefault(10);
9-5. 制約の順番や組み合わせで発生するエラー
複数の制約には、書き方のルールがあります。
C#public class Sample<T> where T : class, IComparable<T>, new()
{
}
一般的に、classやstructなどの制約を先に書き、new()制約は最後に書きます。
次のような組み合わせはできません。
C#// where T : class, struct // 同時に指定できない
classは参照型、structは値型を表すため、同時に満たせないからです。
制約エラーが出たら、「その条件を同時に満たせる型が存在するか」を確認しましょう。
10. ジェネリックと関連機能の違い
10-1. ジェネリックとobject型の違い
object型は何でも入れられますが、取り出すときにキャストが必要です。
C#object value = 123;
int number = (int)value;
ジェネリックは、使う時点で型を決めます。
C#Box<int> box = new Box<int>();
box.Value = 123;
int number = box.Value;
objectは柔軟ですが型安全性が低く、ジェネリックは柔軟性と型安全性を両立できます。
10-2. ジェネリックとインターフェースの違い
インターフェースは「どんな機能を持つか」を表します。
C#public interface IPrintable
{
void Print();
}
ジェネリックは「どんな型で使うか」を後から指定する仕組みです。
C#public class Printer<T>
{
public void Print(T value)
{
Console.WriteLine(value);
}
}
両者は対立するものではなく、組み合わせて使えます。
C#public class Printer<T> where T : IPrintable
{
public void Print(T value)
{
value.Print();
}
}
10-3. ジェネリックとオーバーロードの違い
オーバーロードは、引数の型や数が異なる同名メソッドを複数定義する仕組みです。
C#public void Print(int value)
{
Console.WriteLine(value);
}
public void Print(string value)
{
Console.WriteLine(value);
}
ジェネリックを使うと、1つのメソッドにまとめられる場合があります。
C#public void Print<T>(T value)
{
Console.WriteLine(value);
}
型ごとに処理が大きく違うならオーバーロード、処理が共通ならジェネリックが向いています。
10-4. ジェネリックと継承の使い分け
継承は、「共通の性質を持つクラス」を表現するときに使います。
C#public class Animal
{
public string Name { get; set; }
}
public class Dog : Animal
{
}
ジェネリックは、「同じ処理をさまざまな型で使う」ときに使います。
C#public class Repository<T>
{
public void Add(T item)
{
}
}
共通のプロパティやメソッドを扱いたいなら継承やインターフェース、型に依存しない共通処理を作りたいならジェネリックを検討します。
10-5. ジェネリックとdynamicの違い
dynamicは、コンパイル時ではなく実行時に型を判断します。
C#dynamic value = "hello";
Console.WriteLine(value.Length);
存在しないメンバーを呼んでも、コンパイル時にはエラーにならない場合があります。
C#dynamic value = 123;
// value.Length; // 実行時エラー
ジェネリックはコンパイル時に型をチェックします。
C#public void Print<T>(T value)
{
Console.WriteLine(value);
}
型安全性を重視するなら、基本的にはdynamicよりジェネリックを優先したほうが安全です。
11. 初心者がジェネリックを使いこなすための学習手順
11-1. まずはList<T>とDictionary<TKey, TValue>に慣れる
初心者は、まずList<T>とDictionary<TKey, TValue>から慣れるのがおすすめです。
C#List<string> names = new List<string>();
names.Add("Alice");
C#Dictionary<string, int> scores = new Dictionary<string, int>();
scores["Bob"] = 80;
この2つを使いながら、「Tに具体的な型を入れる」という感覚を身につけましょう。
11-2. 共通処理をジェネリックメソッドに置き換える
次に、型だけが違う共通処理をジェネリックメソッドにしてみます。
C#public static void Print<T>(T value)
{
Console.WriteLine(value);
}
C#Print(100);
Print("hello");
Print(DateTime.Now);
同じ処理を複数の型で使えることを体感できます。
11-3. 型パラメーターTの動きをデバッグで確認する
ジェネリックが分かりにくい場合は、デバッグで型を確認すると理解しやすくなります。
C#public static void ShowType<T>(T value)
{
Console.WriteLine(typeof(T).Name);
}
C#ShowType(123); // Int32
ShowType("hello"); // String
typeof(T)を使うと、Tが実際にどの型として扱われているか確認できます。
11-4. 必要になったらwhere制約を追加する
最初から複雑な制約を覚える必要はありません。
まずは制約なしのジェネリックを使い、Tに対して特定のメソッドやプロパティを呼びたくなったらwhere制約を学ぶ、という順番で十分です。
C#public static void PrintName<T>(T item) where T : IHasName
{
Console.WriteLine(item.Name);
}
「なぜ制約が必要なのか」を実際のエラーから理解すると、定着しやすくなります。
11-5. 実務で使いやすいジェネリック設計の考え方
実務でジェネリックを使うときは、次の考え方が大切です。
まず、型だけが違う共通処理かどうかを確認します。
次に、型パラメーター名を分かりやすくします。単純ならT、キーと値ならTKeyとTValueのようにします。
最後に、制約は必要最小限にします。制約を付けすぎると使いにくくなるため、実装に本当に必要な条件だけを指定しましょう。
12. C#ジェネリックに関するよくある質問
12-1. ジェネリックはいつ使うべき?
型は違うが処理の流れが同じときに使うべきです。
たとえば、値を保持するクラス、検索結果を表すクラス、ページング結果、リポジトリ、共通の変換処理などはジェネリックと相性が良いです。
C#public class Result<T>
{
public bool Success { get; set; }
public T Data { get; set; }
}
逆に、特定の型でしか使わない処理なら、無理にジェネリックにする必要はありません。
12-2. 型パラメーターTは必ずTという名前にする?
必ずTにする必要はありません。
1つだけならTが一般的ですが、意味があるならTItem、TResult、TKey、TValueなどを使えます。
C#public class Result<TResult>
{
public TResult Value { get; set; }
}
複数の型パラメーターを使う場合は、役割が分かる名前にすると読みやすくなります。
12-3. ジェネリック制約は必ず必要?
必ず必要ではありません。
単に値を保持したり、引数をそのまま返したりするだけなら制約は不要です。
C#public static T Echo<T>(T value)
{
return value;
}
一方で、Tに対して特定のメソッドやプロパティを呼びたい場合は制約が必要です。
C#public static void Save<T>(T item) where T : ISavable
{
item.Save();
}
12-4. ジェネリックメソッドとジェネリッククラスはどう使い分ける?
クラス全体で同じ型を扱うなら、ジェネリッククラスを使います。
C#public class Repository<T>
{
public void Add(T item)
{
}
public T Find(int id)
{
return default(T);
}
}
一部のメソッドだけで型を柔軟にしたいなら、ジェネリックメソッドを使います。
C#public static void Print<T>(T value)
{
Console.WriteLine(value);
}
型パラメーターをクラス全体で共有したいか、メソッド単体で使いたいかで判断しましょう。
12-5. 初心者はどこまで理解すればよい?
初心者は、まず次の内容を理解できれば十分です。
List<T>のTは型を表すこと。
List<int>ならint専用、List<string>ならstring専用になること。
ジェネリックを使うと、型安全にコードを再利用できること。
where制約は、Tに条件を付けたいときに使うこと。
最初から高度な制約や共変性・反変性まで理解する必要はありません。まずはList<T>、Dictionary<TKey, TValue>、簡単なジェネリックメソッドを使えるようになることを目標にしましょう。
まとめ
C#ジェネリックは、型を後から指定できる仕組みです。List<T>、Dictionary<TKey, TValue>のように、C#では日常的に使われています。
ジェネリックを使うと、int、string、独自クラスなどを同じクラスやメソッドで扱えます。さらに、object型のような危険なキャストを減らし、コンパイル時に型の間違いを検出できます。
基本構文は、クラス名やメソッド名の後ろに<T>を付けるだけです。
C#public class Box<T>
{
public T Value { get; set; }
}
必要に応じて、where T : class、where T : struct、where T : new()、where T : インターフェース、where T : 基底クラスなどの制約を使います。
ジェネリックは最初は難しく見えますが、考え方は「型を変えても同じ処理を安全に使い回す」ことです。まずはList<T>とDictionary<TKey, TValue>を使いながら、Tがどの型に置き換わるのかを意識すると理解しやすくなります。

