ここでは、C#におけるジェネリックの導入背景、基本構文、制約の指定方法、および実用的な利用例について詳しく解説します。
-
ジェネリックの必要性 引数の型のみが異なるが、処理內容が同一の複数のメソッドを記述する場合、重複コードが増加します。たとえば、int型・double型・float型の配列に対してソート処理を行う場合、型以外はロジックが完全に同じです。このようなケースで、型パラメータによって共通の処理を抽象化できるのがジェネリックです。
-
ジェネリックの概要 ジェネリックは、型をパラメータとして定義された再利用可能なコードを実現します。例えば、
Stack<T>というジェネリッククラスを定義しておけば、Stack<int>やStack<string>など、具体的な型でインスタンス化できます。これにより、実行時の型キャストやボクシング/アンボクシングのオーバーヘッドを回避し、型安全性とパフォーマンスを両立できます。 -
実装例と動作確認
3.1 ジェネリッククラス:スタック実装
<div>
<pre><code>using System;
public class Stack<T>
{
private T[] elements;
private int topIndex = -1;
public Stack(int capacity)
{
elements = new T[capacity];
}
public void Push(T item)
{
if (topIndex >= elements.Length - 1)
throw new InvalidOperationException("Stack is full.");
elements[++topIndex] = item;
}
public T Pop()
{
if (topIndex < 0)
throw new InvalidOperationException("Stack is empty.");
return elements[topIndex--];
}
public void ShowAll()
{
Console.WriteLine("Stack contents:");
for (int i = 0; i <= topIndex; i++)
{
Console.WriteLine(elements[i]);
}
}
}</code></pre>
</div>
<div>
<pre><code>class Program
{
static void Main()
{
// 整数用スタック
var intStack = new Stack<int>(3);
intStack.Push(10);
intStack.Push(20);
intStack.Push(30);
intStack.ShowAll();
Console.WriteLine($"Popped: {intStack.Pop()}");
Console.WriteLine($"Popped: {intStack.Pop()}");
// 文字列用スタック
var stringStack = new Stack<string>(2);
stringStack.Push("One");
stringStack.Push("Two");
stringStack.ShowAll();
}
}</code></pre>
</div>
3.2 ジェネリックメソッド:汎用検索関数
<div>
<pre><code>public static int LocateIndex<T>(T[] source, T target)
where T : IEquatable<T>
{
for (int i = 0; i < source.Length; i++)
{
if (source[i].Equals(target))
return i;
}
return -1;
}</code></pre>
</div>
<div>
<pre><code>// 使用例
int targetInt = 4;
int intIndex = LocateIndex(new int[] { 1, 2, 3, 4, 5 }, targetInt);
Console.WriteLine($"int result: {intIndex}");
float targetFloat = 3.0f;
int floatIndex = LocateIndex(new float[] { 1.0f, 2.0f, 3.0f, 4.0f }, targetFloat);
Console.WriteLine($"float result: {floatIndex}");</code></pre>
</div>
- 制約(Constraints)
型パラメータに適用可能な型を制限することで、より安全かつ柔軟なジェネリック定義を実現します。制約は
where句で指定し、複数指定可能です。
基底部制約:型パラメータは特定の基底クラスを継承することを義務付けます。例:where T : BaseClass
インターフェース制約:型パラメータは指定されたインターフェースを実装することを要求します。例:where T : IList
デフォルトコンストラクタ制約:型パラメータはパブリックな引数なしコンストラクタを持つことが条件になります。例:where T : new()
値型/参照型制約:
- 値型限定:
where T : struct - 参照型限定:
where T : class
複数制約を利用可能で、たとえば以下のように記述できます:
<div>
<pre><code>public class Processor<T>
where T : IComparable, new()
{ ... }</code></pre>
</div>
- ジェネリックデリゲート:Action と Func
Action<T>とFunc<T>は、LINQなどの高階関数処理で頻出する定義済みデリゲートです。
Action:戻り値のないメソッドをラップ。 Func:戻り値のあるメソッドをラップ(第n引数まで入力、最後の型が戻り値)。
<div>
<pre><code>// Action 使用例
Action<string> print = message => Console.WriteLine(message);
print("Hello, Generic World!");
// Func 使用例
Func<int, int, int> add = (x, y) => x + y;
int sum = add(7, 5);
Console.WriteLine($"Sum: {sum}");</code></pre>
</div>
ラムダ式と組み合わせると、コードが簡潔かつ可読性高く保たれます。これは、メソッドをデータと同様に「変数として扱う」デリゲート概念の核となる手法です。