C#-弄懂泛型和协变、逆变
脑图概览
泛型声明和使用
协变和逆变
《C#权威指南》上在委托篇中这样定义:
协变
:委托方法的返回值类型直接或者间接地继承自委托前面的返回值类型;逆变
:委托签名中的参数类型继承自委托方法的参数类型;
在泛型篇中这样定义:
协变
:泛型参数定义的类型只能作为方法的返回类型,不能作为方法的参数类型,且该类型直接或者间接地继承自接口方法的返回值类型;可以使用out关键字声明协变参数。逆变
:泛型参数定义的类型只能作为方法参数的类型,不能作为返回值类型,且该类型是接口方法的参数类型的基类型;可以使用in关键字声明逆变参数
一直没弄懂,或者一时弄懂了也没记住,一定不怪我!看到一个博主这样解释,秒懂:
协变性
:如:string->object (子类到父类的转换)逆变性
:如:object->string (父类到子类的转换)不变性
:
《CLR via C#》 第三版这样定义:
协变量
:意味着泛型类型参数可以从一个派生类更改为它的基类(子类可以转为父类);逆变量
:意味着泛型类型参数可以从一个基类改为该类的派生类(父类可以转为子类);不变量
:意味着泛型参数不能更改;
总结:协变逆变中的协逆是相对于继承关系的继承链方向而言的
《CLR via C#》 底部有注解说:
协变性指定返回类型的兼容性,而逆变性指定参数的兼容性。
基类和子类的转换
面向对象中有一个规则是:子类向上可以转为基类,但是基类不能向下转为子类。
比如子类Student可以通过以下方式转为基类Person:
Person p=new Student();
或者
Student s=new Student();
Person p=s;
但是基类转为子类这样转编译器就会出错:
Person p=new Person();
Student s=p;
或者
Object obj=new Object();
string str=obj;
Func泛型委托的出参只有协变
既然子类可以转为父类,父类不能转为子类,那我这样做行不行?新定义了一个委托,委托返回一个类型
delegate T MyFunc<T>();
void Main()
{
Student s = new Student() { Name = "张三" };
Person p = s;
MyFunc<Student> func1 = () => new Student() { Name = "李四" };
MyFunc<Person> func2 = func1;
Func<Student> func3 = () => new Student() { Name = "王五" };
Func<Person> func4 = func3;
}
public class Person
{
public string Name { get; set; }
}
public class Student : Person
{
public string Number { get; set; }
}
public class Teacher
{
public string TeacherBook { get; set; }
}
这样不行,MyFunc<Person> func2 = func1;
编译器不认编译失败,这句代码却Func<Person> func4 = func3;
正常。
我们知道Student转Person是合法的转换,但是编译器不知道,需要告诉编译器这段转换时合法的,没有必要做强制的类型安全转换。
怎么做?声明类型时指定out关键字,如下:
delegate T MyFunc<out T>();
可以看到编译器通过。out关键字会告诉编译器Student到Person是有效转换。
其实Func委托的定义也是如此:
public delegate TResult Func<out TResult>();//无输入参数,有返回值。
但要是加如下代码呢?
MyFunc<Teacher> func5 = func1;
编译还是会不给过的!因为转换不合法。
那要是 out关键字加在委托的入参类型呢
delegate void MyFunc<out T>(T t);
会告诉你无效
那出参可以协变,入参是否可以协变呢?
完全不可以,就像父类转为子类一样是不合法的
泛型委托Action的入参只有逆变
以下代码可以正常编译为什呢?明明object类型不可以转为string类型
Action<object> action1 = t => { Console.WriteLine(t.GetType()); };
Action<string> action2 = action1;
而反过来却是错的
Action<string> action3 = t => { Console.WriteLine(t.GetType()); };
Action<object> action4 = action3;
看看Action是如何定义的?
public delegate void Action<in T>(T obj);//泛型委托,无返回值
in关键会告诉编译器,要么传递T作为委托的参数类型,要么传递T的派生类型。string是oject的派生类,所有是正常的.
所以这里不能单纯理解为object转为string类型了,而要理解为string可以安全的替换掉object,因为string是object的子类呀,
object有的,string都有,这个转换肯定是安全的。
泛型中的协变和逆变
泛型中的协变和逆变原理与泛型委托一样
out和in总结
out: 输出(作为结果),in:输入(作为参数)
所以如果有一个泛型参数标记为out,则代表它是用来输出的,只能作为结果返回,而如果有一个泛型参数标记为in,则代表它是用来输入的,也就是它只能作为参数。
为什么in只能作为输入参数的逆变?
void Main()
{
var p = new Person();
var s = new Student();
var gs = new GoodStudent();
this.Method(p);//编译出错
this.Method(s);
this.Method(gs);
}
public void Method(Student stu)
{
}
public class Person
{
public string Name { get; set; }
}
public class Student : Person
{
public string Number { get; set; }
}
public class GoodStudent : Student
{
}
方法体形参可以把子类当成父类来用(传递Student还是GoodStudent对象都无所谓),但是不能把父类当成子类来用(传递Person对象就出错了)。【里氏替换原则】
为什么out只能作为返回值的协变?
通常定义一个变量来接受返回值,父类可以接收子类的数据(这里可以理解为object obj=str),子类能接收父类的数据吗(这里理解为string str = (string)objcet)?肯定不是不能。所以只能是协变
相关单词:
- Covariant:协变量
- Contravariant:逆变量
- Covariance:协变性
- Contravariance:逆变性
使用注意
C#4.0之前 IEnumerable<T> 、 IComparable<T> 、 IQueryable<T>
等接口都不支持可变性,在4.0及之后才支持。因为4.0之前定义的泛型接口没有添加out、in关键字,有兴趣可以切换版本看看。
参考
- 景春雷,协变(Covariance)和逆变(Contravariance)的十万个为什么
- 那些年搞不懂的术语、概念:协变、逆变、不变体
- 深入理解 C# 协变和逆变
- Func和Action学习
- 《Visual C# 从入门到精通》 第八版
- 《CLR via C#》 第三版
- 《C# 本质论》 第四版
C#-弄懂泛型和协变、逆变的更多相关文章
- 解读经典《C#高级编程》最全泛型协变逆变解读 页127-131.章4
前言 本篇继续讲解泛型.上一篇讲解了泛型类的定义细节.本篇继续讲解泛型接口. 泛型接口 使用泛型可定义接口,即在接口中定义的方法可以带泛型参数.然后由继承接口的类实现泛型方法.用法和继承泛型类基本没有 ...
- C#中泛型方法与泛型接口 C#泛型接口 List<IAll> arssr = new List<IAll>(); interface IPerson<T> c# List<接口>小技巧 泛型接口协变逆变的几个问题
http://blog.csdn.net/aladdinty/article/details/3486532 using System; using System.Collections.Generi ...
- C#的in/out关键字与协变逆变
C#提供了一组关键字in&out,在泛型接口和泛型委托中,若不使用关键字修饰类型参数T,则该类型参数是不可变的(即不允许协变/逆变转换),若使用in修饰类型参数T,保证"只将T用于输 ...
- 编写高质量代码改善C#程序的157个建议——建议45:为泛型类型参数指定逆变
建议45:为泛型类型参数指定逆变 逆变是指方法的参数可以是委托或者泛型接口的参数类型的基类.FCL4.0中支持逆变的常用委托有: Func<int T,out TResult> Predi ...
- Programming In Scala笔记-第十九章、类型参数,协变逆变,上界下界
本章主要讲Scala中的类型参数化.本章主要分成三个部分,第一部分实现一个函数式队列的数据结构,第二部分实现该结构的内部细节,最后一个部分解释其中的关键知识点.接下来的实例中将该函数式队列命名为Que ...
- java协变逆变,PECS
public static void main(String[] args) { // Object <- Fruit <- Apple <- RedApple System.out ...
- 协变 & 逆变
都跟里氏替换原则有关. 协变:你可以用一个子类对象去替换相应的一个父类对象,这是完全符合里氏替换原则的,和协(谐)的变.如:用Swan替换Bird. 逆变:你可以用一个父类对象去替换相应的一个子类对象 ...
- C#核心语法讲解-泛型(详细讲解泛型方法、泛型类、泛型接口、泛型约束,了解协变逆变)
泛型(generic)是C#语言2.0和通用语言运行时(CLR)的一个新特性.泛型为.NET框架引入了类型参数(type parameters)的概念.类型参数使得设计类和方法时,不必确定一个或多个具 ...
- C#核心语法-泛型(详细讲解泛型方法、泛型类、泛型接口、泛型约束,了解协变逆变)
泛型(generic)是C#语言2.0和通用语言运行时(CLR)的一个新特性.泛型为.NET框架引入了类型参数(type parameters)的概念.类型参数使得设计类和方法时,不必确定一个或多个具 ...
随机推荐
- sql-hive笔试题整理 1 (学生表-成绩表-课程表-教师表)
题记:一直在写各种sql查询语句,最长的有一百多行,自信什么需求都可以接,可......,想了想,可能一直在固定的场景下写,平时也是以满足实际需求为目的,竟不知道应试的题都是怎么出的,又应该怎么做.遂 ...
- php面试专题---MYSQL查询语句优化
php面试专题---MYSQL查询语句优化 一.总结 一句话总结: mysql的性能优化包罗甚广: 索引优化,查询优化,查询缓存,服务器设置优化,操作系统和硬件优化,应用层面优化(web服务器,缓存) ...
- vue-lazyload 图片不更新
前几天在用vue写项目的时候,因为图片比较多,所以采用了懒加载插件 vue-lazyload github:https://github.com/hilongjw/vue-lazyload#readm ...
- day32—CSS多列布局学习
转行学开发,代码100天——2018-04-17 关于多列布局,前期已经梳理过,今天的培训课程学习中再次提及,趁此也做个总结和检验. 多列布局的介绍参考: day08—css布局解决方案之多列布局 ...
- day04—JavaScript之面向对象
转行学开发,代码100天——2018-03-20 对象是变量的容器,是键值对的容器,也是属性和方法的容器. 万物接对象 1.JavaScript中对象定义的方式 方法一:new Object() // ...
- FCKEditor添加字体
默认情况下,FCKEditor在进行文本编辑时,无法使用中文字体.自个摸索了下:打开 fckconfig.js 文件 找到第154行(应该是),会发现:FCKConfig.FontNames = 'A ...
- Python几行代码实现邮件发送
话不多说直接进入正题 首先我们需要安装一个名为'zmail'的包,终端执行'pip install zmail'即可实现安装. 直接上代码 import zmail mail = { 'subject ...
- PHPer面试指南-laravel 篇
简述 Laravel 的生命周期 Laravel 采用了单一入口模式,应用的所有请求入口都是 public/index.php 文件. 注册类文件自动加载器 : Laravel通过 composer ...
- js 函数 写法
// function ckeckName(){}; // function checkUser(){}; // function checkPassWorld(){}; // var checkNa ...
- 诊断:MRP0: Background Media Recovery process shutdown with error ORA-19909
oracle12c data guard,从库无法应用日志,检查alert日至发现 2019-10-21T14:55:40.087819+08:00 MRP0: Background Media Re ...