C#ジェネリックメソッド入門:書き方・使い方・型制約をサンプルで徹底解説
はじめに
C#で同じような処理を複数の型に対して書いていると、「int用」「string用」「DateTime用」のように、型が違うだけで中身がほとんど同じメソッドが増えてしまうことがあります。
そのような場面で役立つのがジェネリックメソッドです。
ジェネリックメソッドを使うと、型を固定せずにメソッドを定義できるため、複数の型に対応した再利用しやすい処理を書けます。さらに、object型を使った実装と違い、型安全性を保ちやすい点も大きなメリットです。
この記事では、C# ジェネリックメソッドの基本的な書き方から、使い方、型制約 where、よくあるエラー、実務での設計ポイントまで、サンプルコードを使ってわかりやすく解説します。
1. C#のジェネリックメソッドとは?まず全体像を理解しよう
1-1. ジェネリックメソッドの基本概念
ジェネリックメソッドとは、型を後から指定できるメソッドのことです。
通常のメソッドでは、引数や戻り値の型をあらかじめ決めておきます。
C#static void PrintString(string value)
{
Console.WriteLine(value);
}
このメソッドは string 型の値しか受け取れません。
一方、ジェネリックメソッドでは、型を T のような型パラメーターで表します。
C#static void Print<T>(T value)
{
Console.WriteLine(value);
}
この Print<T> メソッドは、string、int、DateTime など、さまざまな型の値を受け取れます。
C#Print<string>("Hello");
Print<int>(100);
Print<DateTime>(DateTime.Now);
つまり、ジェネリックメソッドは「型を固定せず、呼び出し時に決められるメソッド」です。
1-2. 通常のメソッドとの違い
通常のメソッドは、引数や戻り値の型が固定されています。
C#static int GetValue(int value)
{
return value;
}
この場合、int 以外の型を扱いたいときは、別のメソッドを用意する必要があります。
C#static string GetValue(string value)
{
return value;
}
しかし、ジェネリックメソッドを使えば、型だけが違うメソッドを1つにまとめられます。
C#static T GetValue<T>(T value)
{
return value;
}
呼び出す側では、次のように複数の型で同じメソッドを使えます。
C#int number = GetValue<int>(10);
string text = GetValue<string>("C#");
通常のメソッドは「特定の型専用」、ジェネリックメソッドは「複数の型に対応できるメソッド」と考えると理解しやすいです。
1-3. ジェネリッククラスとの違い
C#には、ジェネリックメソッドのほかにジェネリッククラスもあります。
ジェネリッククラスは、クラス全体で型パラメーターを使います。
C#class Box<T>
{
public T Value { get; set; }
}
この場合、Box<T> のインスタンスを作るときに型を指定します。
C#Box<int> intBox = new Box<int>();
intBox.Value = 10;
Box<string> stringBox = new Box<string>();
stringBox.Value = "Hello";
一方、ジェネリックメソッドは、メソッド単位で型パラメーターを使います。
C#class Utility
{
public static void Show<T>(T value)
{
Console.WriteLine(value);
}
}
ジェネリッククラスは「クラス全体で型を扱いたいとき」、ジェネリックメソッドは「特定のメソッドだけを汎用化したいとき」に使います。
1-4. ジェネリックメソッドを使うメリット
C#でジェネリックメソッドを使う主なメリットは、次のとおりです。
1つ目は、重複コードを減らせることです。型が違うだけで同じ処理をしているメソッドを、1つのメソッドにまとめられます。
2つ目は、型安全に処理を書けることです。object型を使う場合と違い、呼び出し元で型が明確になるため、不要なキャストや実行時エラーを減らせます。
3つ目は、再利用しやすいユーティリティメソッドを作れることです。値の表示、配列の検索、リストの変換、インスタンス生成など、型に依存しない処理を共通化できます。
4つ目は、可読性が上がることです。適切に使えば、「このメソッドは型に依存しない共通処理である」とコードから読み取りやすくなります。
2. C#ジェネリックメソッドの基本的な書き方
2-1. 基本構文:メソッド名<T>()の意味
ジェネリックメソッドの基本構文は次のようになります。
C#戻り値の型 メソッド名<T>(引数)
{
// 処理
}
たとえば、値をそのまま返すメソッドは次のように書けます。
C#static T Echo<T>(T value)
{
return value;
}
ここで重要なのは、メソッド名の後ろにある <T> です。
C#Echo<T>
この <T> は、「このメソッドでは T という型パラメーターを使います」という宣言です。
T は実際の型ではなく、呼び出し時に int や string などに置き換わる仮の型名です。
C#int number = Echo<int>(123);
string text = Echo<string>("Hello");
このように、T を使うことで、1つのメソッドを複数の型で使えるようになります。
2-2. 型パラメーターTの役割
T は、ジェネリックメソッドで使う代表的な型パラメーター名です。
C#static void Show<T>(T value)
{
Console.WriteLine(value);
}
このコードでは、T が引数 value の型として使われています。
呼び出し時に T は具体的な型になります。
C#Show<int>(10); // T は int
Show<string>("ABC"); // T は string
Show<bool>(true); // T は bool
T は「Type」の頭文字としてよく使われますが、必ず T という名前でなければならないわけではありません。
たとえば、次のように書くこともできます。
C#static void ShowValue<TValue>(TValue value)
{
Console.WriteLine(value);
}
ただし、型パラメーターが1つだけの場合は、慣習的に T がよく使われます。
2-3. 引数にTを使う書き方
ジェネリックメソッドでは、引数の型に T を指定できます。
C#static void Print<T>(T value)
{
Console.WriteLine(value);
}
このメソッドは、さまざまな型の値を受け取れます。
C#Print<int>(100);
Print<string>("C#");
Print<double>(3.14);
さらに、複数の引数に同じ T を指定することもできます。
C#static void PrintPair<T>(T first, T second)
{
Console.WriteLine($"{first}, {second}");
}
この場合、first と second は同じ型である必要があります。
C#PrintPair<int>(1, 2);
PrintPair<string>("A", "B");
同じ T を使うことで、「2つの引数は同じ型である」というルールをメソッド定義に表現できます。
2-4. 戻り値にTを使う書き方
戻り値の型にも T を使えます。
C#static T GetFirst<T>(T first, T second)
{
return first;
}
このメソッドは、受け取った2つの値のうち、最初の値を返します。
C#int number = GetFirst<int>(10, 20);
string text = GetFirst<string>("apple", "banana");
戻り値に T を使うと、呼び出し元では指定した型の値として受け取れます。
C#int result = GetFirst(10, 20);
この場合、T は int と推論されるため、戻り値も int になります。
2-5. 複数の型パラメーターを使う書き方
ジェネリックメソッドでは、型パラメーターを複数指定できます。
C#static void PrintKeyValue<TKey, TValue>(TKey key, TValue value)
{
Console.WriteLine($"{key}: {value}");
}
このメソッドでは、TKey と TValue という2つの型パラメーターを使っています。
C#PrintKeyValue<int, string>(1, "Apple");
PrintKeyValue<string, int>("Age", 30);
型推論を使えば、型指定を省略できます。
C#PrintKeyValue(1, "Apple");
PrintKeyValue("Age", 30);
複数の型パラメーターを使うと、キーと値、入力型と出力型など、異なる型を組み合わせた汎用的な処理を書けます。
3. ジェネリックメソッドの使い方をサンプルコードで解説
3-1. 文字列・数値など複数の型で同じ処理を使う例
まずは、値を表示するだけのシンプルな例です。
C#static void Display<T>(T value)
{
Console.WriteLine($"値: {value}");
}
このメソッドは、さまざまな型で呼び出せます。
C#Display<int>(100);
Display<string>("Hello");
Display<double>(12.34);
Display<bool>(true);
実行結果は次のようになります。
値: 100
値: Hello
値: 12.34
値: True
型ごとに DisplayInt、DisplayString、DisplayDouble のようなメソッドを作らなくても、1つのジェネリックメソッドで対応できます。
3-2. 配列やList<T>を扱う例
ジェネリックメソッドは、配列や List<T> と組み合わせると便利です。
次の例では、配列の中身をすべて表示します。
C#static void PrintArray<T>(T[] items)
{
foreach (T item in items)
{
Console.WriteLine(item);
}
}
呼び出し例です。
C#int[] numbers = { 1, 2, 3 };
string[] names = { "Alice", "Bob", "Charlie" };
PrintArray(numbers);
PrintArray(names);
List<T> を扱う場合も同じように書けます。
C#static void PrintList<T>(List<T> items)
{
foreach (T item in items)
{
Console.WriteLine(item);
}
}
呼び出し例です。
C#List<int> scores = new List<int> { 80, 90, 100 };
List<string> fruits = new List<string> { "Apple", "Banana", "Orange" };
PrintList(scores);
PrintList(fruits);
配列やリストに入っている要素の型が違っても、処理の流れが同じならジェネリックメソッドで共通化できます。
3-3. 引数と戻り値の型をそろえる例
ジェネリックメソッドでは、引数と戻り値に同じ T を使うことで、型をそろえられます。
C#static T SelectFirst<T>(T first, T second)
{
return first;
}
このメソッドは、2つの値のうち最初の値を返します。
C#int number = SelectFirst(10, 20);
string word = SelectFirst("C#", "Java");
number は int、word は string として扱われます。
このように、引数と戻り値に同じ型パラメーターを使うと、「入力と出力の型が同じである」という関係を明確にできます。
3-4. 型を明示して呼び出す方法
ジェネリックメソッドは、型を明示して呼び出せます。
C#static T ConvertValue<T>(object value)
{
return (T)value;
}
呼び出し時には、メソッド名の後ろに型を指定します。
C#object obj = "Hello";
string text = ConvertValue<string>(obj);
この場合、T は string になります。
型を明示する書き方は、型推論がうまく働かない場合や、戻り値の型をはっきり指定したい場合に使います。
C#int number = ConvertValue<int>(123);
ただし、無理なキャストをすると実行時エラーになるため注意が必要です。
C#object value = "ABC";
// 実行時エラーになる
int result = ConvertValue<int>(value);
このようなケースでは、型チェックや安全な変換処理を検討しましょう。
3-5. 型推論でTを省略して呼び出す方法
C#では、多くの場合、引数から型を推論できます。
C#static void Show<T>(T value)
{
Console.WriteLine(value);
}
このメソッドは、次のように型指定を省略して呼び出せます。
C#Show(100);
Show("Hello");
Show(3.14);
C#コンパイラが引数を見て、T を自動的に判断します。
C#Show(100); // T は int
Show("Hello"); // T は string
Show(3.14); // T は double
型推論を使うとコードが短くなり、読みやすくなります。
ただし、戻り値だけに T が使われていて、引数から型を推論できない場合は、型を明示する必要があります。
C#static T CreateDefault<T>()
{
return default;
}
int number = CreateDefault<int>();
string text = CreateDefault<string>();
このように、型推論が使える場合と使えない場合を理解しておくことが大切です。
4. ジェネリックメソッドはどんな場面で使うべきか
4-1. 型だけが違う重複コードを共通化したいとき
ジェネリックメソッドが特に役立つのは、型だけが違う重複コードを共通化したいときです。
たとえば、次のようなメソッドがあるとします。
C#static void PrintIntArray(int[] items)
{
foreach (int item in items)
{
Console.WriteLine(item);
}
}
static void PrintStringArray(string[] items)
{
foreach (string item in items)
{
Console.WriteLine(item);
}
}
どちらも、配列の中身を表示しているだけです。違うのは型だけです。
このような場合は、ジェネリックメソッドにまとめられます。
C#static void PrintArray<T>(T[] items)
{
foreach (T item in items)
{
Console.WriteLine(item);
}
}
呼び出し側は次のようになります。
C#PrintArray(new int[] { 1, 2, 3 });
PrintArray(new string[] { "A", "B", "C" });
重複コードを減らすことで、修正漏れやメンテナンスコストも減らせます。
4-2. 型安全に共通処理を書きたいとき
ジェネリックメソッドは、型安全に共通処理を書きたいときにも便利です。
たとえば、object型を使うと、どんな型でも受け取れます。
C#static object GetValue(object value)
{
return value;
}
しかし、呼び出し元ではキャストが必要になることがあります。
C#object result = GetValue("Hello");
string text = (string)result;
キャストを間違えると、実行時エラーになる可能性があります。
一方、ジェネリックメソッドなら、戻り値の型も保持できます。
C#static T GetValue<T>(T value)
{
return value;
}
呼び出し例です。
C#string text = GetValue("Hello");
int number = GetValue(100);
キャストが不要になり、コンパイル時に型の整合性を確認できます。
4-3. キャストやobject型を避けたいとき
object型を使うと、どんな値でも扱えるため一見便利です。
C#static void Show(object value)
{
Console.WriteLine(value);
}
ただし、object型では元の型情報を活かしにくく、戻り値を使う場合にはキャストが必要になりやすいです。
C#static object GetFirst(object first, object second)
{
return first;
}
string text = (string)GetFirst("A", "B");
このようなコードでは、キャストミスが実行時エラーにつながることがあります。
ジェネリックメソッドを使えば、型を保ったまま処理できます。
C#static T GetFirst<T>(T first, T second)
{
return first;
}
string text = GetFirst("A", "B");
型安全に書けるため、object型よりも安心して使えます。
4-4. ユーティリティメソッドを汎用化したいとき
ジェネリックメソッドは、ユーティリティクラスでよく使われます。
たとえば、配列の先頭要素を取得するメソッドを考えてみましょう。
C#static T FirstOrDefault<T>(T[] items)
{
if (items == null || items.Length == 0)
{
return default;
}
return items[0];
}
呼び出し例です。
C#int[] numbers = { 10, 20, 30 };
string[] names = { "Alice", "Bob" };
int firstNumber = FirstOrDefault(numbers);
string firstName = FirstOrDefault(names);
このような処理は、型に依存しないためジェネリックメソッドと相性が良いです。
ほかにも、次のような処理はジェネリックメソッドに向いています。
配列やリストの要素を表示する処理、値を交換する処理、既定値を返す処理、インスタンスを生成する処理、指定条件に合う要素を探す処理などです。
4-5. ジェネリックメソッドを使わない方がよいケース
ジェネリックメソッドは便利ですが、何でもジェネリックにすればよいわけではありません。
まず、特定の型に強く依存する処理には向いていません。
C#static void PrintLength<T>(T value)
{
// value.Length は使えない
}
T がどんな型になるかわからないため、string の Length のような特定の型のメンバーはそのまま使えません。
また、処理内容が型ごとに大きく異なる場合も、ジェネリックメソッドに無理にまとめると読みにくくなります。
C#// int の場合、string の場合、DateTime の場合で処理が大きく違う
このような場合は、オーバーロードや個別メソッドを使った方がわかりやすいことがあります。
さらに、型パラメーターを使っているだけで実際には汎用性がないメソッドも避けるべきです。
ジェネリックメソッドは、「型が違っても同じ考え方で処理できる」ときに使うのが基本です。
5. 型制約whereの使い方
5-1. 型制約とは何か
型制約とは、ジェネリックメソッドの型パラメーターに対して、使用できる型の条件を指定する仕組みです。
型制約は where キーワードを使って書きます。
C#static void Method<T>(T value) where T : 制約
{
// 処理
}
型制約を指定しない場合、T にはほぼどのような型でも指定できます。
そのため、T に対して特定のメンバーを呼び出すことはできません。
C#static void ShowName<T>(T value)
{
// value.Name は使えない
}
しかし、T が特定のインターフェイスを実装していることを制約で指定すれば、そのインターフェイスのメンバーを使えるようになります。
C#interface IHasName
{
string Name { get; }
}
static void ShowName<T>(T value) where T : IHasName
{
Console.WriteLine(value.Name);
}
型制約を使うことで、ジェネリックメソッドの安全性と表現力を高められます。
5-2. where T : classの使い方
where T : class は、T を参照型に制限する制約です。
C#static void CheckNull<T>(T value) where T : class
{
if (value == null)
{
Console.WriteLine("nullです");
}
else
{
Console.WriteLine("nullではありません");
}
}
呼び出し例です。
C#CheckNull<string>("Hello");
CheckNull<string>(null);
class 制約を付けると、T にはクラス、インターフェイス、デリゲート、配列などの参照型を指定できます。
一方、int や bool のような値型は指定できません。
C#// コンパイルエラー
// CheckNull<int>(10);
参照型だけを対象にしたい場合に使います。
5-3. where T : structの使い方
where T : struct は、T を値型に制限する制約です。
C#static void ShowDefault<T>() where T : struct
{
T value = default;
Console.WriteLine(value);
}
呼び出し例です。
C#ShowDefault<int>();
ShowDefault<DateTime>();
ShowDefault<bool>();
struct 制約を付けると、T には int、double、bool、DateTime、独自の構造体などを指定できます。
一方、string やクラス型は指定できません。
C#// コンパイルエラー
// ShowDefault<string>();
値型だけを扱いたい場合に便利です。
5-4. where T : new()の使い方
where T : new() は、T が引数なしの public コンストラクターを持つことを表す制約です。
この制約を付けると、ジェネリックメソッド内で new T() を使えます。
C#static T CreateInstance<T>() where T : new()
{
return new T();
}
呼び出し例です。
C#Person person = CreateInstance<Person>();
Person クラスは次のように定義できます。
C#class Person
{
public string Name { get; set; }
public Person()
{
Name = "No Name";
}
}
new() 制約を付けずに new T() を書くと、コンパイルエラーになります。
C#// static T Create<T>()
// {
// return new T(); // コンパイルエラー
// }
ジェネリックメソッド内でインスタンスを生成したい場合は、new() 制約を忘れないようにしましょう。
5-5. where T : インターフェイスの使い方
where T : インターフェイス名 と書くと、T を特定のインターフェイスを実装した型に制限できます。
C#interface IPrintable
{
void Print();
}
このインターフェイスを使って、次のようなジェネリックメソッドを書けます。
C#static void PrintItem<T>(T item) where T : IPrintable
{
item.Print();
}
T は IPrintable を実装していることが保証されるため、メソッド内で Print() を呼び出せます。
実装クラスの例です。
C#class Report : IPrintable
{
public void Print()
{
Console.WriteLine("レポートを印刷します");
}
}
呼び出し例です。
C#Report report = new Report();
PrintItem(report);
インターフェイス制約は、ジェネリックメソッドで共通の振る舞いを扱いたいときによく使います。
5-6. where T : 基底クラスの使い方
where T : 基底クラス名 と書くと、T を特定のクラスまたはその派生クラスに制限できます。
C#class Animal
{
public void Eat()
{
Console.WriteLine("食べます");
}
}
class Dog : Animal
{
}
基底クラス制約を使ったメソッドです。
C#static void Feed<T>(T animal) where T : Animal
{
animal.Eat();
}
呼び出し例です。
C#Dog dog = new Dog();
Feed(dog);
T は Animal または Animal を継承した型であることが保証されるため、Animal のメンバーを呼び出せます。
C#Animal animal = new Animal();
Feed(animal);
基底クラスで定義された共通処理を使いたい場合に有効です。
5-7. 複数の型制約を組み合わせる方法
型制約は複数組み合わせることができます。
たとえば、T が特定のインターフェイスを実装し、さらに引数なしコンストラクターを持つことを指定できます。
C#static T CreateAndPrint<T>() where T : IPrintable, new()
{
T item = new T();
item.Print();
return item;
}
呼び出し例です。
C#Report report = CreateAndPrint<Report>();
複数の型パラメーターに制約を付けることもできます。
C#static void CopyName<TSource, TDestination>(TSource source, TDestination destination)
where TSource : IHasName
where TDestination : IHasName
{
Console.WriteLine(source.Name);
Console.WriteLine(destination.Name);
}
制約を複数指定することで、ジェネリックでありながら、メソッド内で使える操作を明確にできます。
ただし、制約を増やしすぎると使える型が限られるため、必要な制約だけを指定することが大切です。
6. 型制約を使った実践的なサンプル
6-1. インターフェイス制約で共通メソッドを呼び出す
インターフェイス制約を使うと、ジェネリックメソッド内で共通メソッドを安全に呼び出せます。
まず、次のようなインターフェイスを定義します。
C#interface IValidatable
{
bool IsValid();
}
このインターフェイスを実装するクラスを作ります。
C#class User : IValidatable
{
public string Name { get; set; }
public bool IsValid()
{
return !string.IsNullOrEmpty(Name);
}
}
ジェネリックメソッドは次のように書けます。
C#static bool Validate<T>(T item) where T : IValidatable
{
return item.IsValid();
}
呼び出し例です。
C#User user = new User { Name = "Alice" };
bool result = Validate(user);
Console.WriteLine(result);
where T : IValidatable を指定しているため、item.IsValid() を安全に呼び出せます。
制約がない場合、T に IsValid() が存在する保証がないため、コンパイルエラーになります。
6-2. new()制約でインスタンスを生成する
ジェネリックメソッド内で T のインスタンスを生成したい場合は、new() 制約を使います。
C#static T Create<T>() where T : new()
{
return new T();
}
使用するクラスを定義します。
C#class Product
{
public string Name { get; set; }
public Product()
{
Name = "未設定";
}
}
呼び出し例です。
C#Product product = Create<Product>();
Console.WriteLine(product.Name);
実行結果は次のようになります。
未設定
new() 制約があることで、コンパイラは T が引数なしコンストラクターを持つことを確認できます。
そのため、メソッド内で new T() を使えるようになります。
6-3. class制約でnull判定を安全に扱う
参照型だけを扱いたい場合は、class 制約を使います。
C#static bool IsNull<T>(T value) where T : class
{
return value == null;
}
呼び出し例です。
C#string name = null;
string message = "Hello";
Console.WriteLine(IsNull(name));
Console.WriteLine(IsNull(message));
実行結果です。
True
False
class 制約を付けることで、T は参照型に限定されます。
C#// コンパイルエラー
// IsNull<int>(10);
nullを扱う前提のメソッドでは、class 制約を付けることで意図が明確になります。
6-4. struct制約で値型だけを受け取る
値型だけを対象にしたい場合は、struct 制約を使います。
C#static void ShowValueTypeDefault<T>() where T : struct
{
T value = default;
Console.WriteLine(value);
}
呼び出し例です。
C#ShowValueTypeDefault<int>();
ShowValueTypeDefault<double>();
ShowValueTypeDefault<DateTime>();
値型には既定値があります。
C#int // 0
double // 0
bool // false
DateTime // 0001/01/01 0:00:00
struct 制約を付けると、参照型が渡されることを防げます。
C#// コンパイルエラー
// ShowValueTypeDefault<string>();
値型専用の処理であることを明確にしたい場合に使います。
6-5. 型制約を使うとコンパイルエラーを防げる理由
型制約を使う最大のメリットは、実行時ではなくコンパイル時に問題を検出できることです。
たとえば、次のようなインターフェイスがあります。
C#interface IHasId
{
int Id { get; }
}
型制約を使ったジェネリックメソッドです。
C#static void PrintId<T>(T item) where T : IHasId
{
Console.WriteLine(item.Id);
}
where T : IHasId により、T は必ず Id プロパティを持つことが保証されます。
そのため、メソッド内で item.Id を安全に使えます。
もし IHasId を実装していない型を渡そうとすると、コンパイルエラーになります。
C#class Customer : IHasId
{
public int Id { get; set; }
}
class Category
{
public int Id { get; set; }
}
Customer customer = new Customer { Id = 1 };
PrintId(customer);
// コンパイルエラー
// Category category = new Category { Id = 2 };
// PrintId(category);
Category には Id プロパティがありますが、IHasId を実装していないため、制約を満たしません。
このように、型制約は「使える型の条件」を明確にし、意図しない型の使用を防いでくれます。
7. ジェネリックメソッドでよくあるエラーと注意点
7-1. Tに対して使えないメンバーを呼び出してしまう
ジェネリックメソッドでよくあるエラーの1つが、T に対して存在が保証されていないメンバーを呼び出してしまうことです。
C#static void PrintLength<T>(T value)
{
Console.WriteLine(value.Length);
}
このコードはコンパイルエラーになります。
T が string なら Length はありますが、int や DateTime には Length がありません。
このような場合は、型制約を使います。
C#interface IHasLength
{
int Length { get; }
}
static void PrintLength<T>(T value) where T : IHasLength
{
Console.WriteLine(value.Length);
}
または、対象が文字列だけならジェネリックメソッドにせず、通常のメソッドにする方が自然です。
C#static void PrintLength(string value)
{
Console.WriteLine(value.Length);
}
ジェネリックメソッドでは、「すべての T に対してその操作が成り立つか」を意識する必要があります。
7-2. 型推論がうまく働かない
C#では、多くの場合、引数から型推論が働きます。
C#static T GetValue<T>(T value)
{
return value;
}
int number = GetValue(10);
しかし、戻り値にしか T が登場しない場合、型推論はできません。
C#static T CreateDefault<T>()
{
return default;
}
// コンパイルエラー
// var value = CreateDefault();
この場合は、型を明示する必要があります。
C#int number = CreateDefault<int>();
string text = CreateDefault<string>();
また、引数に null を渡す場合も型推論が難しくなることがあります。
C#static void Show<T>(T value)
{
Console.WriteLine(value);
}
// 型を明示した方がよい
Show<string>(null);
型推論がうまく働かない場合は、<string> や <int> のように型を明示しましょう。
7-3. where制約の指定順を間違える
型制約には、指定順に注意が必要なものがあります。
特に new() 制約は、複数の制約を指定する場合、最後に書きます。
C#static T Create<T>() where T : IPrintable, new()
{
return new T();
}
次のように new() を先に書くとエラーになります。
C#// コンパイルエラー
// static T Create<T>() where T : new(), IPrintable
// {
// return new T();
// }
また、class や struct のような制約は、他の制約より前に書きます。
C#static T CreateService<T>() where T : class, IService, new()
{
return new T();
}
制約が複数ある場合は、順番も含めて正しく指定しましょう。
7-4. new()制約を付け忘れる
ジェネリックメソッド内で new T() を使いたい場合、new() 制約が必要です。
C#static T Create<T>()
{
return new T();
}
このコードはコンパイルエラーになります。
理由は、T に引数なしコンストラクターがあるとは限らないからです。
正しくは次のように書きます。
C#static T Create<T>() where T : new()
{
return new T();
}
ただし、new() 制約で呼び出せるのは引数なしコンストラクターです。
引数付きコンストラクターを呼び出したい場合は、別の方法を検討します。
C#static T Create<T>(Func<T> factory)
{
return factory();
}
呼び出し例です。
C#var user = Create(() => new User("Alice"));
このように、柔軟にインスタンス生成したい場合は、Func<T> を渡す方法もよく使われます。
7-5. object型やdynamic型との使い分けで迷う
ジェネリックメソッド、object型、dynamic型は、どれもさまざまな型を扱えるように見えるため、使い分けに迷うことがあります。
object型は、すべての型の基底型です。どんな値でも受け取れますが、元の型として扱うにはキャストが必要になることがあります。
C#object value = "Hello";
string text = (string)value;
dynamic型は、コンパイル時の型チェックを一部遅らせ、実行時にメンバー呼び出しを解決します。
C#dynamic value = "Hello";
Console.WriteLine(value.Length);
ただし、存在しないメンバーを呼び出してもコンパイル時には検出されず、実行時エラーになる可能性があります。
ジェネリックメソッドは、型を保ったまま共通処理を書けます。
C#static T Echo<T>(T value)
{
return value;
}
型安全性を重視するなら、まずジェネリックメソッドを検討するのがおすすめです。
object は型情報をあまり使わない処理、dynamic は実行時にメンバーを解決したい特殊な場面で使うとよいでしょう。
8. ジェネリックメソッドと関連機能の違い
8-1. ジェネリックメソッドとジェネリッククラスの使い分け
ジェネリックメソッドとジェネリッククラスは、型パラメーターを使う範囲が違います。
ジェネリッククラスは、クラス全体で型を扱いたいときに使います。
C#class Repository<T>
{
public void Add(T item)
{
Console.WriteLine("追加しました");
}
}
呼び出し例です。
C#Repository<string> repository = new Repository<string>();
repository.Add("Item");
一方、ジェネリックメソッドは、メソッド単位で型を扱いたいときに使います。
C#static void Print<T>(T value)
{
Console.WriteLine(value);
}
クラス全体で同じ型を保持する必要があるならジェネリッククラス、特定の処理だけ型を汎用化したいならジェネリックメソッドを使います。
8-2. ジェネリックメソッドとオーバーロードの違い
オーバーロードは、同じメソッド名で引数の型や数が異なるメソッドを複数定義する機能です。
C#static void Print(int value)
{
Console.WriteLine(value);
}
static void Print(string value)
{
Console.WriteLine(value);
}
型ごとに処理内容を変えたい場合は、オーバーロードが向いています。
一方、型が違っても処理内容が同じなら、ジェネリックメソッドが向いています。
C#static void Print<T>(T value)
{
Console.WriteLine(value);
}
使い分けの目安は、処理が型ごとに違うかどうかです。
型ごとに異なるロジックが必要ならオーバーロード、同じロジックでよいならジェネリックメソッドを使うとよいでしょう。
8-3. ジェネリックメソッドとobject型の違い
object型を使うと、どんな型の値でも受け取れます。
C#static void Show(object value)
{
Console.WriteLine(value);
}
しかし、戻り値として使う場合はキャストが必要になることがあります。
C#static object Get(object value)
{
return value;
}
string text = (string)Get("Hello");
ジェネリックメソッドなら、型を保ったまま値を返せます。
C#static T Get<T>(T value)
{
return value;
}
string text = Get("Hello");
object型は柔軟ですが、型安全性はジェネリックメソッドの方が高くなります。
共通処理を書きつつ型情報を保ちたい場合は、ジェネリックメソッドを使うのが基本です。
8-4. ジェネリックメソッドとdynamic型の違い
dynamic型は、コンパイル時ではなく実行時に型を解決します。
C#static void ShowLength(dynamic value)
{
Console.WriteLine(value.Length);
}
このコードは、value に Length があれば動作します。
C#ShowLength("Hello");
しかし、Length がない値を渡すと実行時エラーになります。
C#// 実行時エラー
// ShowLength(123);
ジェネリックメソッドでは、基本的にコンパイル時に型チェックが行われます。
C#static void Show<T>(T value)
{
Console.WriteLine(value);
}
安全性を重視する場合は、dynamicよりジェネリックメソッドを優先しましょう。
dynamicは、COM連携、JSONの動的処理、実行時まで型がわからない特殊な場面などで使われることがあります。
8-5. List<T>など既存のジェネリック型との関係
C#では、List<T> や Dictionary<TKey, TValue> など、多くの標準ライブラリでジェネリックが使われています。
C#List<string> names = new List<string>();
names.Add("Alice");
names.Add("Bob");
List<T> の T は、リストに格納する要素の型を表します。
C#List<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
ジェネリックメソッドは、こうしたジェネリック型と組み合わせてよく使われます。
C#static 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" });
List<T> などの既存のジェネリック型を理解していると、ジェネリックメソッドの考え方も理解しやすくなります。
9. 実務で使いやすいジェネリックメソッドの設計ポイント
9-1. 型パラメーター名の付け方
型パラメーター名は、読みやすさに大きく影響します。
型パラメーターが1つだけの場合は、一般的に T を使います。
C#static T Echo<T>(T value)
{
return value;
}
意味を明確にしたい場合は、TValue、TKey、TResult などを使います。
C#static TValue GetValue<TKey, TValue>(TKey key)
{
// サンプル
return default;
}
よく使われる名前には、次のようなものがあります。
C#T // 汎用的な型
TKey // キーの型
TValue // 値の型
TResult // 結果の型
TSource // 入力元の型
TDestination // 変換先の型
型パラメーターが複数ある場合は、T1、T2 よりも、意味のある名前を付けると読みやすくなります。
9-2. 読みやすいメソッド名にするコツ
ジェネリックメソッドでも、メソッド名は処理内容が伝わる名前にすることが大切です。
悪い例です。
C#static T Do<T>(T value)
{
return value;
}
Do だけでは、何をするメソッドなのかわかりません。
改善例です。
C#static T GetOrDefault<T>(T value, T defaultValue)
{
if (value == null)
{
return defaultValue;
}
return value;
}
メソッド名から「値がなければ既定値を返す」ことが読み取れます。
ジェネリックメソッドは汎用的になりやすいため、名前が曖昧だと使い方がわかりにくくなります。
Create、Convert、Find、Print、GetOrDefault など、処理の目的が伝わる名前を付けましょう。
9-3. 必要な型制約だけを指定する
型制約は便利ですが、必要以上に指定するとメソッドの使い勝手が悪くなります。
たとえば、インスタンス生成をしないのに new() 制約を付ける必要はありません。
C#// new() 制約は不要
static void Print<T>(T value) where T : new()
{
Console.WriteLine(value);
}
この場合は、次のように制約なしで十分です。
C#static void Print<T>(T value)
{
Console.WriteLine(value);
}
一方、メソッド内でインターフェイスのメソッドを呼び出すなら、インターフェイス制約は必要です。
C#static void Save<T>(T item) where T : ISaveable
{
item.Save();
}
型制約は、「メソッド内でその制約が本当に必要か」を基準に指定しましょう。
9-4. 汎用化しすぎないための判断基準
ジェネリックメソッドは便利ですが、汎用化しすぎると逆に読みにくくなります。
たとえば、型ごとに処理が大きく違うにもかかわらず、1つのジェネリックメソッドにまとめると、条件分岐が増えて複雑になります。
C#static void Process<T>(T value)
{
if (value is int)
{
Console.WriteLine("数値処理");
}
else if (value is string)
{
Console.WriteLine("文字列処理");
}
else if (value is DateTime)
{
Console.WriteLine("日付処理");
}
}
このような場合は、オーバーロードや別メソッドに分けた方がわかりやすいことがあります。
ジェネリックメソッドにするか迷ったら、次の観点で判断するとよいです。
型が違っても処理の流れが同じか、型パラメーターによって安全性が上がるか、呼び出し側のコードが読みやすくなるか、制約が複雑になりすぎないか。
これらを満たす場合は、ジェネリックメソッドにする価値があります。
9-5. テストしやすいジェネリックメソッドにする
実務では、ジェネリックメソッドもテストしやすく設計することが重要です。
まず、複数の型でテストしましょう。
C#static T Echo<T>(T value)
{
return value;
}
このようなメソッドなら、int、string、独自クラスなどでテストできます。
C#int number = Echo(10);
string text = Echo("Hello");
User user = Echo(new User { Name = "Alice" });
また、型制約がある場合は、制約を満たす型と満たさない型を意識します。
C#static bool Validate<T>(T item) where T : IValidatable
{
return item.IsValid();
}
この場合、IValidatable を実装したテスト用クラスを用意すると確認しやすくなります。
さらに、default や null を扱うメソッドでは、値型と参照型で挙動が変わることがあります。
ジェネリックメソッドは複数の型で使えるからこそ、代表的な型を選んでテストすることが大切です。
10. C#ジェネリックメソッドの理解を深めるFAQ
10-1. Tは必ずTという名前にしないといけない?
いいえ、必ず T という名前にする必要はありません。
次のように、任意の名前を使えます。
C#static void Show<TValue>(TValue value)
{
Console.WriteLine(value);
}
ただし、C#では型パラメーター名に T を付ける慣習があります。
型パラメーターが1つだけなら T、意味を明確にしたいなら TValue や TResult のような名前を使うとよいでしょう。
C#static TResult ConvertValue<TSource, TResult>(TSource source)
{
return default;
}
大切なのは、メソッドの役割が読みやすくなる名前を付けることです。
10-2. ジェネリックメソッドで演算子は使える?
通常のジェネリックメソッドでは、T に対して自由に演算子を使うことはできません。
たとえば、次のコードはコンパイルエラーになります。
C#static T Add<T>(T a, T b)
{
return a + b;
}
理由は、T が int なら + が使えますが、すべての型で + が使えるとは限らないからです。
簡単な方法としては、型を限定してオーバーロードを使う方法があります。
C#static int Add(int a, int b)
{
return a + b;
}
static double Add(double a, double b)
{
return a + b;
}
また、数値型を汎用的に扱いたい場合は、C#のバージョンや利用環境に応じて、ジェネリック数値演算に対応したインターフェイスを検討することもあります。
ただし、初心者のうちは「T に対して演算子はそのまま使えないことが多い」と覚えておくとよいでしょう。
10-3. 戻り値だけにTを使うことはできる?
できます。
たとえば、次のようなメソッドです。
C#static T GetDefault<T()
{
return default;
}
正しくは次のように書きます。
C#static T GetDefault<T>()
{
return default;
}
呼び出し時には型を明示する必要があります。
C#int number = GetDefault<int>();
string text = GetDefault<string>();
戻り値だけに T が使われている場合、引数から型推論できないためです。
次のように型指定を省略すると、コンパイラが T を判断できません。
C#// コンパイルエラー
// var value = GetDefault();
戻り値だけに T を使うことは可能ですが、呼び出し時に型を明示する必要がある点に注意しましょう。
10-4. staticメソッドでもジェネリックは使える?
はい、static メソッドでもジェネリックは使えます。
C#class Utility
{
public static void Print<T>(T value)
{
Console.WriteLine(value);
}
}
呼び出し例です。
C#Utility.Print<int>(100);
Utility.Print<string>("Hello");
型推論を使えば、型指定を省略できます。
C#Utility.Print(100);
Utility.Print("Hello");
ユーティリティメソッドはインスタンスを持たずに使うことが多いため、static なジェネリックメソッドは実務でもよく使われます。
C#public static T GetFirstOrDefault<T>(IEnumerable<T> items)
{
foreach (T item in items)
{
return item;
}
return default;
}
このように、共通処理をまとめる場面で static ジェネリックメソッドは非常に便利です。
10-5. ジェネリックメソッドはパフォーマンスに影響する?
ジェネリックメソッドは、型安全性と再利用性を高めるための機能であり、多くの場面でパフォーマンス面でも有利に働きます。
特に、object型を使って値型を扱う場合、ボックス化とアンボックス化が発生することがあります。
C#object value = 10; // ボックス化
int number = (int)value; // アンボックス化
ジェネリックメソッドを使うと、型を保ったまま処理できるため、このような余計な変換を避けやすくなります。
C#static T Echo<T>(T value)
{
return value;
}
int number = Echo(10);
ただし、実際のパフォーマンスは処理内容や利用状況によって変わります。
通常のアプリケーション開発では、まず可読性、型安全性、保守性を重視し、必要になった段階で計測して最適化するのが基本です。
ジェネリックメソッドは、正しく使えばパフォーマンスを大きく悪化させるものではなく、むしろ安全で効率的なコードを書くために役立ちます。
まとめ
C#のジェネリックメソッドは、型を固定せずにメソッドを定義できる便利な機能です。
C#static T Echo<T>(T value)
{
return value;
}
このように書くことで、int、string、DateTime、独自クラスなど、さまざまな型に対して同じ処理を使えます。
ジェネリックメソッドを使う主なメリットは、型だけが違う重複コードを減らせること、型安全に共通処理を書けること、object型によるキャストを避けやすいこと、ユーティリティメソッドを汎用化できることです。
また、where による型制約を使うと、使用できる型を制限できます。
C#static void Print<T>(T item) where T : IPrintable
{
item.Print();
}
型制約を使えば、ジェネリックでありながら、特定のメソッドやプロパティを安全に呼び出せます。
一方で、何でもジェネリックにすればよいわけではありません。型ごとに処理が大きく違う場合や、特定の型専用の処理で十分な場合は、通常のメソッドやオーバーロードの方が読みやすいこともあります。
C#ジェネリックメソッドを使いこなすには、まず「型が違っても同じ処理として表現できるか」を考えることが大切です。
重複コードを減らし、型安全で保守しやすいコードを書くために、ジェネリックメソッドをぜひ活用してみてください。

