C#ジェネリッククラス入門:型安全に使い回せるクラスの作り方を初心者向けにわかりやすく解説

はじめに

C#でコードを書いていると、List<int>Dictionary<string, int>のように、< >を使った書き方をよく見かけます。この<T><TKey, TValue>を使った仕組みが「ジェネリック」です。

ジェネリッククラスを使うと、int用、string用、User用のように型ごとに似たクラスを何度も作らなくても、1つのクラスをさまざまな型で使い回せます。

たとえば、「値を1つ入れておく箱」を作りたい場合、通常なら次のように型ごとにクラスを作る必要があります。

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

public class StringBox
{
public string Value { get; set; }
}

しかし、ジェネリッククラスを使えば次のように1つのクラスで対応できます。

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

このTの部分に、あとからintstringなどの型を指定できます。

この記事では、C#のジェネリッククラスについて、初心者にもわかりやすいように基本から実践例まで順番に解説します。

1. C#のジェネリッククラスとは?初心者向けに基本を解説

C#のジェネリッククラスとは、使用するときに型を指定できるクラスのことです。

通常のクラスでは、プロパティやメソッドの引数、戻り値の型をあらかじめ決めておく必要があります。一方、ジェネリッククラスでは、クラスを定義する時点では具体的な型を決めず、使うタイミングで型を指定できます。

代表的な例はList<T>です。

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

List<T>という同じクラスでも、List<int>なら数値のリスト、List<string>なら文字列のリストとして使えます。

1-1. ジェネリッククラスは「型を後から指定できるクラス」

ジェネリッククラスは、簡単に言うと「中で扱う型を後から決められるクラス」です。

たとえば、次のようなクラスを考えます。

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

この時点では、Valueintなのかstringなのかは決まっていません。Tは仮の型です。

実際に使うときに、次のように型を指定します。

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

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

Box<int>と書けば、Tintとして扱われます。
Box<string>と書けば、Tstringとして扱われます。

つまり、ジェネリッククラスは「型の部分を差し替えられるクラス」と考えると理解しやすいです。

1-2. Tとは何か?型パラメーターの意味

Tは「型パラメーター」と呼ばれるものです。

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

このTは特別な型そのものではありません。intstringUserなど、あとから指定される型の代わりに使う名前です。

たとえば、次のように使った場合、

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

Box<T>Tintに置き換わります。

そのため、実質的には次のようなイメージになります。

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

もちろん実際にC#がこのコードに書き換えるわけではありませんが、初心者のうちは「Tが指定した型に置き換わる」と考えるとわかりやすいです。

なお、型パラメーター名は必ずTである必要はありません。慣習として、1つの型を表す場合はT、キーと値を表す場合はTKeyTValueのように名前を付けることが多いです。

1-3. 通常のクラスとの違い

通常のクラスでは、扱う型をクラス定義時に決めます。

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

このクラスはint専用です。文字列を扱いたい場合は、別のクラスが必要になります。

C#
public class StringBox
{
public string Value { get; set; }
}

一方、ジェネリッククラスでは、型を固定せずに定義できます。

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

この1つのクラスを使って、数値も文字列も扱えます。

C#
Box<int> numberBox = new Box<int>();
Box<string> textBox = new Box<string>();

通常のクラスは「特定の型に向けたクラス」、ジェネリッククラスは「複数の型で使える汎用的なクラス」と考えるとよいでしょう。

1-4. ジェネリックメソッドとの違い

ジェネリックには、ジェネリッククラスだけでなく、ジェネリックメソッドもあります。

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

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

public void SetValue(T value)
{
Value = value;
}
}

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

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

使い方は次のようになります。

C#
Print<int>(100);
Print<string>("Hello");

つまり、ジェネリッククラスは「クラス全体を汎用化する仕組み」、ジェネリックメソッドは「メソッドだけを汎用化する仕組み」です。

クラスのプロパティや複数のメソッドで同じ型を使いたい場合はジェネリッククラス、1つのメソッドだけで型を柔軟にしたい場合はジェネリックメソッドが向いています。

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

ジェネリッククラスを使う主なメリットは、型安全性を保ちながらコードを使い回せることです。

C#は静的型付け言語なので、変数やプロパティの型が重要です。ジェネリッククラスを使うと、型を明確にしながら、同じ処理を複数の型で再利用できます。

2-1. 型安全にコードを書ける

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

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

// エラー
// box.Value = "Hello";

Box<int>では、Valueintとして扱われます。そのため、文字列を代入しようとするとコンパイル時にエラーになります。

これは大きなメリットです。プログラムを実行してからエラーに気づくのではなく、コードを書いている段階でミスに気づけます。

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

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

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

public void Show()
{
Console.WriteLine(Value);
}
}

このクラスは、intでもstringでも使えます。

C#
Box<int> intBox = new Box<int>();
intBox.Value = 10;
intBox.Show();

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

もしジェネリックを使わなければ、IntBoxStringBoxUserBoxのように似たクラスをいくつも作ることになります。

ジェネリッククラスを使えば、処理の共通部分を1つにまとめられます。

2-3. キャストミスや実行時エラーを減らせる

ジェネリックを使わずにobject型で値を扱うと、取り出すときにキャストが必要になります。

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

使う側では、次のようにキャストします。

C#
ObjectBox box = new ObjectBox();
box.Value = 100;

int number = (int)box.Value;

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

C#
box.Value = "Hello";

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

ジェネリッククラスなら、型が最初から決まっているため、このようなキャストミスを防ぎやすくなります。

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

int number = box.Value;

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

2-4. object型で共通化する方法との違い

複数の型を扱いたいだけなら、object型を使う方法もあります。

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

object型はすべての型の基底型なので、数値も文字列も入れられます。

C#
ObjectBox box = new ObjectBox();
box.Value = 123;
box.Value = "Hello";

一見便利ですが、どんな型でも入れられるため、意図しない値が入る可能性があります。

また、値を取り出すときにキャストが必要です。

C#
string text = (string)box.Value;

ジェネリッククラスでは、使う時点で型を指定します。

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

Box<string>なら文字列専用として扱えるため、間違って数値を入れることはできません。

C#
// エラー
// box.Value = 123;

object型は柔軟ですが、型安全性が弱くなります。ジェネリッククラスは柔軟性と型安全性を両立できる方法です。

2-5. コードの重複を減らして保守しやすくなる

同じような処理を型ごとに書くと、コードが重複します。

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

public class StringStorage
{
public string Value { get; set; }
}

public class DateTimeStorage
{
public DateTime Value { get; set; }
}

このようなクラスが増えると、修正が必要になったときにすべてのクラスを変更しなければなりません。

ジェネリッククラスにすれば、共通処理を1か所にまとめられます。

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

修正箇所が少なくなり、コードの保守性が高まります。

3. C#ジェネリッククラスの基本的な書き方

ここからは、C#でジェネリッククラスを書く基本構文を見ていきます。

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

3-1. ジェネリッククラスの基本構文

基本構文は次のとおりです。

C#
public class クラス名<T>
{
// Tを型として使える
}

具体例は次のようになります。

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

Tは型パラメーターです。クラス内では、通常の型と同じように使えます。

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

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

public T GetValue()
{
return _value;
}
}

このように、フィールド、プロパティ、メソッドの引数、戻り値などにTを使えます。

3-2. 型パラメーターTを使ったプロパティの定義

ジェネリッククラスでは、プロパティの型としてTを使えます。

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

このValueの型は、インスタンス化するときに決まります。

C#
Box<int> intBox = new Box<int>();
intBox.Value = 10;

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

Box<int>ではValueintBox<string>ではValuestringです。

同じBox<T>というクラスでも、指定する型によってプロパティの型が変わるのがポイントです。

3-3. コンストラクターで値を受け取る方法

ジェネリッククラスでも、通常のクラスと同じようにコンストラクターを定義できます。

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

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

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

C#
Box<int> intBox = new Box<int>(100);
Console.WriteLine(intBox.Value);

Box<string> stringBox = new Box<string>("Hello");
Console.WriteLine(stringBox.Value);

コンストラクターの引数にもTを使えるため、指定した型に応じた値を受け取れます。

3-4. メソッドの戻り値や引数にTを使う方法

Tは、メソッドの引数や戻り値にも使えます。

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

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

public T GetValue()
{
return _value;
}
}

使用例です。

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

int value = box.GetValue();
Console.WriteLine(value);

Box<int>として使っているため、SetValueの引数はintGetValueの戻り値もintになります。

C#
Box<string> textBox = new Box<string>();
textBox.SetValue("C#");

string text = textBox.GetValue();
Console.WriteLine(text);

Box<string>として使えば、SetValueの引数はstringGetValueの戻り値もstringになります。

3-5. intやstringを指定してインスタンス化する例

ジェネリッククラスを使うときは、クラス名の後ろに具体的な型を指定します。

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

C#のバージョンによっては、右辺の型指定を省略して書けます。

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

ただし、初心者のうちは次のように明示的に書いたほうが理解しやすいです。

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

Box<int>は「intを扱うBox」、Box<string>は「stringを扱うBox」と読むとイメージしやすくなります。

4. 初心者向け:ジェネリッククラスを実際に作ってみよう

ここでは、実際にBox<T>というジェネリッククラスを作って、intstringで使ってみます。

4-1. 値を1つ保持するBox<T>クラスの作成

まずは、値を1つ保持するシンプルなクラスを作ります。

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

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

public void Show()
{
Console.WriteLine(Value);
}
}

このクラスは、指定された型の値をValueに保持します。Showメソッドでは、その値を画面に表示します。

4-2. Box<int>で数値を扱う例

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

C#
Box<int> numberBox = new Box<int>(100);
numberBox.Show();

実行結果は次のようになります。

C#
100

この場合、Valueint型です。

C#
int number = numberBox.Value;
Console.WriteLine(number + 50);

intとして扱えるため、計算にもそのまま使えます。

4-3. Box<string>で文字列を扱う例

次に、Box<string>として使ってみます。

C#
Box<string> messageBox = new Box<string>("Hello C#");
messageBox.Show();

実行結果は次のようになります。

C#
Hello C#

この場合、Valuestring型です。

C#
string message = messageBox.Value;
Console.WriteLine(message.ToUpper());

文字列として扱えるため、ToUpperのようなstringのメソッドも使えます。

4-4. 型が違っても同じクラスを使い回せることを確認する

ここまでの例では、同じBox<T>クラスを使って、数値と文字列の両方を扱いました。

C#
Box<int> numberBox = new Box<int>(100);
Box<string> messageBox = new Box<string>("Hello");

定義したクラスは1つだけです。

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

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

それでも、int用にもstring用にも使えます。これがジェネリッククラスの大きな特徴です。

型ごとに似たクラスを作る必要がなくなり、コードがすっきりします。

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

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

C#
Box<int> numberBox = new Box<int>(100);

// エラー
// numberBox.Value = "Hello";

numberBoxBox<int>なので、Valueにはintしか入れられません。

反対に、Box<string>には文字列しか入れられません。

C#
Box<string> messageBox = new Box<string>("Hello");

// エラー
// messageBox.Value = 123;

このように、型の間違いを実行前に検出できるため、安全なコードを書きやすくなります。

5. 複数の型を扱うジェネリッククラスの作り方

ジェネリッククラスでは、型パラメーターを1つだけでなく、複数指定することもできます。

たとえば、「キー」と「値」のように、異なる2つの型を扱いたい場合に便利です。

5-1. 型パラメーターを2つ使う基本構文

型パラメーターを2つ使う場合は、カンマで区切って書きます。

C#
public class クラス名<T1, T2>
{
}

実際には、意味がわかりやすい名前を付けることが多いです。

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

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

5-2. Pair<TKey, TValue>クラスの作成例

キーと値を1組で保持するPair<TKey, TValue>クラスを作ってみましょう。

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

public Pair(TKey key, TValue value)
{
Key = key;
Value = value;
}

public void Show()
{
Console.WriteLine($"{Key}: {Value}");
}
}

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

C#
Pair<string, int> age = new Pair<string, int>("Alice", 25);
age.Show();

Pair<int, string> status = new Pair<int, string>(200, "OK");
status.Show();

Pair<string, int>では、KeystringValueintになります。
Pair<int, string>では、KeyintValuestringになります。

5-3. Dictionary<TKey, TValue>のような考え方

C#の標準ライブラリにあるDictionary<TKey, TValue>も、複数の型パラメーターを使う代表的なジェネリッククラスです。

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

scores["Alice"] = 90;
scores["Bob"] = 80;

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

C#
int aliceScore = scores["Alice"];
Console.WriteLine(aliceScore);

Dictionary<TKey, TValue>は、キーの型と値の型を自由に指定できます。

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

messages[1] = "Start";
messages[2] = "Stop";

このように、2つの型を組み合わせて使いたい場合に、複数の型パラメーターが役立ちます。

5-4. 型パラメーター名の付け方と命名ルール

型パラメーター名は自由に付けられます。

C#
public class Box<T>
{
}

T以外の名前も使えます。

C#
public class Box<TItem>
{
}

一般的には、次のような名前がよく使われます。

C#
T
TItem
TKey
TValue
TResult
TEntity

1つだけならT、コレクションの要素ならTItem、キーと値ならTKeyTValueのように、役割が伝わる名前を使うと読みやすくなります。

悪い例は、意味のわかりにくい名前です。

C#
public class Pair<A, B>
{
}

短いサンプルなら問題ありませんが、実務ではTKeyTValueのように意味がわかる名前のほうが保守しやすいです。

5-5. 型パラメーターを増やしすぎる場合の注意点

型パラメーターは複数使えますが、増やしすぎるとクラスの意味がわかりにくくなります。

C#
public class Sample<T1, T2, T3, T4, T5>
{
}

このようなクラスは、使う側も読む側も理解しにくくなります。

型パラメーターが多くなりすぎる場合は、設計を見直したほうがよいことがあります。

たとえば、関連する値を別のクラスにまとめられないか考えます。

C#
public class UserInfo
{
public string Name { get; set; }
public int Age { get; set; }
}

ジェネリックは便利ですが、何でも汎用化すればよいわけではありません。読みやすさとのバランスが大切です。

6. ジェネリッククラスの型制約を理解する

ジェネリッククラスでは、Tに指定できる型を制限できます。この仕組みを「型制約」と呼びます。

型制約を使うと、「参照型だけ許可する」「値型だけ許可する」「特定のインターフェイスを実装している型だけ許可する」といった指定ができます。

6-1. 型制約とは何か

型制約とは、型パラメーターに指定できる型の条件を決める仕組みです。

たとえば、次のように書きます。

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

この場合、Tには参照型だけを指定できます。

C#
Repository<string> repo1 = new Repository<string>();

// エラー
// Repository<int> repo2 = new Repository<int>();

stringは参照型なので使えますが、intは値型なので使えません。

型制約を使うことで、ジェネリッククラスの中で使える操作を増やしたり、意図しない型が指定されるのを防いだりできます。

6-2. where T : classで参照型に制限する

where T : classを使うと、Tを参照型に制限できます。

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

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

使える例です。

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

使えない例です。

C#
// エラー
// ReferenceBox<int> intBox = new ReferenceBox<int>(10);

参照型だけを扱いたい場合に、where T : classを使います。

たとえば、エンティティクラスやDTOなど、クラス型だけを対象にするリポジトリを作りたい場合に使われます。

6-3. where T : structで値型に制限する

where T : structを使うと、Tを値型に制限できます。

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

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

使える例です。

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

使えない例です。

C#
// エラー
// ValueBox<string> stringBox = new ValueBox<string>("Hello");

intDateTimeは値型なので使えますが、stringは参照型なので使えません。

値型だけを対象にしたい場合に便利です。

6-4. where T : new()でインスタンス生成を許可する

ジェネリッククラス内でnew T()を使いたい場合は、where T : new()制約が必要です。

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

new()制約は、「引数なしコンストラクターを持つ型だけ許可する」という意味です。

たとえば、次のクラスがあるとします。

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

このクラスは引数なしで生成できるため、Factory<T>で使えます。

C#
Factory<User> factory = new Factory<User>();
User user = factory.Create();

型制約がない状態でnew T()を書こうとすると、コンパイルエラーになります。

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

C#では、Tがどのような型かわからないため、引数なしで生成できる保証がありません。そのため、new()制約が必要です。

6-5. インターフェイス制約を使うケース

型パラメーターに、特定のインターフェイスを実装している型だけを指定したい場合があります。

たとえば、次のようなインターフェイスを用意します。

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

このインターフェイスを実装したクラスを作ります。

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

ジェネリッククラスにインターフェイス制約を付けます。

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

where T : IEntityと書くことで、Tは必ずIEntityを実装していると保証されます。

そのため、クラス内でitem.Idにアクセスできます。

C#
Repository<User> repository = new Repository<User>();
repository.ShowId(new User { Id = 1, Name = "Alice" });

インターフェイス制約は、実務でもよく使われます。特に、共通のプロパティやメソッドを持つ型だけを対象にしたい場合に便利です。

6-6. 型制約を使うとできることが増える理由

型制約がない場合、C#はTがどのような型なのか判断できません。

C#
public class Sample<T>
{
public void DoSomething(T value)
{
// value.Id は使えない
// Console.WriteLine(value.Id);
}
}

Tにはintstringも指定できるため、Idというプロパティがあるとは限りません。そのため、value.Idは使えません。

しかし、インターフェイス制約を付けると状況が変わります。

C#
public class Sample<T> where T : IEntity
{
public void DoSomething(T value)
{
Console.WriteLine(value.Id);
}
}

where T : IEntityにより、Tは必ずIEntityを実装していることが保証されます。したがって、Idにアクセスできます。

型制約は、単に型を制限するだけではありません。ジェネリッククラスの中で安全に使えるメンバーを増やすための仕組みでもあります。

7. C#の代表的なジェネリッククラスの例

C#には、標準ライブラリとして多くのジェネリッククラスが用意されています。

初心者が最初に覚えるべき代表例は、List<T>Dictionary<TKey, TValue>Queue<T>Stack<T>です。

7-1. List<T>は最もよく使うジェネリッククラス

List<T>は、複数の値を順番に管理するためのジェネリッククラスです。

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

numbers.Add(10);
numbers.Add(20);
numbers.Add(30);

List<int>では、intだけを追加できます。

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

文字列のリストを作りたい場合は、List<string>を使います。

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

names.Add("Alice");
names.Add("Bob");

List<T>は、実務でも非常によく使います。配列よりも要素の追加や削除がしやすいため、可変長のデータを扱う場面で便利です。

7-2. Dictionary<TKey, TValue>でキーと値を管理する

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

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

scores["Alice"] = 90;
scores["Bob"] = 80;

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

値を取り出すときは、キーを指定します。

C#
Console.WriteLine(scores["Alice"]);

キーの型と値の型は自由に指定できます。

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

messages[1] = "Success";
messages[2] = "Error";

Dictionary<TKey, TValue>は、IDからデータを取得したい場合や、名前とスコアを対応させたい場合などに使われます。

7-3. Queue<T>とStack<T>の使いどころ

Queue<T>は、先に入れたものを先に取り出すデータ構造です。これをFIFOと呼びます。

C#
Queue<string> queue = new Queue<string>();

queue.Enqueue("A");
queue.Enqueue("B");
queue.Enqueue("C");

Console.WriteLine(queue.Dequeue());

実行結果は次のようになります。

C#
A

最初に入れたAが最初に取り出されます。順番待ちの処理などに向いています。

一方、Stack<T>は、後に入れたものを先に取り出すデータ構造です。これをLIFOと呼びます。

C#
Stack<string> stack = new Stack<string>();

stack.Push("A");
stack.Push("B");
stack.Push("C");

Console.WriteLine(stack.Pop());

実行結果は次のようになります。

C#
C

最後に入れたCが最初に取り出されます。戻る操作や履歴管理のような処理に向いています。

7-4. 自作ジェネリッククラスと標準ライブラリの関係

List<T>Dictionary<TKey, TValue>も、考え方としては自作のジェネリッククラスと同じです。

たとえば、List<T>は「T型の要素を複数保持するクラス」です。

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

自作のBox<T>は「T型の値を1つ保持するクラス」です。

C#
Box<int> numberBox = new Box<int>(10);
Box<string> textBox = new Box<string>("Hello");

標準ライブラリのジェネリッククラスは、高機能で実用的なジェネリッククラスの例です。自作ジェネリッククラスを学ぶことで、List<T>Dictionary<TKey, TValue>の仕組みも理解しやすくなります。

7-5. 実務でジェネリッククラスが使われる場面

実務では、ジェネリッククラスはさまざまな場面で使われます。

たとえば、APIのレスポンスを表すクラスです。

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

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

C#
ApiResponse<User> response = new ApiResponse<User>
{
Success = true,
Data = new User { Id = 1, Name = "Alice" },
Message = "OK"
};

商品情報を返す場合は、同じクラスを使って型だけ変えます。

C#
ApiResponse<Product> response = new ApiResponse<Product>
{
Success = true,
Data = new Product { Id = 10, Name = "Book" },
Message = "OK"
};

このように、共通の構造を持ちながら中身の型だけが変わる場面で、ジェネリッククラスはとても役立ちます。

8. ジェネリッククラスでよくあるエラーと解決方法

ジェネリッククラスは便利ですが、初心者がつまずきやすいポイントもあります。

特に多いのは、「Tの型が決まっていないため特定のメンバーが使えない」「型制約がないためnew T()できない」「nullの扱いで迷う」といったエラーです。

8-1. Tの型が決まっていないため使えないメンバーがある

次のコードを見てください。

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

Tにはどの型が指定されるかわかりません。Nameプロパティを持つ型かもしれませんが、intDateTimeのようにNameを持たない型かもしれません。

そのため、C#はitem.Nameを許可しません。

解決するには、インターフェイス制約を使います。

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

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

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

8-2. 型制約がないとnew T()できない

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

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

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

解決するには、new()制約を付けます。

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

これで、Tには引数なしコンストラクターを持つ型だけを指定できるようになります。

C#
Factory<User> factory = new Factory<User>();
User user = factory.Create();

なお、new()制約は複数の制約と組み合わせる場合、最後に書きます。

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

8-3. nullを扱うときの注意点

ジェネリッククラスでnullを扱うときは、Tが参照型なのか値型なのかを意識する必要があります。

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

Tstringならnullを入れられます。

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

しかし、Tintの場合、通常のintにはnullを入れられません。

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

// エラー
// numberBox.Value = null;

値がない状態を表したい場合は、int?のようにnullable型を使います。

C#
Box<int?> nullableBox = new Box<int?>();
nullableBox.Value = null;

また、Tに対して初期値を返したい場合は、defaultを使うことがあります。

C#
public class Box<T>
{
public T GetDefault()
{
return default;
}
}

Tintなら0boolならfalse、参照型ならnullが返ります。

8-4. 型パラメーターと実際の型の不一致

ジェネリッククラスでは、指定した型と実際に使う値の型が一致している必要があります。

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

この場合、boxintを扱う箱です。文字列を入れることはできません。

C#
// エラー
// box.Value = "Hello";

また、次のような代入もできません。

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

// エラー
// Box<object> objectBox = stringBox;

stringobjectを継承していますが、Box<string>をそのままBox<object>として扱えるわけではありません。

初心者は「stringobjectだから代入できそう」と考えがちですが、ジェネリッククラスでは型の関係に注意が必要です。

8-5. 初心者が混乱しやすいコンパイルエラーの読み方

ジェネリッククラスのエラーでは、Tや型制約に関するメッセージがよく出ます。

たとえば、new T()でエラーになる場合は、「Tは引数なしコンストラクターを持つ必要がある」という意味です。この場合は、where T : new()を検討します。

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

Tのメンバーにアクセスできないエラーが出た場合は、「Tがそのメンバーを持つ保証がない」という意味です。この場合は、インターフェイス制約を検討します。

C#
public class Sample<T> where T : IHasName
{
public void Show(T item)
{
Console.WriteLine(item.Name);
}
}

エラーが出たときは、まず「Tにはどんな型が指定される可能性があるか」を考えると原因を見つけやすくなります。

9. ジェネリッククラスを使うべき場面・使わないほうがよい場面

ジェネリッククラスは便利ですが、常に使えばよいわけではありません。

使うべき場面と、使わないほうがよい場面を理解しておくと、読みやすく保守しやすいコードを書けます。

9-1. 同じ処理で型だけが違う場合は使う

ジェネリッククラスが向いているのは、「処理は同じで、扱う型だけが違う」場合です。

たとえば、値を保持するクラスは型に関係なく同じ処理で済みます。

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

このクラスは、intでもstringでもUserでも使えます。

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

同じようなクラスを型ごとに作っている場合は、ジェネリッククラスにできないか考えてみるとよいでしょう。

9-2. 型安全性を保ちたい場合は使う

複数の型を扱いたい場合でも、object型でまとめると型安全性が弱くなります。

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

このクラスでは、どんな型でも入れられます。

C#
ObjectBox box = new ObjectBox();
box.Value = 100;
box.Value = "Hello";

柔軟ですが、取り出すときにキャストが必要になります。

C#
string text = (string)box.Value;

型を間違えると実行時エラーになります。

ジェネリッククラスなら、使う時点で型を決められます。

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

型安全性を保ちながら汎用的なコードを書きたい場合は、ジェネリッククラスを使うのが適しています。

9-3. 1つの型でしか使わないなら通常のクラスでよい

ジェネリッククラスは、複数の型で使い回すための仕組みです。

そのため、最初から1つの型でしか使わないことが明確なら、通常のクラスで十分です。

C#
public class UserProfile
{
public string Name { get; set; }
public int Age { get; set; }
}

このクラスはユーザー情報を表す専用クラスです。無理にジェネリックにする必要はありません。

C#
public class Profile<TName, TAge>
{
public TName Name { get; set; }
public TAge Age { get; set; }
}

このように汎用化しすぎると、かえって意味がわかりにくくなります。

型を変えて使う必要がないなら、通常のクラスを選びましょう。

9-4. 汎用化しすぎると読みにくくなる

ジェネリッククラスは便利ですが、汎用化しすぎるとコードが読みにくくなります。

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

このようなクラスは、何をするクラスなのか名前だけでは判断しにくくなります。

また、型パラメーターが多いと、使う側のコードも複雑になります。

C#
Processor<User, UserDto, ConvertOption, Result<UserDto>> processor;

実務では、汎用性だけでなく、読みやすさも重要です。

「本当に複数の型で使う必要があるか」「型パラメーターを減らせないか」を考えることが大切です。

9-5. 初心者が判断するときのチェックポイント

初心者がジェネリッククラスを使うべきか迷ったときは、次の点を確認すると判断しやすくなります。

まず、同じようなクラスを型違いで複数作っていないか確認します。

C#
IntBox
StringBox
UserBox

このようなクラスが並んでいる場合は、Box<T>にまとめられる可能性があります。

次に、object型を使って何でも入れられるようにしていないか確認します。

C#
public object Value { get; set; }

型安全性を保ちたいなら、ジェネリッククラスのほうが適していることがあります。

最後に、1つの型でしか使わないクラスを無理にジェネリックにしていないか確認します。

ジェネリッククラスは、必要な場面で使うと強力ですが、不要な場面で使うとコードが複雑になります。

10. C#ジェネリッククラスの理解を深める実践サンプル

ここでは、実務に近いジェネリッククラスのサンプルを紹介します。

単純なBox<T>だけでなく、リポジトリ、共通レスポンス、キャッシュなどの例を見ることで、ジェネリッククラスの使いどころがより具体的に理解できます。

10-1. 汎用的なリポジトリクラスを作る例

リポジトリとは、データの取得や保存を担当するクラスです。

たとえば、ユーザー専用のリポジトリを作ると次のようになります。

C#
public class UserRepository
{
private readonly List<User> _items = new List<User>();

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

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

商品用には、似たようなクラスが必要になります。

C#
public class ProductRepository
{
private readonly List<Product> _items = new List<Product>();

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

public List<Product> GetAll()
{
return _items;
}
}

この重複は、ジェネリッククラスでまとめられます。

C#
public class Repository<T>
{
private readonly List<T> _items = new List<T>();

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

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

使い方です。

C#
Repository<User> userRepository = new Repository<User>();
userRepository.Add(new User { Id = 1, Name = "Alice" });

Repository<Product> productRepository = new Repository<Product>();
productRepository.Add(new Product { Id = 10, Name = "Book" });

同じRepository<T>を使って、ユーザーも商品も管理できます。

10-2. 共通レスポンスクラスResult<T>を作る例

APIや処理結果を表すクラスでは、「成功したか」「メッセージ」「データ」をまとめて返したいことがあります。

そのような場合、Result<T>を作ると便利です。

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

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

ユーザー情報を返す場合です。

C#
Result<User> result = new Result<User>(
true,
"ユーザーを取得しました",
new User { Id = 1, Name = "Alice" }
);

商品情報を返す場合も、同じResult<T>を使えます。

C#
Result<Product> productResult = new Result<Product>(
true,
"商品を取得しました",
new Product { Id = 10, Name = "Book" }
);

Result<T>を使うと、結果の形式を統一しながら、データ部分だけ型を変えられます。

10-3. データを型安全に保持するキャッシュクラスの例

簡単なキャッシュクラスも、ジェネリックで作れます。

C#
public class Cache<T>
{
private readonly Dictionary<string, T> _items = new Dictionary<string, T>();

public void Set(string key, T value)
{
_items[key] = value;
}

public T Get(string key)
{
return _items[key];
}

public bool Contains(string key)
{
return _items.ContainsKey(key);
}
}

文字列をキャッシュする例です。

C#
Cache<string> stringCache = new Cache<string>();

stringCache.Set("greeting", "Hello");
Console.WriteLine(stringCache.Get("greeting"));

ユーザー情報をキャッシュする例です。

C#
Cache<User> userCache = new Cache<User>();

userCache.Set("user:1", new User { Id = 1, Name = "Alice" });
User user = userCache.Get("user:1");

Cache<T>を使えば、キャッシュする値の型を安全に指定できます。

10-4. ジェネリッククラスを使ったコード改善例

次のように、object型を使ったクラスがあるとします。

C#
public class DataHolder
{
public object Data { get; set; }
}

このクラスを使うと、取り出すときにキャストが必要です。

C#
DataHolder holder = new DataHolder();
holder.Data = "Hello";

string text = (string)holder.Data;

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

C#
holder.Data = 123;

// 実行時エラー
string text = (string)holder.Data;

ジェネリッククラスに改善します。

C#
public class DataHolder<T>
{
public T Data { get; set; }
}

使い方です。

C#
DataHolder<string> holder = new DataHolder<string>();
holder.Data = "Hello";

string text = holder.Data;

キャストが不要になり、間違った型の値も入れられなくなります。

C#
// エラー
// holder.Data = 123;

このように、object型で無理に共通化しているコードは、ジェネリッククラスにすると安全で読みやすくなることがあります。

10-5. 通常クラスからジェネリッククラスへリファクタリングする手順

通常クラスをジェネリッククラスへリファクタリングする場合は、まず「型だけが違う重複」を探します。

たとえば、次の2つのクラスがあるとします。

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

public class StringHolder
{
public string Value { get; set; }
}

構造は同じで、違うのはValueの型だけです。この場合、型をTに置き換えます。

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

次に、使用箇所を変更します。

C#
IntHolder intHolder = new IntHolder();

これを次のようにします。

C#
Holder<int> intHolder = new Holder<int>();

文字列用も同じです。

C#
Holder<string> stringHolder = new Holder<string>();

リファクタリングの流れは、次のように考えるとわかりやすいです。

1つ目は、重複しているクラスを見つけることです。
2つ目は、型だけが違う部分をTに置き換えることです。
3つ目は、使用箇所で具体的な型を指定することです。
4つ目は、コンパイルエラーを確認して修正することです。

この手順で進めると、通常クラスからジェネリッククラスへ無理なく移行できます。

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

最後に、C#のジェネリッククラスについて初心者が疑問に思いやすい点をまとめます。

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

使っても問題ありません。

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

型パラメーター名は自由に付けられます。ただし、C#では慣習としてTから始まる名前を使うことが多いです。

よく使われる名前には、次のようなものがあります。

C#
T
TItem
TKey
TValue
TResult
TEntity

読みやすさを考えるなら、型の役割がわかる名前を付けるのがおすすめです。

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

11-2. ジェネリッククラスは継承できる?

ジェネリッククラスも通常のクラスと同じように継承できます。

C#
public class BaseRepository<T>
{
public void Add(T item)
{
Console.WriteLine("追加しました");
}
}

このクラスを継承できます。

C#
public class UserRepository : BaseRepository<User>
{
}

また、派生クラスもジェネリックにできます。

C#
public class CustomRepository<T> : BaseRepository<T>
{
}

このように、ジェネリッククラスは継承とも組み合わせて使えます。

11-3. ジェネリッククラスでインターフェイスを実装できる?

できます。

たとえば、次のようなインターフェイスを定義します。

C#
public interface IRepository<T>
{
void Add(T item);
List<T> GetAll();
}

このインターフェイスをジェネリッククラスで実装できます。

C#
public class Repository<T> : IRepository<T>
{
private readonly List<T> _items = new List<T>();

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

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

使い方です。

C#
IRepository<User> repository = new Repository<User>();
repository.Add(new User { Id = 1, Name = "Alice" });

ジェネリックインターフェイスとジェネリッククラスを組み合わせると、柔軟で拡張しやすい設計ができます。

11-4. ジェネリッククラスと抽象クラスは併用できる?

併用できます。

抽象クラスをジェネリックにすることもできます。

C#
public abstract class BaseService<T>
{
public abstract void Execute(T item);

public void Log(T item)
{
Console.WriteLine(item);
}
}

この抽象クラスを継承します。

C#
public class UserService : BaseService<User>
{
public override void Execute(User item)
{
Console.WriteLine(item.Name);
}
}

ジェネリック抽象クラスを使うと、共通処理を基底クラスにまとめつつ、具体的な処理は派生クラスに任せられます。

ただし、初心者のうちは少し難しく感じるかもしれません。まずは通常のジェネリッククラスを理解してから、抽象クラスとの組み合わせを学ぶとよいでしょう。

11-5. 初心者はどこまで理解すればよい?

初心者は、まず次のポイントを理解できれば十分です。

ジェネリッククラスは、型を後から指定できるクラスです。

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

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

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

Tは、指定した型に置き換わる仮の型です。

また、List<T>Dictionary<TKey, TValue>もジェネリッククラスです。

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

最初から型制約や高度な設計まで完璧に理解する必要はありません。

まずは、Box<T>のような簡単なクラスを自分で作り、intstringを指定して動かしてみることが大切です。

まとめ

C#のジェネリッククラスは、型を後から指定できるクラスです。

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

このように定義しておけば、使うときにintstringなどの型を指定できます。

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

ジェネリッククラスを使うメリットは、型安全にコードを書けること、同じ処理を複数の型で使い回せること、キャストミスや実行時エラーを減らせることです。

object型を使って共通化する方法もありますが、型安全性が弱くなり、キャストが必要になります。ジェネリッククラスを使えば、柔軟性と安全性を両立できます。

また、List<T>Dictionary<TKey, TValue>Queue<T>Stack<T>など、C#の標準ライブラリにも多くのジェネリッククラスがあります。これらを理解するうえでも、自作のジェネリッククラスを学ぶことは重要です。

初心者は、まず次の3つを押さえましょう。

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

Tはあとから指定する型を表すこと。
Box<int>のように使うとTintになること。
Box<string>のように使うとTstringになること。

この基本が理解できれば、C#のジェネリッククラスは決して難しくありません。

同じ処理で型だけが違うコードを見つけたら、ジェネリッククラスで共通化できないか考えてみましょう。型安全で使い回しやすいコードを書けるようになり、C#の理解も一段深まります。