C# ジェネリック制約(where 句)

ジェネリック制約(where 句)を使うと、型パラメータに条件を設けることができます。制約を付けることで、特定のメソッドやプロパティが使えることを保証し、より安全なコードが書けます。

制約がないと何が問題か

制約のない型パラメータ T は、どんな型でも受け入れます。そのため、T に対して使えるメソッドは object のものだけです。

// 制約なし:T のメソッドを呼べない
public void Process<T>(T item)
{
    // item.SomeMethod(); // エラー:T に SomeMethod があるとは限らない
    Console.WriteLine(item?.ToString()); // object のメソッドなら OK
}

where 句で制約を追加

where 句を使うと、型パラメータに条件を付けられます。

public interface IDisplayable
{
    string GetDisplay();
}

// T は IDisplayable を実装している型に限定
public void ShowDisplay<T>(T item) where T : IDisplayable
{
    Console.WriteLine(item.GetDisplay()); // GetDisplay を呼べる
}

制約の種類

C# では様々な制約を指定できます。

where T : class

T は参照型でなければならない

where T : struct

T は値型(null 非許容)でなければならない

where T : new()

T は引数なしのコンストラクタを持たなければならない

where T : BaseClass

T は指定したクラスまたはその派生クラスでなければならない

where T : IInterface

T は指定したインターフェースを実装していなければならない

複数の制約を組み合わせる

一つの型パラメータに複数の制約を付けることもできます。

public class Repository<T> where T : class, IEntity, new()
{
    public T Create()
    {
        return new T(); // new() 制約があるので可能
    }

    public void Save(T entity)
    {
        Console.WriteLine($"ID: {entity.Id}"); // IEntity のメンバーにアクセス可能
    }
}

public interface IEntity
{
    int Id { get; }
}

複数の型パラメータへの制約

複数の型パラメータがある場合、それぞれに制約を付けられます。

public class Mapper<TSource, TDestination>
    where TSource : class
    where TDestination : class, new()
{
    public TDestination Map(TSource source)
    {
        var dest = new TDestination();
        // マッピング処理
        return dest;
    }
}

new() 制約の活用

new() 制約を使うと、ジェネリックメソッド内でインスタンスを生成できます。

public static T CreateInstance<T>() where T : new()
{
    return new T();
}

var list = CreateInstance<List<int>>();
var dict = CreateInstance<Dictionary<string, int>>();
制約なし

object のメンバーしか使えない。型の性質を仮定できない。

制約あり

指定した型やインターフェースのメンバーが使える。意図が明確。

制約を適切に使うことで、ジェネリクスの柔軟性と型安全性を両立できます。コンパイル時に不正な型の使用を検出できるため、バグの早期発見にも役立ちます。