型別參數的條件約束 (C# 程式設計手冊)Constraints on type parameters (C# Programming Guide)

條件約束會通知編譯器有關型別引數必須要有的功能。Constraints inform the compiler about the capabilities a type argument must have. 如果沒有任何條件約束,則型別引數可以是任何型別。Without any constraints, the type argument could be any type. 編譯器只能採用 System.Object 成員,這是任何 .NET 型別的最終基底類別。The compiler can only assume the members of System.Object, which is the ultimate base class for any .NET type. 如需詳細資訊,請參閱為什麼使用條件約束For more information, see Why use constraints. 如果用戶端程式碼嘗試使用條件約束不允許的類型來具現化類別,則結果是編譯時間錯誤。If client code tries to instantiate your class by using a type that is not allowed by a constraint, the result is a compile-time error. 條件約束是使用 where 內容關鍵字所指定。Constraints are specified by using the where contextual keyword. 下表列出七種類型的條件約束:The following table lists the seven types of constraints:

條件約束Constraint 描述Description
where T : struct 型別引數必須是實值型別。The type argument must be a value type. 您可以指定 Nullable<T> 以外的任何實值型別。Any value type except Nullable<T> can be specified. 如需可為 null 的實數值型別的詳細資訊,請參閱nullable 實數值型別For more information about nullable value types, see Nullable value types.
where T : class 型別引數必須是參考型別。The type argument must be a reference type. 此條件約束也適用於任何類別、介面、委派或陣列型別。This constraint applies also to any class, interface, delegate, or array type.
where T : notnull 型別引數必須是不可為 null 的型別。The type argument must be a non-nullable type. 引數可以是8.0 或更新版本中C#不可為 null 的參考型別,或不可為 null 的實數值型別。The argument can be a non-nullable reference type in C# 8.0 or later, or a not nullable value type. 此條件約束也適用於任何類別、介面、委派或陣列型別。This constraint applies also to any class, interface, delegate, or array type.
where T : unmanaged 型別引數必須是 unmanaged 型別The type argument must be an unmanaged type.
where T : new() 型別引數必須有公用無參數建構函式。The type argument must have a public parameterless constructor. 與其他條件約束搭配使用時,new() 條件約束必須是最後一個指定的。When used together with other constraints, the new() constraint must be specified last.
where T : <基底類別名稱>where T : <base class name> 型別引數必須是或衍生自指定的基底類別。The type argument must be or derive from the specified base class.
where T : <介面名稱>where T : <interface name> 型別引數必須是或實作指定的介面。The type argument must be or implement the specified interface. 您可以指定多個介面條件約束。Multiple interface constraints can be specified. 條件約束介面也是泛型。The constraining interface can also be generic.
where T : U 針對 T 提供的型別引數必須是或衍生自針對 U 所提供的引數。The type argument supplied for T must be or derive from the argument supplied for U.

有些條件約束會互斥。Some of the constraints are mutually exclusive. 所有實值型別都必須有可存取的無參數建構函式。All value types must have an accessible parameterless constructor. @No__t-0 條件約束意指 @no__t 1 條件約束,而 @no__t 2 條件約束無法與 struct 條件約束結合。The struct constraint implies the new() constraint and the new() constraint can't be combined with the struct constraint. unmanaged 條件約束表示 struct 條件約束。The unmanaged constraint implies the struct constraint. @No__t-0 條件約束無法與 struct 或 @no__t 2 條件約束結合。The unmanaged constraint can't be combined with either the struct or new() constraints.

為什麼使用條件約束Why use constraints

透過限制型別參數,即可增加條件約束類型和其繼承階層中所有類型所支援項目的允許作業和方法呼叫數目。By constraining the type parameter, you increase the number of allowable operations and method calls to those supported by the constraining type and all types in its inheritance hierarchy. 當您設計泛型類別或方法時,如果您要在簡單指派以外的泛型成員上執行任何作業,或呼叫 System.Object 不支援的任何方法,則必須將條件約束套用至類型參數。When you design generic classes or methods, if you'll be performing any operation on the generic members beyond simple assignment or calling any methods not supported by System.Object, you'll have to apply constraints to the type parameter. 例如,基底類別條件約束會告知編譯器只有這個類型的物件或衍生自這個類型的物件才會用作型別引數。For example, the base class constraint tells the compiler that only objects of this type or derived from this type will be used as type arguments. 編譯器具有這項保證之後,就可以允許在泛型類別中呼叫該類型的方法。Once the compiler has this guarantee, it can allow methods of that type to be called in the generic class. 下列程式碼範例示範您可以套用基底類別條件約束來新增至 GenericList<T> 類別的功能 (在泛型簡介中)。The following code example demonstrates the functionality you can add to the GenericList<T> class (in Introduction to Generics) by applying a base class constraint.

public class Employee
{
    public Employee(string s, int i) => (Name, ID) = (s, i);
    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 屬性。The constraint enables the generic class to use the Employee.Name property. 條件約束指定 T 型別的所有項目保證都是 Employee 物件或繼承自 Employee 的物件。The constraint specifies that all items of type T are guaranteed to be either an Employee object or an object that inherits from Employee.

多個條件約束可以套用至相同的型別參數,而且條件約束本身可以是泛型型別,如下所示:Multiple constraints can be applied to the same type parameter, and the constraints themselves can be generic types, as follows:

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

套用 where T : class 條件約束時,請避免在型別參數上使用 ==!= 運算子,因為這些運算子只會測試參考識別是否相等,但不會測試值是否相等。When applying the where T : class constraint, avoid the == and != operators on the type parameter because these operators will test for reference identity only, not for value equality. 即使在用作引數的型別中多載這些運算子,也會發生這種行為。This behavior occurs even if these operators are overloaded in a type that is used as an argument. 下列程式碼說明這點;輸出為 false,即使 String 類別多載 == 運算子也是一樣。The following code illustrates this point; the output is false even though the String class overloads the == operator.

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 是在編譯時期的參考型別,而且必須使用對所有參考型別有效的預設運算子。The compiler only knows that T is a reference type at compile time and must use the default operators that are valid for all reference types. 如果您必須測試值是否相等,則建議同時套用 where T : IEquatable<T>where T : IComparable<T> 條件約束,並在任何將用來建構泛型類別的類別中實作介面。If you must test for value equality, the recommended way is to also apply the where T : IEquatable<T> or where T : IComparable<T> constraint and implement the interface in any class that will be used to construct the generic class.

限制多個參數Constraining multiple parameters

您可以將條件約束套用至多個參數,以及將多個條件約束套用至單一參數,如下列範例所示:You can apply constraints to multiple parameters, and multiple constraints to a single parameter, as shown in the following example:

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

未繫結的型別參數Unbounded type parameters

沒有條件約束的型別參數 (例如公用類別 SampleClass<T>{} 中的 T) 稱為「未繫結的型別參數」。Type parameters that have no constraints, such as T in public class SampleClass<T>{}, are called unbounded type parameters. 未繫結的型別參數具有下列規則:Unbounded type parameters have the following rules:

  • 不能使用 != 和 @no__t 1 運算子,因為不保證具體類型引數會支援這些運算子。The != and == operators can't be used because there's no guarantee that the concrete type argument will support these operators.
  • 它們可以與 System.Object 進行來回轉換,或明確轉換成任何介面類型。They can be converted to and from System.Object or explicitly converted to any interface type.
  • 您可以將它們與 Null 比較。You can compare them to null. 如果未繫結的參數與 null 進行比較,則型別引數是實值型別時,比較一律會傳回 false。If an unbounded parameter is compared to null, the comparison will always return false if the type argument is a value type.

作為條件約束的型別參數Type parameters as constraints

具有專屬型別參數的成員函式需要將該參數限制為包含類型的型別參數時,將泛型型別參數用作條件約束十分有用,如下列範例所示:The use of a generic type parameter as a constraint is useful when a member function with its own type parameter has to constrain that parameter to the type parameter of the containing type, as shown in the following example:

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

在上述範例中,TAdd 方法內容中的類型條件約束,以及 List 類別內容中的未繫結型別參數。In the previous example, T is a type constraint in the context of the Add method, and an unbounded type parameter in the context of the List class.

型別參數也可以在泛型類別定義中用作條件約束。Type parameters can also be used as constraints in generic class definitions. 型別參數必須與任何其他型別參數一起宣告於角括弧內:The type parameter must be declared within the angle brackets together with any other type parameters:

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

型別參數作為條件約束對泛型類別來說不實用,因為編譯器除了會假設型別參數衍生自 System.Object 之外,不會再做其他任何假設。The usefulness of type parameters as constraints with generic classes is limited because the compiler can assume nothing about the type parameter except that it derives from System.Object. 如果您要強制兩個型別參數之間具有繼承關係,請在泛型類別上將型別參數用作條件約束。Use type parameters as constraints on generic classes in scenarios in which you want to enforce an inheritance relationship between two type parameters.

NotNull 條件約束NotNull constraint

從C# 8.0 開始,您可以使用 notnull 條件約束來指定類型引數必須是不可為 null 的實數值型別或不可為 null 的參考型別。Beginning with C# 8.0, you can use the notnull constraint to specify that the type argument must be a non-nullable value type or non-nullable reference type. @No__t-0 條件約束只能用在 @no__t 1 的內容中。The notnull constraint can only be used in a nullable enable context. 如果您在可為 null 的遺忘式內容中加入 notnull 條件約束,編譯器會產生警告。The compiler generates a warning if you add the notnull constraint in a nullable oblivious context.

不同于其他條件約束,當類型引數違反 notnull 條件約束時,編譯器會在 @no__t 1 內容中編譯該程式碼時產生警告。Unlike other constraints, when a type argument violates the notnull constraint, the compiler generates a warning when that code is compiled in a nullable enable context. 如果程式碼是在可為 null 的遺忘式內容中編譯,則編譯器不會產生任何警告或錯誤。If the code is compiled in a nullable oblivious context, the compiler doesn't generate any warnings or errors.

非受控條件約束Unmanaged constraint

從 C# 7.3 開始,您可以使用 unmanaged 條件約束指定型別參數必須是非受控型別Beginning with C# 7.3, you can use the unmanaged constraint to specify that the type parameter must be an unmanaged type. unmanaged 條件約束可讓您撰寫可重複使用的常式來使用型別,而型別可以操作為記憶體區塊,如下列範例所示:The unmanaged constraint enables you to write reusable routines to work with types that can be manipulated as blocks of memory, as shown in the following example:

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;
}

上述方法必須在 unsafe 內容中進行編譯,因為它在不知道是內建型別的型別上使用 sizeof 運算子。The preceding method must be compiled in an unsafe context because it uses the sizeof operator on a type not known to be a built-in type. 如果沒有 unmanaged 條件約束,則 sizeof 運算子無法使用。Without the unmanaged constraint, the sizeof operator is unavailable.

委派條件約束Delegate constraints

而且,從 C# 7.3 開始,您可以使用 System.DelegateSystem.MulticastDelegate 作為基底類別條件約束。Also beginning with C# 7.3, you can use System.Delegate or System.MulticastDelegate as a base class constraint. CLR 一律允許這個條件約束,但 C# 語言不允許它。The CLR always allowed this constraint, but the C# language disallowed it. System.Delegate 條件約束可讓您撰寫程式碼,以型別安全方式使用委派。The System.Delegate constraint enables you to write code that works with delegates in a type-safe manner. 下列程式碼會定義擴充方法,以結合兩個委派,前提是它們是相同的類型:The following code defines an extension method that combines two delegates provided they're the same type:

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

您可以使用上述方法來結合型別相同的委派:You can use the above method to combine delegates that are the same type:

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);

如果您將最後一行取消註解,則不會編譯它。If you uncomment the last line, it won't compile. @No__t-0 和 test 都是委派類型,但它們是不同的委派類型。Both first and test are delegate types, but they're different delegate types.

列舉條件約束Enum constraints

從 C# 7.3 開始,您也可以指定 System.Enum 型別作為基底類別條件約束。Beginning in C# 7.3, you can also specify the System.Enum type as a base class constraint. CLR 一律允許這個條件約束,但 C# 語言不允許它。The CLR always allowed this constraint, but the C# language disallowed it. 使用 System.Enum 的泛型提供型別安全程式設計,以快取在 System.Enum 中使用靜態方法的結果。Generics using System.Enum provide type-safe programming to cache results from using the static methods in System.Enum. 下列範例會尋找列舉型別的所有有效值,然後建置將這些值對應至其字串表示法的字典。The following sample finds all the valid values for an enum type, and then builds a dictionary that maps those values to its string representation.

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;
}

使用的方法會利用反映,而這會影響效能。The methods used make use of reflection, which has performance implications. 您可以呼叫這個方法來建置可快取和重複使用的集合,而不是重複需要反映的呼叫。You can call this method to build a collection that is cached and reused rather than repeating the calls that require reflection.

您可以如下列範例所示使用它來建立列舉,並建置其值和名稱的字典:You could use it as shown in the following sample to create an enum and build a dictionary of its values and names:

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}");

另請參閱See also