windows C#-泛型类型参数的约束详解(二)

未绑定的类型参数

没有约束的类型参数(如公共类 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 以外,不会做其他任何假设。 如果要在两个类型参数之间强制继承关系,可以将类型参数用作泛型类的约束。

notnull 约束

可以使用 notnull 约束指定类型参数必须是不可为 null 的值类型或不可为 null 的引用类型。 与大多数其他约束不同,如果类型参数违反 notnull 约束,编译器会生成警告而不是错误。

notnull 约束仅在可为 null 上下文中使用时才有效。 如果在过时的可为 null 上下文中添加 notnull 约束,编译器不会针对违反约束的情况生成任何警告或错误。

class 约束

可为 null 的上下文中的 class 约束指定类型参数必须是不可为 null 的引用类型。 在可为 null 上下文中,当类型参数是可为 null 的引用类型时,编译器会生成警告。

default 约束

添加可为空引用类型会使泛型类型或方法中的 T? 使用复杂化。 T? 可以与 struct 或 class 约束一起使用,但必须存在其中一项。 使用 class 约束时,T? 引用了 T 的可为空引用类型。 可在这两个约束均未应用时使用 T?。 在这种情况下,对于值类型和引用类型,T? 解读为 T?。 但是,如果 T 是 Nullable<T>的实例,则 T? 与 T 相同。 换句话说,它不会成为 T??。

由于现在可在没有 class 或 struct 约束的情况下使用 T?,因此在重写或显式接口实现中可能会出现歧义。 在这两种情况下,重写不包含约束,但从基类继承。 当基类不应用 class 或 struct 约束时,派生类需要通过某种方式在不使用任一种约束的情况下指定应用于基方法的重写。 派生方法应用 default 约束。 default 约束不阐明 class 和 struct 约束。

非托管约束

可使用 unmanaged 约束来指定类型参数必须是不可为 null 的非托管类型。 通过 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;
}

以上方法必须在 unsafe 上下文中编译,因为它并不是在已知的内置类型上使用 sizeof 运算符。 如果没有 unmanaged 约束,则 sizeof 运算符不可用。

unmanaged 约束表示 struct 约束,且不能与其结合使用。 因为 struct 约束表示 new() 约束,且 unmanaged 约束也不能与 new() 约束结合使用。

委托约束

可以使用 System.Delegate 或 System.MulticastDelegate 作为基类约束。 CLR 始终允许此约束,但 C# 语言不允许。 使用 System.Delegate 约束,用户能够以类型安全的方式编写使用委托的代码。 以下代码定义了合并两个同类型委托的扩展方法:

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 均为委托类型,但它们是不同的委托类型。

枚举约束

还可指定 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}");

类型参数实现声明的接口

某些场景要求为类型参数提供的参数实现该接口。 例如:

public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
    static abstract T operator +(T left, T right);
    static abstract T operator -(T left, T right);
}

此模式使 C# 编译器能够确定重载运算符或任何 static virtual 或 static abstract 方法的包含类型。 它提供的语法使得可以在包含类型上定义加法和减法运算符。 如果没有此约束,需要将参数和自变量声明为接口,而不是类型参数:

public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
    static abstract IAdditionSubtraction<T> operator +(
        IAdditionSubtraction<T> left,
        IAdditionSubtraction<T> right);

    static abstract IAdditionSubtraction<T> operator -(
        IAdditionSubtraction<T> left,
        IAdditionSubtraction<T> right);
}

上述语法要求实现者对这些方法使用显式接口实现。 提供额外的约束使接口能够根据类型参数来定义运算符。 实现接口的类型可以隐式实现接口方法。

Allows ref struct

allows ref struct 反约束声明相应的类型参数可以是 ref struct 类型。 该类型参数的实例必须遵循以下规则:

  • 它不能被装箱。
  • 它参与引用安全规则。
  • 不能在不允许 ref struct 类型的地方使用实例,例如 static 字段。
  • 实例可以使用 scoped 修饰符进行标记。

不会继承 allows ref struct 子句。 在以下代码中:

class SomeClass<T, S>
    where T : allows ref struct
    where S : T
{
    // etc
}

S 的参数不能是 ref struct,因为 S 没有 allows ref struct 子句。

具有 allows ref struct 子句的类型参数不能用作类型参数,除非相应的类型参数也具有 allows ref struct 子句。 下面的示例说明了此规则:

public class Allow<T> where T : allows ref struct
{

}

public class Disallow<T>
{
}

public class Example<T> where T : allows ref struct
{
    private Allow<T> fieldOne; // Allowed. T is allowed to be a ref struct

    private Disallow<T> fieldTwo; // Error. T is not allowed to be a ref struct
}

前面的示例表明,对于一个可能是 ref struct 类型的类型参数,不能将其替换为不能是 ref struct 类型的类型参数。

相关推荐
小青柑-10 分钟前
Go语言中的接收器(Receiver)详解
开发语言·后端·golang
豪宇刘32 分钟前
JavaScript 延迟加载的方法
开发语言·javascript
摇光931 小时前
js迭代器模式
开发语言·javascript·迭代器模式
美丽的欣情2 小时前
Qt实现海康OSD拖动Demo
开发语言·qt
C++小厨神2 小时前
Bash语言的计算机基础
开发语言·后端·golang
BinaryBardC2 小时前
Bash语言的软件工程
开发语言·后端·golang
飞yu流星2 小时前
C++ 函数 模板
开发语言·c++·算法
没有名字的鬼2 小时前
C_字符数组存储汉字字符串及其索引
c语言·开发语言·数据结构
专注于开发微信小程序打工人3 小时前
庐山派k230使用串口通信发送数据驱动四个轮子并且实现摄像头画面识别目标检测功能
开发语言·python
土豆凌凌七3 小时前
GO:sync.Map
开发语言·后端·golang