C#ジェネリックとは?初心者向けにの意味・使い方・where制約までサンプルコードで解説
はじめに
C#を学んでいると、List<T>やDictionary<TKey, TValue>、Func<T, TResult>のように、<T>が付いた書き方をよく見かけます。
この<T>を使った仕組みが「ジェネリック」です。
C#ジェネリックは、最初は記号が多くて難しく見えますが、考え方はシンプルです。ひと言でいうと、使う型を後から決められる仕組みです。
たとえば、int用、string用、Userクラス用に同じような処理を何度も書くのは大変です。そこでジェネリックを使うと、型だけを差し替えて、同じ処理を安全に再利用できます。
この記事では、C#ジェネリックとは何か、<T>の意味、ジェネリックメソッド・ジェネリッククラスの使い方、where制約まで、初心者向けにサンプルコード付きで解説します。
1. C#ジェネリックとは?まずは「型を後から決める仕組み」と理解しよう
1-1. ジェネリックの意味を初心者向けに一言で解説
C#ジェネリックとは、クラスやメソッドを作る時点では具体的な型を決めず、使うときに型を指定できる仕組みです。
たとえば、次のようなイメージです。
C#List<int> numbers = new List<int>();
List<string> names = new List<string>();
List<T>は、C#でよく使われるジェネリックの代表例です。
List<int>と書けばintの一覧を扱うリストになり、List<string>と書けばstringの一覧を扱うリストになります。
つまり、Tの部分に入る型を変えることで、同じListという仕組みをいろいろな型で使い回せるのです。
1-2. なぜC#でジェネリックが必要なのか
ジェネリックがない場合、型ごとに似たようなコードを何度も書く必要があります。
たとえば、intの値を保管するクラスと、stringの値を保管するクラスを作るとします。
C#public class IntBox
{
public int Value { get; set; }
}
public class StringBox
{
public string Value { get; set; }
}
この2つのクラスは、扱う型が違うだけで、構造はほとんど同じです。
型が増えるたびにUserBox、ProductBox、DateTimeBoxのようなクラスを作るのは非効率です。
そこでジェネリックを使うと、次のように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";
このように、C#ジェネリックを使うと、型ごとに同じようなコードを書く手間を減らせます。
1-3. ジェネリックを使うと何が便利になるのか
ジェネリックを使う主なメリットは、次の3つです。
1つ目は、コードの重複を減らせることです。型が違っても処理内容が同じであれば、ジェネリックで1つにまとめられます。
2つ目は、型安全に書けることです。List<int>にはintしか入れられないため、間違ってstringを追加しようとするとコンパイルエラーになります。
C#List<int> numbers = new List<int>();
numbers.Add(10);
// numbers.Add("abc"); // コンパイルエラー
3つ目は、キャストを減らせることです。object型で共通化すると、取り出すときにキャストが必要になることがありますが、ジェネリックなら指定した型のまま扱えます。
C#List<string> names = new List<string>();
names.Add("田中");
string name = names[0]; // キャスト不要
1-4. List<T>やDictionary<TKey, TValue>でよく見るジェネリックの例
C#ジェネリックは、実は日常的によく使われています。
代表的なのがList<T>です。
C#List<string> fruits = new List<string>();
fruits.Add("Apple");
fruits.Add("Orange");
fruits.Add("Banana");
foreach (string fruit in fruits)
{
Console.WriteLine(fruit);
}
Dictionary<TKey, TValue>もよく使われるジェネリックです。
C#Dictionary<string, int> scores = new Dictionary<string, int>();
scores["Alice"] = 90;
scores["Bob"] = 80;
Console.WriteLine(scores["Alice"]);
この例では、TKeyにstring、TValueにintを指定しています。
つまり、キーがstring、値がintの辞書になります。
C#ジェネリックは、特別な上級機能というより、List<T>やDictionary<TKey, TValue>を通じて多くのC#プログラムで自然に使われている基本機能です。
2. C#の<T>とは?Tの意味と読み方をわかりやすく解説
2-1. <T>は「型パラメーター」を表す
C#の<T>は、型パラメーターを表します。
型パラメーターとは、あとから具体的な型を入れるための仮の名前です。
C#public class Box<T>
{
public T Value { get; set; }
}
このコードでは、Tが型パラメーターです。
Box<int>として使えば、Tはintになります。
C#Box<int> box = new Box<int>();
box.Value = 123;
Box<string>として使えば、Tはstringになります。
C#Box<string> box = new Box<string>();
box.Value = "Hello";
つまりTは、クラスやメソッドの中で「まだ決まっていない型」を表すための記号です。
2-2. Tにはint・string・自作クラスなどを指定できる
Tには、C#のさまざまな型を指定できます。
たとえば、値型であるintを指定できます。
C#Box<int> numberBox = new Box<int>();
numberBox.Value = 10;
参照型であるstringも指定できます。
C#Box<string> textBox = new Box<string>();
textBox.Value = "C#";
自作クラスも指定できます。
C#public class User
{
public string Name { get; set; }
}
Box<User> userBox = new Box<User>();
userBox.Value = new User { Name = "山田" };
Console.WriteLine(userBox.Value.Name);
このように、ジェネリックのTには、int、string、DateTime、自作クラス、構造体、インターフェイスを実装したクラスなど、さまざまな型を指定できます。
2-3. Tという名前に決まりはある?T以外の名前も使える
Tという名前は慣習的によく使われますが、必ずTでなければならないわけではありません。
たとえば、次のように書くこともできます。
C#public class Box<TItem>
{
public TItem Value { get; set; }
}
この場合、型パラメーター名はTItemです。
C#Box<string> box = new Box<string>();
box.Value = "Hello";
動作はTを使った場合と同じです。
一般的には、型パラメーターが1つだけならT、意味を明確にしたい場合はTItem、TEntity、TResultのような名前が使われます。
2-4. TKey・TValue・TResultなど複数の型パラメーターの意味
ジェネリックでは、型パラメーターを複数指定できます。
代表例がDictionary<TKey, TValue>です。
C#Dictionary<string, int> ages = new Dictionary<string, int>();
ages["Taro"] = 25;
この場合、TKeyはキーの型、TValueは値の型を表します。
他にも、戻り値の型を表すTResult、要素の型を表すTItem、エンティティの型を表すTEntityなどがよく使われます。
C#public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
}
使うときは、次のように型を2つ指定します。
C#Pair<string, int> pair = new Pair<string, int>();
pair.First = "Score";
pair.Second = 100;
型パラメーター名は自由ですが、コードを読む人が意味を理解しやすい名前にすることが大切です。
3. ジェネリックを使わないコードと使うコードの違い
3-1. 型ごとに同じ処理を書く場合の問題点
まず、ジェネリックを使わない場合を見てみましょう。
値をそのまま返す処理を、int用とstring用に分けて書くと次のようになります。
C#public static int EchoInt(int value)
{
return value;
}
public static string EchoString(string value)
{
return value;
}
処理内容は同じなのに、型が違うだけでメソッドを分けています。
さらにdouble、bool、DateTimeにも対応したい場合、同じようなメソッドがどんどん増えてしまいます。
C#public static double EchoDouble(double value)
{
return value;
}
public static bool EchoBool(bool value)
{
return value;
}
このようなコードは、修正や管理が大変になります。
3-2. object型で共通化した場合の問題点
型ごとの重複を避けるために、object型を使って共通化する方法もあります。
C#public static object Echo(object value)
{
return value;
}
objectはC#のほぼすべての型の基底型なので、intやstringを渡せます。
C#object result1 = Echo(100);
object result2 = Echo("Hello");
しかし、objectで返ってくるため、元の型として使うにはキャストが必要です。
C#int number = (int)Echo(100);
string text = (string)Echo("Hello");
また、間違ったキャストを書いてもコンパイル時には気づきにくく、実行時エラーになる可能性があります。
C#object result = Echo(100);
// 実行時エラーになる
string text = (string)result;
object型による共通化は便利に見えますが、型安全性が下がる点に注意が必要です。
3-3. ジェネリックで型安全に共通化するメリット
ジェネリックを使うと、型ごとの重複を減らしながら、型安全に共通化できます。
C#public static T Echo<T>(T value)
{
return value;
}
このメソッドは、渡された型と同じ型で値を返します。
C#int number = Echo<int>(100);
string text = Echo<string>("Hello");
Echo<int>なら戻り値はint、Echo<string>なら戻り値はstringです。
そのため、objectのように毎回キャストする必要がありません。
さらに、型の不一致があればコンパイル時に検出できます。
3-4. サンプルコードで比較:int版・string版・ジェネリック版
ここで、int版、string版、ジェネリック版を比較してみましょう。
C#public static int GetFirstInt(int a, int b)
{
return a;
}
public static string GetFirstString(string a, string b)
{
return a;
}
public static T GetFirst<T>(T a, T b)
{
return a;
}
使い方は次のとおりです。
C#int number = GetFirst<int>(10, 20);
string text = GetFirst<string>("A", "B");
Console.WriteLine(number); // 10
Console.WriteLine(text); // A
さらに、C#では型推論が働くため、型指定を省略できる場合があります。
C#int number = GetFirst(10, 20);
string text = GetFirst("A", "B");
このように、ジェネリックを使うと、処理の共通化と型安全性を両立できます。
4. C#ジェネリックメソッドの基本的な使い方
4-1. ジェネリックメソッドの基本構文
ジェネリックメソッドは、メソッド名の後ろに<T>を付けて定義します。
C#戻り値の型 メソッド名<T>(引数)
{
// 処理
}
具体例は次のとおりです。
C#public static T ReturnValue<T>(T value)
{
return value;
}
このメソッドでは、引数の型も戻り値の型もTです。
使うときにTへ具体的な型が入ります。
C#int number = ReturnValue<int>(10);
string text = ReturnValue<string>("Hello");
4-2. 引数と戻り値にTを使うサンプルコード
ジェネリックメソッドでは、引数や戻り値にTを使えます。
C#public static T GetLast<T>(T first, T second)
{
return second;
}
使い方は次のとおりです。
C#int result1 = GetLast<int>(1, 2);
string result2 = GetLast<string>("A", "B");
Console.WriteLine(result1); // 2
Console.WriteLine(result2); // B
Tはメソッド内でも変数の型として使えます。
C#public static void PrintValue<T>(T value)
{
T temp = value;
Console.WriteLine(temp);
}
C#PrintValue<int>(123);
PrintValue<string>("C#ジェネリック");
4-3. 呼び出し時に型を明示する場合と省略できる場合
ジェネリックメソッドを呼び出すときは、型を明示できます。
C#int number = ReturnValue<int>(100);
一方で、引数から型を推測できる場合は、型指定を省略できます。
C#int number = ReturnValue(100);
string text = ReturnValue("Hello");
このように、C#コンパイラが100を見て「これはintだ」と判断できるため、<int>を書かなくても動作します。
ただし、型推論ができない場合は、型を明示する必要があります。
C#public static T CreateDefault<T>()
{
return default(T);
}
int number = CreateDefault<int>();
string text = CreateDefault<string>();
このメソッドは引数がないため、コンパイラがTを推測できません。そのため、CreateDefault<int>()のように型を指定します。
4-4. ジェネリックメソッドでよくあるコンパイルエラー
初心者がよく遭遇するのが、Tに対して存在しないメンバーを呼び出そうとするエラーです。
C#public static void PrintName<T>(T value)
{
// Console.WriteLine(value.Name); // コンパイルエラー
}
Tにはintが入るかもしれませんし、stringが入るかもしれません。
すべての型にNameプロパティがあるわけではないため、C#はこのコードを許可しません。
このような場合は、後で説明するwhere制約を使って、Tに指定できる型を制限します。
C#public interface IHasName
{
string Name { get; }
}
public static void PrintName<T>(T value) where T : IHasName
{
Console.WriteLine(value.Name);
}
where T : IHasNameと書くことで、TはIHasNameを実装している型に限定されます。そのため、Nameプロパティを安全に呼び出せます。
5. C#ジェネリッククラスの基本的な使い方
5-1. ジェネリッククラスの基本構文
ジェネリッククラスは、クラス名の後ろに<T>を付けて定義します。
C#public class クラス名<T>
{
// Tを使ったフィールド、プロパティ、メソッド
}
具体例は次のとおりです。
C#public class Container<T>
{
public T Item { get; set; }
}
このContainer<T>は、使うときに型を指定します。
C#Container<int> intContainer = new Container<int>();
Container<string> stringContainer = new Container<string>();
Container<int>ではItemはint型になり、Container<string>ではItemはstring型になります。
5-2. フィールド・プロパティ・メソッドでTを使う方法
ジェネリッククラスでは、Tをフィールド、プロパティ、メソッドの引数、戻り値などに使えます。
C#public class Storage<T>
{
private T _value;
public T Value
{
get { return _value; }
set { _value = value; }
}
public void SetValue(T value)
{
_value = value;
}
public T GetValue()
{
return _value;
}
}
使い方は次のとおりです。
C#Storage<string> storage = new Storage<string>();
storage.SetValue("Hello");
string value = storage.GetValue();
Console.WriteLine(value);
この例では、Storage<string>として使っているため、Tはstringとして扱われます。
5-3. 自作のBox<T>クラスを作るサンプルコード
初心者向けのわかりやすい例として、値を1つだけ入れられるBox<T>クラスを作ってみましょう。
C#public class Box<T>
{
public T Value { get; set; }
public Box(T value)
{
Value = value;
}
public void Print()
{
Console.WriteLine(Value);
}
}
このBox<T>は、どんな型の値でも入れられる箱のようなクラスです。
C#Box<int> numberBox = new Box<int>(100);
numberBox.Print(); // 100
Box<string> textBox = new Box<string>("C#");
textBox.Print(); // C#
Box<int>ならValueはint型、Box<string>ならValueはstring型です。
同じBox<T>クラスを使いながら、型に応じた安全なコードを書けます。
5-4. ジェネリッククラスをインスタンス化する方法
ジェネリッククラスをインスタンス化するときは、クラス名の後ろに具体的な型を指定します。
C#Box<int> box = new Box<int>(10);
C#のバージョンによっては、右辺の型指定を省略して次のように書けます。
C#Box<int> box = new(10);
ただし、初心者のうちは次のように左右両方に型を書いたほうが理解しやすいです。
C#Box<string> box = new Box<string>("Hello");
複数の型パラメーターを持つクラスの場合は、複数の型を指定します。
C#public class Pair<TKey, TValue>
{
public TKey Key { get; set; }
public TValue Value { get; set; }
}
C#Pair<string, int> score = new Pair<string, int>();
score.Key = "Alice";
score.Value = 90;
5-5. ジェネリッククラスとジェネリックメソッドの違い
ジェネリッククラスとジェネリックメソッドの違いは、型パラメーターをどこで使うかです。
ジェネリッククラスは、クラス全体でTを使います。
C#public class Box<T>
{
public T Value { get; set; }
}
一方、ジェネリックメソッドは、メソッド単位でTを使います。
C#public static T Echo<T>(T value)
{
return value;
}
クラス全体で同じ型を扱いたい場合はジェネリッククラス、特定の処理だけを型に依存させたい場合はジェネリックメソッドを使うと考えるとわかりやすいです。
たとえば、値を保持する箱を作りたいならジェネリッククラスが向いています。
C#Box<int> box = new Box<int>();
単に値を受け取って返すような汎用処理なら、ジェネリックメソッドが向いています。
C#int value = Echo(10);
6. where制約とは?Tに使える型を制限する方法
6-1. where制約が必要になる理由
ジェネリックのTには、基本的にさまざまな型を指定できます。
しかし、そのままだとTに対して呼び出せるメンバーは限られます。
C#public static void PrintLength<T>(T value)
{
// Console.WriteLine(value.Length); // コンパイルエラー
}
Tにstringが入るならLengthを使えますが、intにはLengthがありません。
C#コンパイラは、Tがどんな型になるかわからないため、このコードを許可しません。
そこで使うのがwhere制約です。
where制約を使うと、Tに指定できる型を制限できます。
C#public static void PrintName<T>(T value) where T : IHasName
{
Console.WriteLine(value.Name);
}
このように制約を付けることで、Tに対して安全にメンバーを呼び出せるようになります。
6-2. where T : classの意味と使い方
where T : classは、Tを参照型に制限する制約です。
C#public class Repository<T> where T : class
{
public void Save(T entity)
{
Console.WriteLine("保存しました");
}
}
この場合、Tにはクラスなどの参照型を指定できます。
C#public class User
{
public string Name { get; set; }
}
Repository<User> repository = new Repository<User>();
repository.Save(new User { Name = "山田" });
一方、intのような値型は指定できません。
C#// Repository<int> repository = new Repository<int>(); // コンパイルエラー
where T : classは、エンティティクラスやDTOなど、参照型だけを扱いたい場合によく使います。
6-3. where T : structの意味と使い方
where T : structは、Tを値型に制限する制約です。
C#public static void PrintDefault<T>() where T : struct
{
T value = default(T);
Console.WriteLine(value);
}
この場合、int、double、DateTime、boolなどの値型を指定できます。
C#PrintDefault<int>();
PrintDefault<DateTime>();
一方、stringや自作クラスなどの参照型は指定できません。
C#// PrintDefault<string>(); // コンパイルエラー
値型だけを対象にした処理を書きたい場合に、where T : structを使います。
6-4. where T : new()の意味と使い方
where T : new()は、Tが引数なしコンストラクターを持つ型であることを表す制約です。
この制約を付けると、メソッドやクラスの中でnew T()を書けるようになります。
C#public static T CreateInstance<T>() where T : new()
{
return new T();
}
使い方は次のとおりです。
C#public class User
{
public string Name { get; set; }
}
User user = CreateInstance<User>();
new()制約がない場合、new T()は書けません。
C#public static T Create<T>()
{
// return new T(); // コンパイルエラー
return default(T);
}
where T : new()を使うことで、C#コンパイラに「この型は引数なしでインスタンス化できる」と伝えられます。
6-5. where T : インターフェイスの意味と使い方
where T : インターフェイス名と書くと、Tをそのインターフェイスを実装した型に制限できます。
C#public interface IPrintable
{
void Print();
}
C#public class Report : IPrintable
{
public void Print()
{
Console.WriteLine("レポートを印刷します");
}
}
このインターフェイスを制約に使います。
C#public static void ExecutePrint<T>(T item) where T : IPrintable
{
item.Print();
}
Tは必ずIPrintableを実装しているため、メソッド内でPrint()を安全に呼び出せます。
C#Report report = new Report();
ExecutePrint(report);
インターフェイス制約は、ジェネリックで共通処理を書くときに非常によく使われます。
6-6. where制約を複数組み合わせるときの書き方
where制約は複数組み合わせることができます。
たとえば、参照型であり、IPrintableを実装していて、引数なしコンストラクターを持つ型に制限する場合は次のように書きます。
C#public static T CreateAndPrint<T>()
where T : class, IPrintable, new()
{
T item = new T();
item.Print();
return item;
}
使い方は次のとおりです。
C#public class Report : IPrintable
{
public void Print()
{
Console.WriteLine("印刷します");
}
}
Report report = CreateAndPrint<Report>();
複数制約を書く場合、new()制約は最後に書く必要があります。
C#where T : class, IPrintable, new()
次のようにnew()を途中に書くとエラーになります。
C#// where T : class, new(), IPrintable // コンパイルエラー
6-7. where制約でできること・できないこと
where制約を使うと、Tに指定できる型を制限できます。
できることの例は次のとおりです。
C#where T : class
where T : struct
where T : new()
where T : IPrintable
where T : BaseEntity
BaseEntityのような基底クラスを制約にすることもできます。
C#public class BaseEntity
{
public int Id { get; set; }
}
public static void PrintId<T>(T entity) where T : BaseEntity
{
Console.WriteLine(entity.Id);
}
一方で、where制約で何でも自由に指定できるわけではありません。
たとえば、「Tは必ずNameというプロパティを持つ型」という制約は、直接は書けません。
C#// where T : Nameプロパティを持つ型
このような場合は、Nameプロパティを持つインターフェイスを作り、そのインターフェイスで制約します。
C#public interface IHasName
{
string Name { get; }
}
public static void PrintName<T>(T item) where T : IHasName
{
Console.WriteLine(item.Name);
}
where制約は、ジェネリックを安全に使うための重要な仕組みです。
7. C#ジェネリックの実践サンプルコード
7-1. 汎用的な値の入れ替えメソッドを作る
ジェネリックの定番サンプルとして、値を入れ替えるSwapメソッドがあります。
C#public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
使い方は次のとおりです。
C#int x = 10;
int y = 20;
Swap(ref x, ref y);
Console.WriteLine(x); // 20
Console.WriteLine(y); // 10
stringでも使えます。
C#string first = "A";
string second = "B";
Swap(ref first, ref second);
Console.WriteLine(first); // B
Console.WriteLine(second); // A
型に関係なく同じ処理を使えるのが、C#ジェネリックの強みです。
7-2. 共通のリポジトリクラスをジェネリックで作る
実務では、データを扱うリポジトリクラスにジェネリックを使うことがあります。
まず、共通の基底クラスを用意します。
C#public abstract class Entity
{
public int Id { get; set; }
}
Userクラスを作ります。
C#public class User : Entity
{
public string Name { get; set; }
}
ジェネリックリポジトリを作ります。
C#public class Repository<T> where T : Entity
{
private readonly List<T> _items = new List<T>();
public void Add(T item)
{
_items.Add(item);
}
public T FindById(int id)
{
return _items.FirstOrDefault(item => item.Id == id);
}
}
使い方は次のとおりです。
C#Repository<User> userRepository = new Repository<User>();
userRepository.Add(new User { Id = 1, Name = "田中" });
User user = userRepository.FindById(1);
Console.WriteLine(user.Name);
where T : Entityを付けているため、Tには必ずIdプロパティがあります。そのため、FindByIdメソッド内でitem.Idを安全に使えます。
7-3. インターフェイス制約を使って共通処理を呼び出す
インターフェイス制約を使うと、共通のメソッドを持つ型だけを対象にできます。
C#public interface IValidatable
{
bool IsValid();
}
このインターフェイスを実装するクラスを作ります。
C#public class UserInput : IValidatable
{
public string Name { get; set; }
public bool IsValid()
{
return !string.IsNullOrEmpty(Name);
}
}
ジェネリックメソッドで制約を付けます。
C#public static void ValidateAndPrint<T>(T item) where T : IValidatable
{
if (item.IsValid())
{
Console.WriteLine("有効なデータです");
}
else
{
Console.WriteLine("無効なデータです");
}
}
使い方は次のとおりです。
C#UserInput input = new UserInput { Name = "佐藤" };
ValidateAndPrint(input);
where T : IValidatableによって、Tに対してIsValid()を呼び出せます。
7-4. new()制約を使ってTのインスタンスを生成する
new()制約を使うと、ジェネリックの中でTのインスタンスを作れます。
C#public static T Create<T>() where T : new()
{
return new T();
}
自作クラスで試してみます。
C#public class Product
{
public string Name { get; set; }
}
C#Product product = Create<Product>();
product.Name = "ノートPC";
Console.WriteLine(product.Name);
複数のインスタンスを作るメソッドも作れます。
C#public static List<T> CreateList<T>(int count) where T : new()
{
List<T> list = new List<T>();
for (int i = 0; i < count; i++)
{
list.Add(new T());
}
return list;
}
C#List<Product> products = CreateList<Product>(3);
Console.WriteLine(products.Count); // 3
ただし、new()制約で呼び出せるのは引数なしコンストラクターだけです。引数ありコンストラクターは直接呼び出せません。
7-5. List<T>を使った実務でよくある処理例
List<T>は、C#ジェネリックの中でも特によく使われます。
たとえば、ユーザー一覧を扱う処理です。
C#public class User
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
C#List<User> users = new List<User>
{
new User { Id = 1, Name = "田中", Age = 25 },
new User { Id = 2, Name = "佐藤", Age = 30 },
new User { Id = 3, Name = "鈴木", Age = 20 }
};
foreachで一覧表示できます。
C#foreach (User user in users)
{
Console.WriteLine($"{user.Name}:{user.Age}歳");
}
条件に合うデータを取得する場合は、LINQを使うことも多いです。
C#List<User> adults = users
.Where(user => user.Age >= 20)
.ToList();
特定のIDのユーザーを探す場合は、次のように書けます。
C#User target = users.FirstOrDefault(user => user.Id == 2);
if (target != null)
{
Console.WriteLine(target.Name);
}
List<T>を使うと、Tに指定した型のリストとして安全に扱えます。
8. C#ジェネリックのメリットとデメリット
8-1. メリット1:同じ処理を型ごとに書かなくて済む
C#ジェネリックの大きなメリットは、同じ処理を型ごとに何度も書かなくて済むことです。
たとえば、次のようなメソッドを型ごとに用意する必要がなくなります。
C#public static int GetValue(int value)
{
return value;
}
public static string GetValue(string value)
{
return value;
}
ジェネリックを使えば、1つのメソッドで済みます。
C#public static T GetValue<T>(T value)
{
return value;
}
型が増えてもメソッドを増やす必要がないため、保守しやすいコードになります。
8-2. メリット2:コンパイル時に型チェックできる
ジェネリックは、コンパイル時に型チェックされます。
C#List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
// numbers.Add("3"); // コンパイルエラー
List<int>にはintしか入れられません。
間違った型を入れようとすると、実行前にエラーとして検出されます。
これは、object型で何でも入れられるリストより安全です。
C#List<object> values = new List<object>();
values.Add(1);
values.Add("文字列");
values.Add(DateTime.Now);
List<object>は自由度が高い反面、取り出すときに型を意識する必要があります。
ジェネリックを使うと、型のミスを早い段階で見つけやすくなります。
8-3. メリット3:キャストやボックス化を減らせる
ジェネリックを使うと、キャストを減らせます。
C#List<string> names = new List<string>();
names.Add("田中");
string name = names[0]; // キャスト不要
object型を使う場合は、取り出すときにキャストが必要です。
C#List<object> names = new List<object>();
names.Add("田中");
string name = (string)names[0];
また、intなどの値型をobjectとして扱うと、ボックス化が発生することがあります。
ジェネリックを使えば、値型もその型のまま扱えるため、不要なボックス化を減らせます。
C#List<int> numbers = new List<int>();
numbers.Add(10);
int number = numbers[0];
実務では、パフォーマンスや可読性の面でもジェネリックは重要です。
8-4. デメリット1:初心者には構文がわかりにくい
C#ジェネリックは便利ですが、初心者には構文が難しく見えることがあります。
C#Dictionary<string, List<int>> data = new Dictionary<string, List<int>>();
<T>や<TKey, TValue>、さらに入れ子になった型を見ると、最初は読みづらく感じるかもしれません。
ただし、基本は「<>の中に型を指定している」と考えれば大丈夫です。
C#List<string> // stringのリスト
List<int> // intのリスト
Dictionary<string, int> // stringをキー、intを値にする辞書
慣れるまでは、よく使うList<T>から理解していくのがおすすめです。
8-5. デメリット2:where制約がないと使えるメンバーが限られる
ジェネリックでは、Tがどんな型かわからないため、そのままだと特定のプロパティやメソッドを呼び出せません。
C#public static void PrintId<T>(T item)
{
// Console.WriteLine(item.Id); // コンパイルエラー
}
Tに必ずIdがあるとは限らないためです。
この場合は、基底クラスやインターフェイスを使って制約を付けます。
C#public interface IHasId
{
int Id { get; }
}
public static void PrintId<T>(T item) where T : IHasId
{
Console.WriteLine(item.Id);
}
where制約を理解しないままジェネリックを使うと、「なぜメンバーを呼び出せないのか」でつまずきやすくなります。
8-6. デメリット3:複雑にしすぎると可読性が下がる
ジェネリックは便利ですが、使いすぎるとコードが読みにくくなることがあります。
C#public class Service<TRepository, TEntity, TKey, TResult>
{
}
型パラメーターが多すぎると、何を表しているのか理解しづらくなります。
また、where制約が複雑に重なると、初心者だけでなく経験者にとっても読みづらいコードになります。
C#public class Service<T>
where T : class, IHasId, IPrintable, new()
{
}
ジェネリックは、共通化のための便利な手段です。
しかし、何でもジェネリックにすればよいわけではありません。単純なコードで十分な場合は、無理にジェネリック化しないことも大切です。
9. 初心者がつまずきやすいC#ジェネリックの注意点
9-1. Tの型が決まるタイミングを誤解しやすい
Tは、クラスやメソッドを定義した時点では具体的な型ではありません。
使うときに型が決まります。
C#public class Box<T>
{
public T Value { get; set; }
}
この時点では、Tがintなのかstringなのかは決まっていません。
C#Box<int> intBox = new Box<int>();
このようにBox<int>として使ったとき、Tはintになります。
C#Box<string> stringBox = new Box<string>();
Box<string>として使ったとき、Tはstringになります。
初心者は「Tという特別な型がある」と考えてしまうことがありますが、Tは型そのものではなく、型を入れるための仮の名前です。
9-2. Tに対して何でも呼び出せるわけではない
Tにはさまざまな型が入る可能性があります。
そのため、制約がない状態では、特定の型にしか存在しないメンバーを呼び出せません。
C#public static void Show<T>(T value)
{
Console.WriteLine(value.ToString());
}
ToString()はすべての型が持っているため呼び出せます。
一方、次のコードはエラーになります。
C#public static void ShowName<T>(T value)
{
// Console.WriteLine(value.Name); // コンパイルエラー
}
Nameプロパティがあるかどうかは、Tだけでは判断できないためです。
この場合は、インターフェイスや基底クラスの制約を使います。
C#public interface IHasName
{
string Name { get; }
}
public static void ShowName<T>(T value) where T : IHasName
{
Console.WriteLine(value.Name);
}
9-3. nullを扱うときは参照型・値型の違いに注意する
ジェネリックでnullを扱うときは、参照型と値型の違いに注意が必要です。
参照型はnullを持てます。
C#string text = null;
一方、通常のintなどの値型はnullを持てません。
C#// int number = null; // コンパイルエラー
ジェネリックでは、Tが参照型なのか値型なのかがわからない場合があります。
C#public static T GetDefault<T>()
{
return default(T);
}
Tがintならdefault(T)は0です。
C#int number = GetDefault<int>(); // 0
Tがstringならdefault(T)はnullです。
C#string text = GetDefault<string>(); // null
このように、Tに入る型によってdefault(T)の値が変わる点に注意しましょう。
9-4. where T : new()では引数ありコンストラクターを呼べない
where T : new()を付けると、new T()を使えます。
C#public static T Create<T>() where T : new()
{
return new T();
}
ただし、呼び出せるのは引数なしコンストラクターだけです。
次のように引数ありコンストラクターを呼ぶことはできません。
C#public static T Create<T>(string name) where T : new()
{
// return new T(name); // コンパイルエラー
return new T();
}
たとえば、次のようなクラスがあるとします。
C#public class User
{
public string Name { get; }
public User(string name)
{
Name = name;
}
}
このクラスには引数なしコンストラクターがないため、where T : new()の対象にできません。
C#// User user = Create<User>(); // コンパイルエラー
new()制約は便利ですが、万能ではないことを覚えておきましょう。
9-5. ジェネリックを使うべき場面と使わないほうがよい場面
ジェネリックを使うべき場面は、複数の型に対して同じ処理を安全に使い回したい場合です。
たとえば、次のような場面ではジェネリックが向いています。
C#public static T GetFirst<T>(List<T> items)
{
return items[0];
}
List<int>でもList<string>でも使えます。
C#int number = GetFirst(new List<int> { 1, 2, 3 });
string text = GetFirst(new List<string> { "A", "B", "C" });
一方、特定の型だけで使う処理なら、無理にジェネリックにする必要はありません。
C#public static int Add(int a, int b)
{
return a + b;
}
このように、int専用の足し算で十分なら、ジェネリックにするとかえって読みにくくなる場合があります。
ジェネリックは、共通化したい理由が明確なときに使うのが基本です。
10. C#ジェネリックに関するよくある質問
10-1. C#の<T>は何と読みますか?
<T>は、「ティー」または「型パラメーターのT」と読むことが多いです。
たとえば、List<T>は「リスト・オブ・ティー」や「T型のリスト」のように説明されることがあります。
厳密な読み方よりも、Tが「あとから指定する型」を表していると理解することが大切です。
10-2. Tにはどんな型を指定できますか?
Tには、int、double、bool、DateTimeのような値型、stringや自作クラスのような参照型を指定できます。
C#Box<int> intBox = new Box<int>();
Box<string> stringBox = new Box<string>();
Box<User> userBox = new Box<User>();
ただし、where制約が付いている場合は、その条件を満たす型しか指定できません。
C#public class Repository<T> where T : class
{
}
この場合、Tには参照型だけを指定できます。
10-3. ジェネリックとobject型の違いは何ですか?
object型はさまざまな型を扱えますが、取り出すときにキャストが必要になることがあります。
C#object value = "Hello";
string text = (string)value;
一方、ジェネリックは指定した型のまま扱えます。
C#List<string> names = new List<string>();
names.Add("田中");
string name = names[0];
ジェネリックは、型安全に共通化できる点が大きな違いです。
object型は柔軟ですが、型のミスに気づきにくくなる場合があります。通常は、型が決まっているならジェネリックを使うほうが安全です。
10-4. ジェネリックと継承・インターフェイスの違いは何ですか?
ジェネリックは、型を後から指定して処理を共通化する仕組みです。
C#public class Box<T>
{
public T Value { get; set; }
}
継承は、あるクラスの性質を別のクラスに引き継ぐ仕組みです。
C#public class Animal
{
public void Eat()
{
Console.WriteLine("食べます");
}
}
public class Dog : Animal
{
}
インターフェイスは、クラスが持つべき機能の約束を定義する仕組みです。
C#public interface IPrintable
{
void Print();
}
実務では、ジェネリックとインターフェイスを組み合わせることもよくあります。
C#public static void Print<T>(T item) where T : IPrintable
{
item.Print();
}
ジェネリック、継承、インターフェイスは役割が違いますが、組み合わせることで柔軟で安全なコードを書けます。
10-5. where T : classとwhere T : structの違いは何ですか?
where T : classは、Tを参照型に制限します。
C#public class Service<T> where T : class
{
}
この場合、stringや自作クラスなどを指定できます。
C#Service<string> service1 = new Service<string>();
Service<User> service2 = new Service<User>();
where T : structは、Tを値型に制限します。
C#public class ValueService<T> where T : struct
{
}
この場合、int、double、DateTimeなどを指定できます。
C#ValueService<int> service1 = new ValueService<int>();
ValueService<DateTime> service2 = new ValueService<DateTime>();
簡単にいうと、classは参照型、structは値型を指定するための制約です。
10-6. 初心者はまずどこまで覚えればよいですか?
初心者は、まず次の内容を理解すれば十分です。
List<T>のTは、リストに入れる要素の型を表します。
C#List<int> numbers = new List<int>();
List<string> names = new List<string>();
<T>は、あとから型を指定するための仕組みです。
C#public class Box<T>
{
public T Value { get; set; }
}
ジェネリックメソッドでは、引数や戻り値にTを使えます。
C#public static T Echo<T>(T value)
{
return value;
}
where制約を使うと、Tに指定できる型を制限できます。
C#public static void Print<T>(T item) where T : IPrintable
{
item.Print();
}
最初からすべてを完璧に覚える必要はありません。まずはList<T>、次に自作のジェネリックメソッド、最後にwhere制約という順番で理解していくと、C#ジェネリックを無理なく学べます。
まとめ
C#ジェネリックとは、型を後から指定できる仕組みです。
<T>のTは型パラメーターであり、int、string、自作クラスなど、さまざまな型を指定できます。
C#public class Box<T>
{
public T Value { get; set; }
}
C#Box<int> intBox = new Box<int>();
Box<string> stringBox = new Box<string>();
ジェネリックを使うと、型ごとに同じ処理を書く必要がなくなり、コードの重複を減らせます。
また、object型を使った共通化と違い、コンパイル時に型チェックできるため、型安全なコードを書けます。
ジェネリックメソッドでは、メソッド単位で型を後から指定できます。
C#public static T Echo<T>(T value)
{
return value;
}
ジェネリッククラスでは、クラス全体で型パラメーターを使えます。
C#public class Storage<T>
{
public T Value { get; set; }
}
さらに、where制約を使うと、Tに指定できる型を制限できます。
C#public static T Create<T>() where T : new()
{
return new T();
}
C#ジェネリックは、最初は難しく見えますが、基本は「型を後から決める仕組み」です。
まずはList<T>やDictionary<TKey, TValue>のような標準ライブラリの使い方から慣れ、次に自分でジェネリックメソッドやジェネリッククラスを作ってみると理解しやすくなります。
ジェネリックを使いこなせるようになると、C#でより安全で再利用しやすいコードを書けるようになります。

