C#におけるジェネリックの基礎と応用

ジェネリックの概要

ジェネリックは、異なるデータ型に対して共通のコード構造を提供する機能です。これにより、型安全性を保ちながら再利用可能なコンポーネントを作成できます。

public class StackContainer<T>
{
    private int position = 0;
    private T[] elements;
    
    public void AddElement(T item) { ... }
    public T RemoveElement() { ... }
}

ジェネリッククラスの定義と利用

ジェネリッククラスはテンプレートとして機能し、実際の型指定時に具象クラスが生成されます。

クラス定義の構文

型パラメータは山括弧で囲み、複数の場合はカンマ区切りで指定します。これらのパラメータはクラス内部で型として使用できます。

public class DataHolder<X, Y>
{
    public X FirstValue;
    public Y SecondValue;
}

具象型のインスタンス化

DataHolder<short, int> instance = new DataHolder<short, int>();
var autoInstance = new DataHolder<short, int>();

同じジェネリック定義から複数の異なる型を持つクラスを生成でき、それぞれが独立した型として扱われます。

var firstInstance = new DataHolder<short, int>();
var secondInstance = new DataHolder<int, long>();

実装例

public class GenericStack<ElementType>
{
    private ElementType[] storage;
    private int index = 0;
    private const int Capacity = 10;
    
    private bool IsFull => index == Capacity;
    private bool IsEmpty => index == 0;
    
    public GenericStack()
    {
        storage = new ElementType[Capacity];
    }
    
    public void Push(ElementType element)
    {
        if (!IsFull)
            storage[index++] = element;
    }
    
    public ElementType Pop()
    {
        if (!IsEmpty)
            return storage[--index];
        return default(ElementType);
    }
    
    public void Display()
    {
        for (int i = index - 1; i >= 0; i--)
            Console.WriteLine(storage[i]);
    }
}
static void Main()
{
    var integerStack = new GenericStack<int>();
    var textStack = new GenericStack<string>();
    
    for (int i = 1; i <= 5; i++)
        integerStack.Push(i * 2);
    integerStack.Display();
    
    textStack.Push("Hello World");
    textStack.Push("C# Generics");
    textStack.Display();
}

ジェネリックの利点

  • コードの重複を避け、メンテナンス性を向上
  • 実行時効率が良く、型安全性を確保
  • ソースコード量を削減可能

型制約の設定

型パラメータに対して制約を設けることで、使用可能な型を限定できます。

制約の必要性

ジェネリックはすべての型を受け入れますが、特定のメンバーにアクセスするには制約が必要です。

class ComparisonHelper<T>
{
    static public bool Compare(T a, T b)
    {
        return a < b;  // コンパイルエラー
    }
}

Where句の使用

whereキーワードを使用して制約を指定します。各型パラメータごとにwhere句を記述可能です。

class Example<T1, T2, T3> where T2 : Constraint1, Constraint2 
                          where T3 : Constraint3, Constraint4

制約の種類と順序

制約タイプ 説明
クラス名 指定クラスまたはそのサブクラスのみ許可
class 参照型(クラス、配列、デリゲート、インターフェース)を許可
struct 値型のみ許可
インターフェース名 指定インターフェースまたはその実装クラスのみ許可
new() 引数なしパブリックコンストラクタを持つ型を許可

制約の記述順序は以下の通り:

where T : プライマリ制約(0-1個), セカンダリ制約(0個以上), コンストラクタ制約(0-1個)
class TestClass<T1, T2> where T1 : IComparable<T1> { }
class DictionaryImplementation<KeyType, ValueType> 
    where KeyType : IEnumerable<KeyType>, new() { }

ジェネリックメソッド

メソッド定義

ジェネリックメソッドは型パラメータリストとオプションの制約を持ちます。

public void ProcessItems<S, T>(S item1, T item2) where S : BaseType

呼び出し方

static void ExecuteOperation<T1, T2>(T1 param1, T2 param2)
{
    T1 local1 = param1;
    T2 local2 = param2;
    Console.WriteLine($"param1:{param1}, param2:{param2}");
}
static void Main()
{
    short val1 = 10;
    int val2 = 20;
    ExecuteOperation<short, int>(val1, val2);
}

型推論

引数から型を推論できる場合、明示的な型指定は不要です。

static void Main()
{
    short val1 = 10;
    int val2 = 20;
    ExecuteOperation(val1, val2); // 型推論により簡略化
}

実装例

class Utility
{
    public static void ReverseDisplay<T>(T[] array)
    {
        Array.Reverse(array);
        foreach (var element in array)
            Console.Write($"{element}, ");
        Console.WriteLine();
    }
}
static void Main()
{
    var numbers = new int[] { 1, 3, 5, 7 };
    var texts = new string[] { "alpha", "beta", "gamma" };
    var decimals = new double[] { 1.23, 4.56, 7.89 };
    
    Utility.ReverseDisplay(numbers);
    Utility.ReverseDisplay(texts);
    Utility.ReverseDisplay(decimals);
}

拡張メソッドとジェネリック

拡張メソッドにより、既存のジェネリッククラスに新しい機能を追加できます。

class Container<T>
{
    private T[] values = new T[3];
    public Container(T v0, T v1, T v2)
    {
        values[0] = v0;
        values[1] = v1;
        values[2] = v2;
    }
    public T[] GetItems() => values;
}
static class ContainerExtensions
{
    public static void Show<T>(this Container<T> container)
    {
        var items = container.GetItems();
        Console.WriteLine($"{items[0]} | {items[1]} | {items[2]}");
    }
}
static void Main()
{
    var intContainer = new Container<int>(1, 3, 5);
    var stringContainer = new Container<string>("x", "y", "z");
    intContainer.Show();
    stringContainer.Show();
}

ジェネリック構造体

構造体もジェネリックとして定義可能で、クラスと同様のルールが適用されます。

struct DataItem<T>
{
    public T Value { get; set; }
    public DataItem(T data) => Value = data;
}
static void Main()
{
    var intItem = new DataItem<int>(100);
    var stringItem = new DataItem<string>("test");
    
    Console.WriteLine(intItem.Value);
    Console.WriteLine(stringItem.Value);
}

ジェネリックデリゲート

デリゲートもジェネリックとして定義でき、型パラメータは戻り値や引数に使用できます。

delegate TResult OperationDelegate<TInput, TResult>(TInput input);

例1

public delegate void StringProcessor<T>(T value);
class TextHandler
{
    public static void PrintText(string text) => Console.WriteLine(text);
    public static void PrintUppercase(string text) => Console.WriteLine(text.ToUpper());
}
static void Main()
{
    StringProcessor<string> processor = TextHandler.PrintText;
    processor += TextHandler.PrintUppercase;
    processor("hello world");
}

例2

public delegate TR CalculationDelegate<T1, T2, TR>(T1 p1, T2 p2);
class Calculator
{
    public static string AddNumbers(int p1, int p2)
    {
        int sum = p1 + p2;
        return sum.ToString();
    }
}
static void Main()
{
    CalculationDelegate<int, int, string> calc = Calculator.AddNumbers;
    Console.WriteLine(calc(10, 20));
}

ジェネリックインターフェース

インターフェースのメンバーでジェネリック型パラメータを使用できます。

interface IDataProcessor<T>
{
    T Process(T input);
}
class DataHandler<S> : IDataProcessor<S>
{
    public S Process(S input) => input;
}
static void Main()
{
    var intHandler = new DataHandler<int>();
    var stringHandler = new DataHandler<string>();
    
    Console.WriteLine(intHandler.Process(42));
    Console.WriteLine(stringHandler.Process("generic"));
}

非ジェネリッククラスでの複数インターフェース実装

interface IProcessor<T>
{
    T Handle(T value);
}
class MultiHandler : IProcessor<int>, IProcessor<string>
{
    public int Handle(int value) => value;
    public string Handle(string value) => value;
}
static void Main()
{
    var handler = new MultiHandler();
    Console.WriteLine(handler.Handle(123));
    Console.WriteLine(handler.Handle("test"));
}

共変性と反変性

共変性

派生型を基底型として扱える特性です。outキーワードで指定します。

class BaseClass {}
class DerivedClass : BaseClass {}
static void Main()
{
    BaseClass baseObj = new BaseClass();
    BaseClass derivedAsBase = new DerivedClass();
}

class Animal {}
class Cat : Animal {}
delegate T Creator<out T>();
static Cat CreateCat() => new Cat();
static void Main()
{
    Creator<Cat> catFactory = CreateCat;
    Creator<Animal> animalFactory = catFactory; // 共変性により可能
}

反変性

基底型を派生型として扱える特性です。inキーワードで指定します。

delegate void ActionHandler<in T>(T parameter);
class Mammal
{
    public int Legs = 4;
}
class Lion : Mammal {}
static void ProcessMammal(Mammal m)
{
    Console.WriteLine(m.Legs);
}
static void Main()
{
    ActionHandler<Mammal> mammalHandler = ProcessMammal;
    ActionHandler<Lion> lionHandler = mammalHandler; // 反変性により可能
    lionHandler(new Lion());
}

インターフェースでの共変性・反変性

interface IRetriever<out T>
{
    T Get();
}
class SimpleRetriever<T> : IRetriever<T>
{
    public T[] items = new T[2];
    public T Get() => items[0];
}
class Creature
{
    public string Name;
}
class Dog : Creature {}
static void DisplayName(IRetriever<Creature> retriever)
{
    Console.WriteLine(retriever.Get().Name);
}
static void Main()
{
    var dogRetriever = new SimpleRetriever<Dog>();
    dogRetriever.items[0] = new Dog { Name = "Rex" };
    
    IRetriever<Creature> creatureRetriever = dogRetriever;
    DisplayName(creatureRetriever);
}

型変換の自動処理

delegate T FactoryMethod<out T>();
static Dog GenerateDog() => new Dog();
static void Main()
{
    FactoryMethod<Creature> creatureFactory1 = GenerateDog; // 暗黙的変換
    FactoryMethod<Dog> dogFactory = GenerateDog;
    FactoryMethod<Creature> creatureFactory2 = dogFactory; // out指定が必要
}

注意事項

  • 変性は参照型にのみ適用される
  • in/outキーワードはデリゲートとインターフェースでのみ有効
  • キーワードなしの場合は不変となる
delegate T ComplexDelegate<out R, in S, T>();
// R: 共変, S: 反変, T: 不変

タグ: csharp generics programming-concepts type-safety

6月15日 21:46 投稿