首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

C#高级语法之泛型、泛型约束,类型安全、逆变和协变

(给DotNet加星标,提升.Net技能)

转自:也难熬

cnblogs.com/ckka/p/11395777.html

一、为什么使用泛型?

泛型其实就是一个不确定的类型,可以用在类和方法上,泛型在声明期间没有明确的定义类型,编译完成之后会生成一个占位符,只有在调用者调用时,传入指定的类型,才会用确切的类型将占位符替换掉。

首先我们要明白,泛型是泛型,集合是集合,泛型集合就是带泛型的集合。下面我们来模仿这List集合看一下下面这个例子:

我们的目的是要写一个可以存放任何动物的集合,首先抽象出一个动物类:

然后创建Dog类和Pig类

因为我们的目的是存放所有的动物,然后我们来写一个AnimalHouse用来存放所有动物:

AnimalHouse类型可以存放所有的动物,但是每次存入子类对象的时候就会进行装箱操作,每次取出的话,还要再次进行拆箱操作,会消耗额外的性能,因为所有的子类都能存放,所以拆箱的话也会很麻烦。

如果我们有方法可以做到,让调用者来决定添加什么类型(具体的类型,例如Dog、Pig),然后我们创建什么类型,是不是这些问题就不存在了?泛型就可以做到。

我们看一下泛型是如何定义的:

其中CName和MName是可变的类型(名字也是可变的),用法的话就和类型用法一样,用的时候就把它当成具体的类型来用。

了解过泛型,接下来我们使用泛型把AnimalHouse类更改一下,将所有类型Animal更改为泛型,如下:

AnimalHouse类型想要存储什么样的动物,就可以完全交由调用者来决定:

调用方法的时候,原本写的是T类型,当声明的时候传入具体的类型之后,类中所有的T都会变成具体的类型,例如Dog类型,Pig类型

这样我们的问题就解决了,当调用者传入什么类型,我们就构造什么类型的集合来存放动物。

但是还有一个问题,就是调用者也可以不传入动物,调用者可以传入一个桌子(Desk类)、电脑(Computer),但是这些都不是我们想要的。

比如我们需要调用动物的CanRun方法,让动物跑一下再放入集合里(z),因为我们知道动物都是继承自Animal类,所有动物都会有CanRun方法,但是如果传入过来一个飞Desk类我们还能使用CanRun方法吗?答案是未知的,所以为了确保安全,我们需要对传入的类型进行约束。

二、泛型约束

泛型约束就是对泛型(传入的类型)进行约束,约束就是指定该类型必须满足某些特定的特征,例如:可以被实例化、比如实现Animal类等等

我们来看一下官方文档上都有那些泛型约束:

对多个参数应用约束:

使用的话只需要在泛型后面添加 where 泛型 : 泛型约束1、泛型约束2....,如果有new()约束的话则必须放在最后,说明都有很详细的介绍。

然后我们来为AnimalHouse添加泛型约束为:必须包含公共无参构造函数和基类必须是Animal

然后调用的时候我们传入Object试一下

提示Object类型不能传入AnimalHouse中,因为无法转换为Animal类型。

我们在写一个继承Animal类的Tiger子类,然后私有化构造函数

然后创建AnimalHouse类型对象,传入Tiger类试一下:

提示必须是公共无参的非抽象类型构造函数。现在我们的AnimalHouse类就很完善了,可以存入所有的动物,而且只能存入动物

三、逆变和协变

先来看一个问题

这样写编译是不会报错的,因为Dog继承了Animal,默认会进行一个隐式转换,但是下面这样写

这样写的话会报一个无法转换类型的错误。

强转的话,会转换失败,我们设个断点在后一句,然后监视一下animalHouse的值,可以看到值为null

协变就是为了解决这一问题的,这样做其实也是为了解决类型安全问题(百度百科):例如类型安全代码不能从其他对象的私有字段读取值。它只从定义完善的允许方式访问类型才能读取。

因为协变只能用在接口或者委托类型中,所以我们将AnimalHouse抽象抽来一个空接口IAnimalHouse,然后实现该接口:

协变是在T泛型前使用out关键字,其他不需要做修改

接下来我们用接口来调用一下,现在一切ok了,编译也可以通过

协变的作用就是可以将子类泛型隐式转换为父类泛型,而逆变就是将父类泛型隐式转换为子类泛型

将接口类型改为使用in关键字

逆变就完成了:

逆变和协变还有两点:协变时泛型无法作为参数、逆变时泛型无法作为返回值。

逆变:

协变:

语法都是一些 非常粗糙的东西,重要的是思想、思想、思想。然后我们来看一下为什么要有逆变和协变?

什么叫做类型安全?C#中的类型安全个人理解大致就是:一个对象向父类转换时,会隐式安全的转换,而两种不确定可以成功转换的类型(父类转子类),转换时必须显式转换。解决了类型安全大致就是,这两种类型一定可以转换成功。(如果有错误,欢迎指正)。

协变的话我相信应该很好理解,将子类转换为父类,兼容性好,解决了类型安全(因为子类转父类是肯定可以转换成功的);而协变作为返回值是百分百的类型安全

“逆变为什么又是解决了类型安全呢?子类转父类也安全吗?不是有可能存在失败吗?”

其实逆变的内部也是实现子类转换为父类,所以说也是安全的。

“可是我明明看到的是IAnimalHouse dogHouse = animalHouse;将父类对象赋值给了子类,你还想骗人?”

这样写确实是将父类转换为子类,不过逆变是用在作为参数传递的。这是因为写代码的“视角”原因,为什么协变这么好理解,因为子类转换父类很明显可一看出来“IAnimalHouse animalHouse = dogHouse;”,然后我们换个“视角”,将逆变作为参数传递一下,看这个例子:

先将IAnimalHouse接口修改一下:

然后我们在主类(Main函数所在的类)中添加一个TestIn方法来说明为什么逆变是安全的:

接下来我们将“视角”切到TestIn中,作为第一视角,我们正在写这个方法,至于其他人如何调用我们都是不得而知的

我们就随便在当前方法中添加一个操作:为dog变量添加一个Dog对象,TestIn方法改为如下:

我们将“视角”调用者视角,如果我们想调用当前方法,只有两种方法:

第一种的话我们就不看了,很正常也很合理,我们主要来看第二种,那第二种类型安全又在哪儿呢?

可能有人已经反应过来了,我们再来看一下TestIn方法,有一个需要传递过来的IAnimalHouse类型的dog对象,如果调用者是使用第二种方法调用的,那这个所谓的IAnimalHouse类型的dog对象是不是其实就是AnimalHouse类型的对象?而dog.AddAnimal(参数类型);的参数类型是不是就是需要一个Animal类型的对象?那传入一个Dog类型的d对象是不是最终也是转换为Animal类型放入dog对象中?所以当逆变作为参数传递时,类型是安全的。

思考:那么,现在你能明白上面那个错误,为什么“协变时泛型无法作为参数、逆变时泛型无法作为返回值”了吗?

如果我哪点讲的有误或者那点不是太明白都可以留言指正或提问。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20190825A0838200?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券