型パラメーターの制約 (C# プログラミング ガイド)

制約では、型引数に必要な機能をコンパイラに通知します。 制約のない型引数は、任意の型にすることができます。 コンパイラは、.NET 型の最終的な基底クラスになる、System.Object のメンバーを見なすことができるだけです。 詳細については、「制約を使用する理由」を参照してください。 クライアント コードが制約を満たさない型を使用している場合、コンパイラではエラーが発行されます。 制約を指定するには、where コンテキスト キーワードを使用します。 次の表は、さまざまな制約の種類の一覧です。

制約 説明
where T : struct この型引数は null 非許容値型である必要があります。 null 許容値型の詳細については、「null 許容値型」を参照してください。 すべての値の型にはアクセス可能なパラメーターなしのコンストラクターがあるため、struct 制約は new() 制約を意味し、new() 制約と組み合わせることはできません。 struct 制約を unmanaged 制約と組み合わせることはできません。
where T : class この型引数は参照型である必要があります。 この制約は、任意のクラス、インターフェイス、デリゲート、または配列型にも適用されます。 C# 8.0 以降の null 許容コンテキストでは、T は null 非許容の参照型である必要があります。
where T : class? 型の引数は、null 許容または null 非許容の参照型である必要があります。 この制約は、任意のクラス、インターフェイス、デリゲート、または配列型にも適用されます。
where T : notnull この型引数は null 非許容型である必要があります。 引数は、C# 8.0 以降の null 非許容参照型、または null 非許容値型にできます。
where T : default この制約により、メソッドをオーバーライドするとき、または明示的なインターフェイスの実装を提供するときに、制約のない型パラメーターを指定する必要がある場合の、曖昧さが解決されます。 default 制約は、class または struct 制約のない基本メソッドを意味します。 詳細については、 制約の仕様の提案を参照してください。
where T : unmanaged この型引数は null 非許容でアンマネージド型である必要があります。 unmanaged 制約は struct 制約を意味し、struct 制約とも new() 制約とも組み合わせることはできません。
where T : new() この型引数には、パラメーターなしのパブリック コンストラクターが必要です。 new() 制約を別の制約と併用する場合、この制約を最後に指定する必要があります。 new() 制約は、structunmanaged 制約と組み合わせることはできません。
where T :where T : この型引数は、指定された基底クラスであるか、そのクラスから派生している必要があります。 C# 8.0 以降の null 許容コンテキストでは、T は指定の基底クラスから派生した null 非許容の参照型である必要があります。
where T :where T : この型引数は、指定された基底クラスであるか、そのクラスから派生している必要があります。 C# 8.0 以降の null 許容コンテキストでは、T は指定の基底クラスから派生した null 許容または非許容のいずれかの参照型である場合があります。
where T :where T : この型引数は、指定されたインターフェイスであるか、そのインターフェイスを実装している必要があります。 複数のインターフェイス制約を指定することができます。 制約のインターフェイスを汎用的にすることもできます。 C# 8.0 以降の null 許容コンテキストでは、T は指定したインターフェイスを実装する null 非許容型である必要があります。
where T :where T : この型引数は、指定されたインターフェイスであるか、そのインターフェイスを実装している必要があります。 複数のインターフェイス制約を指定することができます。 制約のインターフェイスを汎用的にすることもできます。 C# 8.0 の null 許容コンテキストでは、T は null 許容参照型、null 非許容参照型、または値型である場合があります。 T は null 許容値型ではない可能性があります。
where T : U T に指定する型引数は、U に指定された引数であるか、その引数から派生している必要があります。 null 許容コンテキストでは、U が null 非許容参照型である場合、T は null 非許容参照型である必要があります。 U が null 許容参照型である場合、T は null 許容または null 非許容のいずれかになります。

制約を使用する理由

制約では、型パラメーターの能力と期待を指定します。 これらの制約を宣言することで、制約型の操作とメソッドの呼び出しを使用できるようになります。 ジェネリック クラスまたはメソッドで、単純な割り当てや、System.Object でサポートされていない任意のメソッド呼び出しでジェネリック メンバーに対して任意の操作を使用する場合は、型パラメーターに制約を適用します。 たとえば、この基底クラスの制約は、この型のオブジェクト、またはこの型から派生したオブジェクトのみを型引数として使用することをコンパイラに指示しています。 コンパイラがこの保証を獲得したら、その型のメソッドをジェネリック クラスで呼び出すことができるようになります。 基底クラスの制約を適用して GenericList<T> クラス (「GenericList<T>」を参照) に追加できる機能を説明するコード例を次に示します。

public class Employee
{
    public Employee(string name, int id) => (Name, ID) = (name, id);
    public string Name { get; set; }
    public int ID { get; set; }
}

public class GenericList<T> where T : Employee
{
    private class Node
    {
        public Node(T t) => (Next, Data) = (null, t);

        public Node? Next { get; set; }
        public T Data { get; set; }
    }

    private Node? head;

    public void AddHead(T t)
    {
        Node n = new Node(t) { Next = head };
        head = n;
    }

    public IEnumerator<T> GetEnumerator()
    {
        Node? current = head;

        while (current != null)
        {
            yield return current.Data;
            current = current.Next;
        }
    }

    public T? FindFirstOccurrence(string s)
    {
        Node? current = head;
        T? t = null;

        while (current != null)
        {
            //The constraint enables access to the Name property.
            if (current.Data.Name == s)
            {
                t = current.Data;
                break;
            }
            else
            {
                current = current.Next;
            }
        }
        return t;
    }
}

この制約ではジェネリック クラスで Employee.Name プロパティを使用できるようにします。 制約では、型 T のすべての項目が、Employee オブジェクトまたは Employee から継承するオブジェクトのいずれかになることが保証されることを指定します。

同じ型パラメーターに複数の制約を適用できます。また、制約自体をジェネリック型にすることもできます。次に例を示します。

class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()
{
    // ...
}

where T : class 制約を適用する場合は、型パラメーターに == および != 演算子を使用しないでください。これらの演算子でテストされるのは、値の等価性ではなく、参照 ID についてのみです。 これらの演算子が、引数として使用されている型内でオーバーロードされている場合でも、この動作が発生します。 この点を説明するコードを次に示します。String クラスが == 演算子をオーバーロードしている場合でも、出力は false です。

public static void OpEqualsTest<T>(T s, T t) where T : class
{
    System.Console.WriteLine(s == t);
}

private static void TestStringEquality()
{
    string s1 = "target";
    System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
    string s2 = sb.ToString();
    OpEqualsTest<string>(s1, s2);
}

コンパイラは T がコンパイル時に参照型であることしか認識しておらず、すべての参照型で有効な既定の演算子を使用する必要があります。 値の等価性をテストする必要がある場合は、where T : IEquatable<T> または where T : IComparable<T> 制約も適用し、ジェネリック クラスの制約に使用されるすべてのクラスでそのインターフェイスを実装することをお勧めします。

複数のパラメーターを制約する

複数のパラメーターに制約を適用できます。また、複数の制約を 1 つのパラメーターに適用することができます。次に例を示します。

class Base { }
class Test<T, U>
    where U : struct
    where T : Base, new()
{ }

非バインド型パラメーター

パブリック クラス SampleClass<T>{} の T など、制約がない型パラメーターは、非バインド型パラメーターと呼ばれます。 非バインド型パラメーターには次の規則があります。

  • != および == 演算子は使用できません。これは、具象型引数によってこれらの演算子がサポートされるという保証がないためです。
  • これらの演算子は System.Object との間で相互に変換できます。また、任意のインターフェイス型に明示的に変換できます。
  • これらは null と比較することができます。 非バインド型パラメーターと null を比較し、その型引数が値の型の場合、比較結果として常に false が返されます。

制約としての型パラメーター

制約としてジェネリック型パラメーターを使用する方法は、独自の型パラメーターがあるメンバー関数が、含まれる型の型パラメーターにそのパラメーターを制約する必要がある場合に便利です。次に例を示します。

public class List<T>
{
    public void Add<U>(List<U> items) where U : T {/*...*/}
}

前の例の T は、Add メソッドのコンテキストでは型の制約ですが、List クラスのコンテキストでは非バインド型パラメーターです。

型パラメーターは、ジェネリック クラス定義の制約としても使用できます。 型パラメーターは、他の型パラメーターと共に山かっこ内で宣言する必要があります。

//Type parameter V is used as a type constraint.
public class SampleClass<T, U, V> where T : V { }

ジェネリック クラスで制約として型パラメーターを使用する方法が便利なのは、限られた場合のみです。コンパイラでは、System.Object から派生したことを除き、型パラメーターに関して何も仮定できないためです。 2 つの型パラメーター間に継承関係を適用するシナリオには、ジェネリック クラスの制約として型パラメーターを使用してください。

notnull 制約

C# 8.0 以降では、notnull 制約を使用して、型引数が null 非許容値型または null 非許容参照型である必要があることを指定できます。 他のほとんどの制約とは異なり、型引数が notnull 制約に違反すると、コンパイラによりエラーではなく警告が生成されます。

notnull 制約は、null 許容コンテキストで使用した場合にのみ有効です。 null 許容が未指定のコンテキストで notnull 制約を追加した場合、コンパイラによって制約違反に関する警告やエラーが生成されません。

class 制約

C# 8.0 以降では、null 許容コンテキスト内の class 制約は、型引数が null 非許容型である必要があることを指定します。 null 許容コンテキストでは、型引数が null 許容参照型である場合、コンパイラによって警告が生成されます。

default 制約

null 許容参照型を追加すると、ジェネリック型またはメソッドでの T? の使用が複雑になります。 C# 8 より前の T? は、struct 制約が T に適用されているときにのみ使用できました。 そのコンテキストでは、T?TNullable<T> 型を参照します。 C# 8 以降の T? は、struct または class のいずれかの制約と共に使用できますが、そのうちの 1 つが存在する必要があります。 class 制約が使用されていると、T?T の null 許容参照型を参照しました。 C# 9 以降の T? は、どちらの制約も適用されていないときに使用できます。 その場合、T? は、値型と参照型について、C# 8 と同じように解釈されます。 ただし、TNullable<T> のインスタンスである場合は、T?T と同じになります。 つまり、T?? にはなりません。

T? は、class または struct の制約なしで使用できるようになったため、オーバーライドまたは明示的なインターフェイスの実装で曖昧さが発生する可能性があります。 どちらの場合も、オーバーライドには制約は含まれませんが、基底クラスから継承されます。 基底クラスで class または struct のいずれの制約も適用されていない場合は、派生クラスで、どちらの制約もない基本メソッドに適用されるオーバーライドを指定する必要があります。 それは、派生メソッドによって default 制約が適用されるときです。 default 制約では、class または struct のどちらの制約も明確に "default"。

アンマネージド制約

C# 7.3 以降、unmanaged 制約を指定して、型パラメーターが null 非許容でunmanagedである必要があることを指定できます。 unmanaged 制約では、次の例のように、メモリのブロックとして操作できる型を処理する再利用可能なルーチンを記述できます。

unsafe public static byte[] ToByteArray<T>(this T argument) where T : unmanaged
{
    var size = sizeof(T);
    var result = new Byte[size];
    Byte* p = (byte*)&argument;
    for (var i = 0; i < size; i++)
        result[i] = *p++;
    return result;
}

ビルトイン型ではない型で sizeof 演算子を使用するため、先行するメソッドは unsafe コンテキストでコンパイルされる必要があります。 unmanaged 制約なしで、sizeof 演算子を使用することはできません。

unmanaged 制約は struct 制約を意味するため、これと組み合わせることはできません。 struct 制約は new() 制約を意味するため、unmanaged 制約を new() 制約と組み合わせることもできません。

制約をデリゲートする

また、C# 7.3 以降、基底クラスの制約として System.Delegate または System.MulticastDelegate を使用することもできます。 CLR では常にこの制約を許可していますが、C# 言語では許可されていません。 System.Delegate 制約では、タイプ セーフな方法でデリゲートを処理するコードを記述できます。 次のコードでは、2 つのデリゲートが同じ型である場合にそれらを組み合わせる拡張メソッドを定義します。

public static TDelegate? TypeSafeCombine<TDelegate>(this TDelegate source, TDelegate target)
    where TDelegate : System.Delegate
    => Delegate.Combine(source, target) as TDelegate;

上述のメソッドを使用して、同じ型のデリゲートを組み合わせることができます。

Action first = () => Console.WriteLine("this");
Action second = () => Console.WriteLine("that");

var combined = first.TypeSafeCombine(second);
combined!();

Func<bool> test = () => true;
// Combine signature ensures combined delegates must
// have the same type.
//var badCombined = first.TypeSafeCombine(test);

最後の行のコメントを解除した場合、コンパイルされません。 firsttest は両方ともデリゲート型ですが、これらは異なるデリゲート型です。

列挙の制約

C# 7.3 以降、基底クラスの制約として System.Enum 型を指定することもできます。 CLR では常にこの制約を許可していますが、C# 言語では許可されていません。 System.Enum を使用するジェネリックは、System.Enum の静的メソッドの使用から結果をキャッシュするために、タイプ セーフのプログラミングを提供します。 次の例では、列挙型の有効な値をすべて見つけて、それらの値をその文字列表記にマップするディクショナリをビルドします。

public static Dictionary<int, string> EnumNamedValues<T>() where T : System.Enum
{
    var result = new Dictionary<int, string>();
    var values = Enum.GetValues(typeof(T));

    foreach (int item in values)
        result.Add(item, Enum.GetName(typeof(T), item)!);
    return result;
}

Enum.GetValuesEnum.GetName ではリフレクションが使用されます。これは、パフォーマンスに影響を与えます。 リフレクションを必要とする呼び出しを繰り返すのではなく、EnumNamedValues を呼び出してキャッシュおよび再利用されるコレクションを作成できます。

次の例で示すように、このメソッドを使用して、列挙を作成し、その値と名前のディクショナリをビルドできます。

enum Rainbow
{
    Red,
    Orange,
    Yellow,
    Green,
    Blue,
    Indigo,
    Violet
}
var map = EnumNamedValues<Rainbow>();

foreach (var pair in map)
    Console.WriteLine($"{pair.Key}:\t{pair.Value}");

関連項目