来源: 五分钟看完,彻底理解协变逆变 – BruceNeter – 博客园
其实这是C#的老知识点了,但是今天发现同事对这个竟然还一知半解,就和他们讲解了下,顺便也回顾了下,同事我也把我对这个的全部理解,融化成几分钟的讲解,保证大家5分钟内全部理解,看不懂来打我。
协变、逆变 解决的问题
泛型类型转换
比如Person类是Student的父类,我们平时可以直接:
Person A = new Student();
这是所谓的隐式转换,相信百分之999.99%的人都知道。
然后随着大家写代码越来越多,就会遇到这样的场景。
//我有一个集合
//我手上有一批学生
IEnumerable<Student> students = new List<Student>();
//我要他们先做人
IEnumerable<Person> peoples = students;
第一次看到这种代码,其实哪怕你一点不知道协变,逆变,你也觉得这是一段正常不过的代码,因为每个学生都是人,都可以直接转成 人这个类型,那我一批学生不就是一批人吗。是的,你这样想绝对没错,不然微软怎么会能让你这样写没问题还编译通过呢?
但是如果我自己写一个:
//定义一个工作的泛型接口
public interface IWork<T>
{
}
实现类
public class Work<T> : IWork<T>
{
}
//直接报错
IWork<Person> work = new Work<Student>();;
现实给了我们当头一棒,这时候,我们应该找到 IEnumerable,选中然后狠狠的F12去看一下,为什么官方的就可以。
我们发现官方在泛型前面多了一个out关键字。破案了~
现在我们在我们的代码中也加入out关键字
public interface IWork<out T>
{
}
public class Work<T> : IWork<T>
{
}
IWork<Person> work = new Work<Student>();
OK~代码正常运行。
原则核心
这里开始我们挑战五分钟速通,如果按照正常博客上来先讲概念,别说五分钟了,可能大家也就迷迷糊糊地看完了,所以我们直接整活。
核心依据
正如数学的发展是从1+1=2作为开始,我们也需要一些真理来支撑我们讲下去。那么我们的核心依据就是:
里氏替换——C#里,子类转父类可以直接隐式转换
就这么短,就完事了?对,记住就行!!!
Out/In 输入输出?
讲到这里,我们继续忽悠,out是啥?来个翻译!不就是输出吗?in是啥,不就是输入吗?那么带入一下,Out不就是返回值吗,In不就是入参吗。那不就是方法的特征么。(先假设,再假设)
In:那么根据核心依据,子类转父类可以直接转,入参如果限定是Person类型,那么你给我限定为Student或者任意的Person类型的派生类,我都是可以接受的,因为都是安全的,可以直接转换过来的。
这种从基类转向派生类的兼容,就是所谓的逆变。
说白了,我让你给我一个人,你说不行,我给你找个学生,那肯定是满足需求的。
Out:Out代表的是返回值,根据核心依据,我返回的是Student类型,你说不行,你给我返回Person类型,那我不是笑开花了,我连Student都能返回,你让我返回父类,那我不是直接转就过去了,总归是类型安全的。
这种从派生类转向基类的兼容,就是所谓的协变。
说白了,我可以造个学生,结果你说给个人就行, 那不是so easy。
In示意图
Out示意图
证明
好了,我们说了这么多,至少证明下In/Out是代表的入参和返回值吧?直接show you code:
当Out作为返回值时的泛型没有问题,但是入参就报错了
当In作为入参时的泛型没有问题,但是返回值就报错了
好了,这还需要再解释吗?最后我们总结下,逆变和协变就是让方法有了泛型类型上的转换能力,强化了方法的多态能力。
问题点
1.属性为啥可以用逆变协变?
属性不就是get/set方法。
2.为什么接口和委托可以用逆变协变,类不行?
拜托你找一下共同点,接口和委托的共同点,都是行为,也就是方法为核心。接口里不能有字段。这也印证了我说的逆变协变最终是为方法服务的。
之所以类不行,我大概理解是方法和实例是分开的,本身不和实例存储在一起,也不是每个实例一份,如果逆变和协变可以服务类,那么会出现同样的类型,但是每个实例内部的同一个字段的类型都不一样,这对于存储和类型安全都是问题。
3.逆变和协变有啥用?
当你…设计问题,我就有遇到,有时候用上能更加优雅或者灵活的写代码吧,看你吧,少年。