C#高级语法之泛型、泛型约束,类型安全、逆变和协变(思想原理)

一、为什么使用泛型?

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

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

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

//动物类
public class Animal
{
//随便定义出一个属性和方法
public String SkinColor { get; set; }//皮肤颜色
//会跑的方法
public virtual void CanRun()
{
Console.WriteLine("Animal Run Can");
}
}

然后创建Dog类和Pig类

//动物子类 Dog
public class Dog : Animal
{
//重写父类方法
public override void CanRun()
{
Console.WriteLine("Dog Can Run");
}
}

//动物子类 Pig
public class Pig : Animal
{
//重写父类方法
public override void CanRun()
{
Console.WriteLine("Pig Can Run");
}
}

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

//存放所有动物
public class AnimalHouse
{
//由于自己写线性表需要考虑很多东西,而且我们是要讲泛型的,所以内部就用List来实现
private List<Animal> animal = new List<Animal>();

//添加方法
public void AddAnimal(Animal a)
{
animal.Add(a);
}
//移除方法,并返回是否成功
public bool RemoveAnimal(Animal a)
{
return animal.Remove(a);
}

}

AnimalHouse类型可以存放所有的动物,存放起来很方便。但是每次取出的话,使用起来会很不方便,因为只能用一些动物的特征,而无法使用子类的特征,例如Dog子类有CanSwim()方法(会游泳的方法),而动物中是没有这个方法的,所以就无法进行调用,必须将Animal类型转换为Dog类型才可以使用,不仅会增加额外的开销而且还有很大的不确定性,可能转换失败,因为AnimalHouse中是存放了很多种动物子类。

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

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

//用在类中
public class ClassName<CName>
{
//用在方法中
public void Mothed<MName>() {

}

//泛型类中具体使用CName
//返回值为CName并且接受一个类型为CName类型的对象
public CName GetC(CName c) {
//default关键字的作用就是返回类型的默认值
return default(CName);
}
}

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

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

public class AnimalHouse<T>
{
private List<T> animal = new List<T>();

public void AddAnimal(T a)
{
animal.Add(a);
}
public bool RemoveAnimal(T a)
{
return animal.Remove(a);
}

}

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

//声明存放所有Dog类型的集合
AnimalHouse<Dog> dog = new AnimalHouse<Dog>();
//声明存放所有Pig类型的集合
AnimalHouse<Pig> pig = new AnimalHouse<Pig>();

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

640?wx_fmt=png

 640?wx_fmt=png

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

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

二、泛型约束

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

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

约束说明
where T : struct类型参数必须是值类型。 可以指定除 Nullable<T> 以外的任何值类型。 有关可以为 null 的类型的详细信息,请参阅可以为 null 的类型。
where T : class类型参数必须是引用类型。 此约束还应用于任何类、接口、委托或数组类型。
where T : unmanaged类型参数必须是非托管类型。
where T : new()类型参数必须具有公共无参数构造函数。 与其他约束一起使用时,new() 约束必须最后指定。
where T : <基类名>类型参数必须是指定的基类或派生自指定的基类。
where T : <接口名称>类型参数必须是指定的接口或实现指定的接口。 可指定多个接口约束。 约束接口也可以是泛型。
where T : U为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。

对多个参数应用约束:

//微软官方例子
class Base { }
class Test<T, U>
where U : struct
where T : Base, new()
{ }

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

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

//Animal约束T必须是Animal的子类或者本身,new()约束放在最后
public class AnimalHouse<T> where T : Animal, new()
{
private List<T> animal = new List<T>();

public void AddAnimal(T a)
{
//调用CanRun方法
//如果不加Animal泛型约束是无法调用.CanRun方法的,因为类型是不确定的
a.CanRun();
//添加
animal.Add(a);
}
public bool RemoveAnimal(T a)
{
return animal.Remove(a);
}

}

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

640?wx_fmt=png

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

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

//动物子类 Tiger
public class Tiger : Animal
{
//私有化构造函数
private Tiger()
{

}
public override void CanRun()
{
Console.WriteLine("Tiger Can Run");
}
}

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

640?wx_fmt=png

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

三、逆变和协变

先来看一个问题

Dog dog = new Dog();
Animal animal = dog;

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

AnimalHouse<Dog> dogHouse = new AnimalHouse<Dog>();
AnimalHouse<Animal> animalHouse = dogHouse;

640?wx_fmt=png

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

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

//强转编译会通过,强转的话会转换失败,值为null
IAnimalHouse<Animal> animalHouse = dogHouse as IAnimalHouse<Animal>;

640?wx_fmt=png

640?wx_fmt=png

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

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

//动物房子接口(所有动物的房子必须继承该接口,例如红砖动物房子,别墅动物房)
public interface IAnimalHouse<T> where T : Animal,new()
{

}
//实现IAnimalHouse接口
public class AnimalHouse<T> : IAnimalHouse<T> where T : Animal,new()
{
private List<T> animal = new List<T>();

public void AddAnimal(T a)
{
a.CanRun();
animal.Add(a);
}
public bool RemoveAnimal(T a)
{
return animal.Remove(a);
}
}

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

public interface IAnimalHouse<out T> where T : Animal,new()
{

}

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

IAnimalHouse<Dog> dogHouse = new AnimalHouse<Dog>();
IAnimalHouse<Animal> animalHouse = dogHouse;

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

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

public interface IAnimalHouse<in T> where T : Animal,new()
{

}

逆变就完成了:

IAnimalHouse<Animal> animalHouse = new AnimalHouse<Animal>();
IAnimalHouse<Dog> dogHouse = animalHouse;

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

逆变:

640?wx_fmt=png

协变:

640?wx_fmt=png

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

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

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

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

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

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

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

先将IAnimalHouse接口修改一下:

public interface IAnimalHouse<in T> where T : Animal,new()
{
//添加方法
void AddAnimal(T a);
//移除方法
bool RemoveAnimal(T a);
}

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

//需要一个IAnimalHouse<Dog>类型的参数
public void TestIn(IAnimalHouse<Dog> dog) {

}

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

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

//需要一个IAnimalHouse<Dog>类型的参数
public static void TestIn(IAnimalHouse<Dog> dog) {
Dog d = new Dog();
dog.AddAnimal(d);
}

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

//第一种
AnimalHouse<Dog> dogHouse = new AnimalHouse<Dog>();
TestIn(dogHouse);
//第二种
AnimalHouse<Animal> animalHouse = new AnimalHouse<Animal>();
//因为使用了in关键字所以可以传入父类对象
TestIn(animalHouse);

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

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

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

逆变思考答案,建议自己认真思考过后再看

640?wx_fmt=png

协变思考答案,建议自己认真思考过后再看

640?wx_fmt=png

原文链接:https://www.cnblogs.com/ckka/p/11395777.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com 

640?wx_fmt=jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/314202.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mysql-5.7.10-winx64 MySQL服务无法启动,服务没有报告任何错误的解决办法

总结报错原因&#xff1a; 在my.init文件下新增data目录&#xff08;datadir F:\mysqldata &#xff09; 最新解压版本的mysql 解压安装的时候报错 D:\mysql\mysql-5.7.10-winx64\bin>net start mysql MySQL 服务正在启动 …. MySQL 服务无法启动。 服务没有报告任何…

F - Sugoroku2(动态规划)

F - Sugoroku2 一个经典的概率期望dp的模型&#xff0c;现在要求从0移动到n&#xff0c;每次等概率移动1到m的距离&#xff0c;有k个点&#xff0c;一旦到达就移动回到0&#xff0c;一旦到达n或超过n游戏结束&#xff0c;求解步数期望。 那么我们dp的时候可以发现每一个值会有…

威佐夫博弈及其拓展

威佐夫博弈 普通威佐夫博弈&#xff1a; 两种操作&#xff1a;一、同时在两堆上取相同的个数。二、在某一堆上取任意个数。&#xff08;每次取不为0&#xff09; a[n]nα,b[n]a[n]n,α152a[n] n \alpha, b[n] a[n] n,\alpha \frac{1 \sqrt5}{2}a[n]nα,b[n]a[n]n,α215​…

E - Rotate and Flip(转化一般性)

E - Rotate and Flip 对于有n个点&#xff0c;m个变换&#xff08;包括旋转90度和关于某条直线对称&#xff09;&#xff0c;q次询问ai点在bi个变换后的坐标。 显然对于这些变换我们都是能够直接利用公式求解的&#xff0c;所以我们直接用未知数表示变换后的坐标即可&#xf…

谈自由,ASP.NET Core才是未来?

首先我要说一下自己对自由的理解&#xff1a;自由是我可以选择不干什么&#xff0c;但我要保留我可以干什么的可能性。比如说我现在只有一个码农的角色&#xff0c;但我仍然要保留可以扮演其他角色的可能&#xff0c;比如成为一个作者&#xff0c;当我写下文章的时候已经是了&a…

Spring Boot 学习之旅

1. Spring Boot默认读取的application.properties有点坑&#xff0c;并没有主动去掉每一行后边的空格&#xff0c;如 encoding.spring.thymeleaf.encodingUTF-8 就识别成了UTF-8空格&#xff0c;所以导致查找编码格式的时候报错。

Min_25筛有关求解次小质因子

#188. 【UR #13】Sanrd 题意化简就是求次小质因子&#xff0c;这一步我们可以在Min_25筛的ans计算中得到&#xff0c; S(n, j)表示的是最小质因子大于等于primejprime_jprimej​的加上质数的答案贡献&#xff0c; 要满足次小质因子&#xff0c;一定有除去这个数之后只剩下质…

今天,全网曝光这几个公众号

有人统计过&#xff0c;我们平均每天花在看内容上的时间是5-6小时与其每天被各种看过就忘的内容占据时间不如看点真正对你有价值的信息下面小编为你推荐几个高价值的公众号&#xff0c;这些公众号都是专注.NET技术它们提供的信息能真正提高你生活的质量当你迷茫的时候刷刷这些大…

dp套dp(动态规划)

dp套dp 这是一个对于一类动态规划的计数问题的处理方法&#xff0c;问题常常是如果形式确定就可以直接dp&#xff0c;但是现在却要求满足某个要求的所有方案数&#xff0c;一般的处理方法就是一维负责增量构造&#xff0c;其他维度用来表示内部dp状态&#xff0c;然后转移时候…

Redis学习之Docker环境搭建

最近想学习下Redis&#xff0c;想在本机部署redis集群&#xff0c;发现redis对windows支持不太友好&#xff0c;因此想着安装linux虚拟机&#xff0c;部署一个redis集群&#xff0c;供学习用。 首先想到的是linux虚拟机使用起来太麻烦&#xff0c;想用之前用过的vagrant来简化虚…

.NET Core 小程序开发零基础系列(1)——开发者启用并牵手成功

最近几个月本人与团队一直与小程序打交道&#xff0c;对小程序的实战开发算比较熟悉&#xff0c;也因一些朋友经常问我各种小程序问题&#xff0c;无不能一一回答&#xff0c;想了很久&#xff0c;决定还是空余时间来写写文章吧&#xff0c;偶尔发现一个人安静的时候写文章特爽…

Tarjan缩点/边双/点双

文章目录代码实现实际应用1.有向图另外&#xff1a;对于缩点之后的DAG的处理2.无向图求法细节细节&#xff1a;目录&#xff1a;1.「POJ 3694」Network2.「2019 ICPC 横滨站」3. P3225 [HNOI2012]矿场搭建4. 一本通 分离的路径代码实现 所以其实就三个玩意 1.dfn[],low[],indx…

「LibreOJ Round #11」Misaka Network 与求和(杜教筛 + Min_25)

#572. 「LibreOJ Round #11」Misaka Network 与求和 推式子 ∑i1n∑j1nf(gcd(i,j))k∑d1nf(d)k∑i1nd∑j1nd[gcd(i,j)1]∑d1nf(d)k∑K1ndμ(k)(nKd)2tKd∑t1n(nt)2∑d∣tf(d)kμ(td)我们记f(x)kF(x)上面式子后半部分是一个迪利克雷卷积形式:F∗μ所以我们卷上一个I&#xff0c…

学习笔记之12个月提升计划

Java世界博大精深&#xff0c;有太多的东西要学。如果一头扎进去&#xff0c;很可能会淹没在Java技术的海洋里。于是&#xff0c;最近一直在思考列一个提纲&#xff0c;作为高级工程师到资深、再到架构之路的路标。 学习笔记一栏&#xff0c;即为本计划的博客记录。将自己的计划…

从“梁漱溟:思考问题有八层境界”所联想到的

最近一段时间以来写的文章比较少了&#xff0c;这固然是有一些客观原因&#xff0c;但确实有我不可说的一些自我反省和认识等主观因素。记得8月初有一次友人聚餐&#xff0c;席间有朋友聊到公众号的运营心得体会&#xff0c;其中有一条是&#xff1a;避免粉丝减少的黄金法则之一…

P4126 [AHOI2009]最小割(网络流/最小割)

P4126 [AHOI2009]最小割 https://www.cnblogs.com/dugudashen/p/6228304.html 求解一张有向图中关于最小割的可行边和必须边&#xff0c;可行边定义为存在一种最小割包含这条边&#xff0c;必须边定义为任意一种最小割包含这条边。 可行边的条件&#xff1a; 满流对于边<…

vagrant 环境配置

vagrant是简便虚拟机操作的一个软件&#xff0c;而使用虚拟机有几个好处&#xff1a; 1、为了开发环境与生产环境一致&#xff08;很多开发环境为windows而生产环境为linux&#xff09;&#xff0c;不至于出现在开发环境正常而移步到正式生产环境时出现各种问题&#xff0c;而…

与Min_25筛有关的一些模板

模板 求∑i1nf(i),f(pk)pk(pk−1)\sum \limits_{i 1} ^{n} f(i), f(p ^ k) p ^ k \times(p ^ k - 1)i1∑n​f(i),f(pk)pk(pk−1)&#xff0c;最后对mod1e97\bmod 1e9 7mod1e97&#xff0c;这个函数是个积性函数。 /*Author : lifehappy */ #pragma GCC optimize(2) #pragm…

P4897 【模板】最小割树(Gomory-Hu Tree)(网络流/最小割/树形结构)

P4897 【模板】最小割树&#xff08;Gomory-Hu Tree&#xff09; 这个算法可以用来求解一个无向图上任意两点的最小割&#xff0c;具体过程就是每次选择两个点求最小割&#xff0c;然后在一个新图中这两个点连边&#xff0c;然后对于这两个点的连通块分别递归处理&#xff0c;…

程序员过关斩将--cookie和session的关系其实很简单

喜欢就点关注吧!月高风下&#xff0c;下班路上....菜菜哥&#xff0c;告诉你一个秘密&#xff0c;但是不允许告诉任何人这么秘密&#xff0c;你有男票了&#xff1f;~不是&#xff0c;昨天我偷偷去面试了&#xff0c;结果挂了这不是好事吗&#xff0c;上天让公司留住你.....好吧…