C#8.0本质论第十二章–泛型
C#通过泛型来促进代码重用,在词义上等价于C++模板。
在泛型编程中,数据类型也是一种参数。
12.1如果C#没有泛型
为object的方法使用值类型时,“运行时”将自动对它进行装箱,获取值类型的实例时则需要显式拆箱。
12.2捕捉异常
C#的设计者采用了与C++模板相似的语法。所以C#泛型类和结构要求开发者声明泛型类型参数以及提供泛型类型实参。
12.2.1使用泛型类
12.2.2定义简单泛型类
12.2.3泛型的优点
泛型促进了类型安全。
编译时类型检查减少了在运行时发生InvalidCastException异常的概率。
为泛型类成员使用值类型,不再造成到object的装箱转换。
C#泛型缓解了代码膨胀。
性能得以提升.
内存消耗减少,由于避免了装箱,因此减少了在堆上的内存消耗。
代码可读性更好。
12.2.4类型参数命名规范
为了强调类型参数,名称应包含T前缀。
12.2.5泛型接口和结构
12.2.6定义构造函数和终结器
泛型类或结构的构造函数(和终结器)不要求类型参数。
12.2.7用default操作符指定默认值
假定有一个结构体类型Pair的构造函数,实例化时只对数的一半进行初始化,字段Second处于未初始化的状态,会造成编译错误,我们无法给Second设置初始值,因为在编写构造函数时还不知道它的类型T具体是什么。
为应对这样的局面,C#提供了default操作符。
public struct Pair<T> : IPair<T>
{public Pair(T first){First = first;Second = default;// ...}
}
12.2.8多个类型参数
类型参数的数量(或称为元数,即arity)区分了同名类,仅元数不同的泛型应放到同一个C#文件中。
方法可以通过“参数数组”获取任意数量的实参,但泛型类型不可以。每个泛型类型的元数都必须固定。
12.2.9嵌套泛型类型
避免在嵌套类型中用同名参数隐藏外层类型的类型参数。
12.3约束
为避免异常,而是生成编译时的错误,C#允许为泛型类中声明的每个类型参数提供可选的约束列表。需要使用where关键字,后跟一对“参数:要求”
12.3.1接口约束
规定某个数据类型必须实现某个接口。
12.3.2类型参数约束
有时要求将类型实参转换为特定的类型。
假如同时指定了多个约束,那么类类型约束必须第一个出现。和接口约束不同的是不允许多个类类型约束。
12.3.3非托管约束
从C#8.0开始,结构也可以是泛型的。
12.3.4非空约束
notnull即非空约束,不能和struct和class约束共用。
12.3.5struct/class约束
将类型参数限制为任何非可空值类型或任何引用类型。
12.3.6多个约束
如果有多个类型参数,每个类型参数前面都要使用where关键字。两个where子句之间并不存在逗号。
public class EntityDictionary<TKey, TValue>: System.Collections.Generic.Dictionary<TKey, TValue>where TKey : notnullwhere TValue : EntityBase
{// ...
}
12.3.7构造函数约束
并非所有对象都肯定有公共默认构造函数,所以编译器不允许为未约束的类型参数调用默认构造函数。要在其他所有约束之后添加new()。只能对默认构造函数进行约束。
12.3.8约束继承
无论泛型类型参数,还是它们的约束,都不会被派生类继承,因为泛型类型参数不是成员。
12.4泛型方法
12.4.1泛型方法类型推断
未避免多余的编码,当编译器可以逻辑推断出想要的类型参数时,调用时可以不指定类型实参。这称为方法类型推断。
12.4.2指定约束
12.5协变性和逆变性
刚接触泛型类型的人经常问一个问题:为什么不能将List类型的表达式赋给List类型的变量。既然string能转换成object,string列表应该也应兼容于object列表呀?实际情况并非如此,这个赋值动作既不类型安全,也不合法。因为他们不是协变量(covariant).
“协变量”借鉴自范畴论的术语。假定两个类型X和Y具有特殊关系,即每个X类型的值都能转换成Y类型.
使用仅一个参数的泛型类型时,可以简单地说“I是协变的”,从I想I的转换称为协变转换。
为什么不合法:
// ...
// Error: Cannot convert type ...
Pair<PdaItem> pair = (Pair<PdaItem>)new Pair<Contact>();
IPair<PdaItem> duple = (IPair<PdaItem>)new Pair<Contact>();
// ...
Contact contact1 = new("Princess Buttercup");
Contact contact2 = new("Inigo Montoya");
Pair<Contact> contacts = new(contact1, contact2);// This gives an error: Cannot convert type ...
// But suppose it did not
IPair<PdaItem> pdaPair = (IPair<PdaItem>) contacts;
// This is perfectly legal, but not type-safe
pdaPair.First = new Address("123 Sesame Street");
先在应该很清楚为什么字符串列表不能作为对象列表使用了。在字符串列表中不能插入整数,但在对象列表中可以。
12.5.1使用out类型参数修饰符允许协变性
从C#4开始加入了对安全协变性的支持。要指出泛型接口应该对它的某个类型参数协变,就用out修饰符来修饰改类型参数。
用out修饰IReadOnlyPair接口的类型参数,会导致编译器验证T是否真的只用作“输出”,且永远不用于形参或属性的赋值方法。
协变转换有一些重要限制:
只有泛型接口和泛型委托才能协变。泛型类和结构永远不是协变的。
提供给“来源”和“目标”泛型类型的类型实参必须是引用类型,不能是值类型。
接口或委托必须声明为支持协变,编译器必须验证协变所针对的类型参数确实只用在“输出”位置。
12.5.2使用in类型参数修饰符允许协变性
协变性的反方向称为逆变性(contravariance)。假定X和Y类型彼此相关,每个X类型的值都能转换成Y类型。如果I和I类型总是具有相反的特殊关系–也就是说I类型的每个值都能转换成I类型–就说“I对T逆变”。
12.5.3数组对不安全协变性的支持
12.6泛型的内部机制
事实上,泛型类的“类型参数”成了元数据,“运行时“在需要时会利用它们构造恰当的类。为避免装箱,对于基于值的类型参数,其泛型实现和引用类型参数的泛型实现是不同的。
用值类型作为类型参数首次构造一个泛型类型时,”运行时“会将指定的类型参数放到CIL中合适的位置,从而创建一个具体化的泛型类型。每当代码用到时,都重用已经生成的具体化的类。
对于引用类型,泛型的工作方式稍有不同。使用引用类型作为类型参数首次构造一个泛型类型时,”运行时“会在CIL代码中用object引用替换类型参数来创建一个具体的泛型类型(而不是基于所提供的类型实参)。以后,每次引用类型参数实例化一个构造好的类型,”运行时“都重用之前生成好的泛型类型的版本–即使提供的引用类型与第一次不同。