C#8.0本质论第十二章--泛型

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接口约束

规定某个数据类型必须实现某个接口。

public class BinaryTree<T> where T : System.IComparable<T>
{
}
12.3.2类型参数约束

有时要求将类型实参转换为特定的类型。

public class EntityDictionary<TKey, TValue> : System.Collections.Generic.Dictionary<TKey, TValue>
    where TKey : notnull
    where TValue : EntityBase
{
    // ...
}

假如同时指定了多个约束,那么类类型约束必须第一个出现。和接口约束不同的是不允许多个类类型约束。

12.3.3非托管约束

从C#8.0开始,结构也可以是泛型的。

12.3.4非空约束

notnull即非空约束,不能和struct和class约束共用。如上面12.3.2的代码。

12.3.5struct/class约束

将类型参数限制为任何非可空值类型或任何引用类型。

12.3.6多个约束

如果有多个类型参数,每个类型参数前面都要使用where关键字。两个where子句之间并不存在逗号。

public class EntityDictionary<TKey, TValue>
    : System.Collections.Generic.Dictionary<TKey, TValue>
    where TKey : notnull
    where 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< X >向I< Y >的转换称为协变转换

为什么不合法:

// ...
// 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< X >和I< Y >类型总是具有相反的特殊关系–也就是说I< Y >类型的每个值都能转换成I< X >类型–就说“I< T >对T逆变”。

12.5.3数组对不安全协变性的支持

12.6泛型的内部机制

事实上,泛型类的“类型参数”成了元数据,“运行时“在需要时会利用它们构造恰当的类。为避免装箱,对于基于值的类型参数,其泛型实现和引用类型参数的泛型实现是不同的。

用值类型作为类型参数首次构造一个泛型类型时,”运行时“会将指定的类型参数放到CIL中合适的位置,从而创建一个具体化的泛型类型。每当代码用到时,都重用已经生成的具体化的类。

对于引用类型,泛型的工作方式稍有不同。使用引用类型作为类型参数首次构造一个泛型类型时,”运行时“会在CIL代码中用object引用替换类型参数来创建一个具体的泛型类型(而不是基于所提供的类型实参)。以后,每次引用类型参数实例化一个构造好的类型,”运行时“都重用之前生成好的泛型类型的版本–即使提供的引用类型与第一次不同。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值