探索 C# 10 的泛型特性
特性在 C# 里一直是地位很高的语法机制。它提供了一种高配版的注释信息,写入到元数据里,并且一直长期存在。和注释不同,特性可使用反射直接获取内容,而注释不参与编译因此无法从代码机制来获取。
C# 10 推广了特性,使得特性可以用泛型。
基本语法
我们使用同样的泛型语法修饰和使用到特性类型上去:
就可以了。
在使用的时候:
即直接使用尖括号即可。和之前的用法一样,无参可以省略小括号,特性后缀 Attribute 可以省略。
支持的泛型类型
既然都说到了泛型特性,那么我们就得说一下规范用法,以及 C# 允许的泛型特性用法。首先是泛型的实体类型的支持。
首先给出结论。

这个表看起来有点大,是因为有很多细节要说的。我们把所有不可以作为泛型参数的类型都说一下,具体是为什么。
指针类型:之所以指针类型不可以,是因为泛型类型参数本身的限制。泛型类型参数在提供给代码和 IDE 服务的时候,在无泛型类型参数的约束的时候,默认是走
object
里作为数据信息显示的。这就是为什么你在对一个没有任何约束的泛型类型参数为类型的对象使用.ToString
会直接给你显示object.ToString
的原因;而众所周知,指针是不走任何数据类型派生的特殊数据类型,因此指针类型在 C# 是不能作为泛型类型参数的。动态类型:
dynamic
类型是一种特殊的类型,它可以在你任何时候使用的时候都不产生报错,以此方式来简化反射调用。可问题就在于它自身的类型是不确定的,dynamic
这个类型只是美其名曰给了一个标记,以便书写代码,实际上在使用的时候,它什么类型都可以充当。因此,特性里记录元数据信息的时候不能记录一个可变数据,因此动态类型不被允许。引用结构:
ref struct
修饰的引用结构类型在 C# 7 里诞生出来就不被允许作为泛型类型参数,因为它仅放在栈内存里。而普通的结构是可以通过装箱进入堆内存的,因此为了优化内存使用和优化性能,这种数据类型是不可以任何形式进入堆内存的。而刚才说过,泛型类型参数在没有任何约束的时候是走object
类型的,而object
是引用类型,因此不被允许。可空引用类型:C# 8 的可空引用类型允许用户对引用类型使用可空记号
?
来表明它的可空性。但是,在底层代码分析上,这个记号是不存在的,它们被转换为了特性标记存储到了元数据里。换言之,如果我们允许可空引用类型作为泛型类型参数的话,此时记录元数据的时候就必须有办法去区分类型T?
和T
。而实际上这是做不到的。所以,可空引用类型是不允许的。平台敏感整数类型:C# 9 有两个类型
nint
和nuint
用于向下兼容 C/C++ 里的int
和unsigned
类型,用于简化和更加便利去使用平台调用(P/Invoke)代码,更好体现可交互性。问题在于,nint
和nuint
并不是真实存在的数据类型,在底层也都被翻译和转换为了IntPtr
和UIntPtr
类型了,因此实际上它们并不是真实存在的数据类型,所以是不被允许作为泛型类型参数的,因为自身在运行时就不存在,就谈不上什么元数据存储了。开放的泛型类型:这种开放的泛型类型,在 C# 里目前只有唯一一个语法可以使用:
typeof
表达式。你可以使用typeof(List<>)
来获取一个带一个泛型类型参数的List<T>
类型的类型本身信息。但是,你无法使用List<> l = new List<>();
之类的语法来创建没有给出明确类型的开放泛型类型。所以,C# 机制本身也就限制了无法把这个玩意儿作为泛型类型参数里:泛型类型参数目前没有闭合,因此是可变的。泛型参数:泛型参数是可变的,和前文的开放泛型类型是一样的道理,因此不允许。
可空泛型类型(有值类型或引用类型约束):同上。约束只是限制它在哪个范围里取值,但没有限制泛型类型本身的唯一性。
可空泛型类型(没有任何约束):同上。
可把泛型类型用于泛型特性的任何一个地方
比如,你可以这么写代码:
P
特性接收的是 T
类型的参数,并且赋值给了 T
类型的 Element