型パラメーターの制約 (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() 制約は、struct や unmanaged 制約と組み合わせることはできません。 |
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?
は T
の Nullable<T> 型を参照します。 C# 8 以降の T?
は、struct
または class
のいずれかの制約と共に使用できますが、そのうちの 1 つが存在する必要があります。 class
制約が使用されていると、T?
は T
の null 許容参照型を参照しました。 C# 9 以降の T?
は、どちらの制約も適用されていないときに使用できます。 その場合、T?
は、値型と参照型について、C# 8 と同じように解釈されます。 ただし、T
が Nullable<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);
最後の行のコメントを解除した場合、コンパイルされません。 first
と test
は両方ともデリゲート型ですが、これらは異なるデリゲート型です。
列挙の制約
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.GetValues
と Enum.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}");