类型参数的约束(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. 有关可为空的值类型的详细信息,请参阅可为空的值类型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. 参数可以是 C# 8.0 或更高版本中的不可为 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 类型参数必须是非托管类型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 提供的参数或派生自为 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. struct 约束包含 new() 约束,且 new() 约束不能与 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. unmanaged 约束不能与 structnew() 约束结合使用。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. 下面的代码说明了这一点;即使 String 类重载 == 运算符,输出也为 false。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:

  • 不能使用 !=== 运算符,因为无法保证具体的类型参数能支持这些运算符。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. notnull 约束只能在 nullable enable 上下文中使用。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 约束,那么在 nullable enable 上下文中编译该代码时,编译器会生成警告。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. firsttest 均为委托类型,但它们是不同的委托类型。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