一、基本概念

协变和逆变是在计算机科学中,描述具有父/子型别关系的多个型别,通过型别构造器、构造出的多个复杂型别之间是否有父/子型别有序或逆序的关系;

官方描述:协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型。以上概念理解起来绕口或看得不明所以,简单点说就是以下意思:

协变:就是对具体成员的输出参数进行一次类型转换,且类型转换的准则是 “里氏替换原则”。例如,如果Cat是Animal的子类型,那么Cat类型的表达式可用于任何出现Animal类型表达式的地方。所谓的变型(variance)是指如何根据组成类型之间的子类型关系,来确定更复杂的类型之间(例如Cat列表之于Animal列表,回传Cat的函数之于回传Animal的函数...等等)的子类型关系。当我们用类型构造出更复杂的类型,原本类型的子类型性质可能被保持、反转、或忽略───取决于类型构造器的变型性质。

逆变:是对具体成员的输入参数进行一次类型转换,且类型转换的准则是"里氏替换原则"。在使用委托时,因为委托方法签名参数比方法参数更具体,因此可以在传递给处理程序方法时对它们进行隐式转换。这样,当创建可由大量类使用的更加通用的委托方法时,使用逆变就更为简单了。

二、遇到的情况

在通常情况下,简单的父子类关系根据“里氏替换原则”,父类型(也叫基类型)的变量可以指向其子类对象,进而可以正常调用子类对象的具体实现,实现了面向对象的多态特性,如下代码:

定义的的测试类:

/// <summary>/// 水果类/// </summary>public class Fruit{public string Name { get; set; }}/// <summary>/// 苹果类/// </summary>public class Apple : Fruit{}

通常情况下的父子类的关系及使用:

 Fruit fruit = new Fruit();   //正常Apple apple = new Apple();  //正常Fruit fruit1 = new Apple(); //正常,里氏替换原则//Apple apple1 = new Fruit();  //语法错误,父类型对象不转换为子类

但在某些场景中,我们既需要父子类关系的原本特性被保持,同时又需要更为复杂数据结构特性以满足复杂的业务需求,简单的父子类关系这种形式貌似难以满足或实现,如下:

List<Fruit> fruits = new List<Fruit>();  //正常List<Apple> apples = new List<Apple>();  //正常//List<Fruit> fruits1 = new List<Apple>();  //错误

问题来了:一个Apple是Fruit,难道一堆Apple就不是Fruit了?

此时,协变和逆变就是为了解决此类问题出现的;

  • IEnumerable<Cat>是IEnumerable<Animal>的子类型,因为类型构造器IEnumerable<T>是协变的(covariant)。注意到复杂类型IEnumerable的子类型关系和其接口中的参数类型是一致的,亦即,参数类型之间的子类型关系被保持住了。

  • Action<Cat>是Action<Animal>的超类型,因为类型构造器Action<T>是逆变的(contravariant)。(在此,Action<T>被用来表示一个参数类型为T或sub-T的一级函数)。注意到T的子类型关系在复杂类型Action的封装下是反转的,但是当它被视为函数的参数时其子类型关系是被保持的。

  • IList<Cat>或IList<Animal>彼此之间没有子类型关系。因为IList<T>类型构造器是不变的(invariant),所以参数类型之间的子类型关系被忽略了。

三、协变(Covariance)

1、out关键字(C#中)

对于泛型类型参数,out 关键字可指定类型参数是协变的。可以在泛型接口和委托中使用 out 关键字,如下代码:

/// <summary>/// 协变/// </summary>/// <typeparam name="T"></typeparam>public interface MyList<out T>{T GetName();}/// <summary>/// 实现接口/// </summary>public class MyList: MyList<Apple>{public Apple GetName(){return new Apple();}}

调用:

 //协变MyList<Apple> apple = new MyList();//  fruit和apple指向同一对象:new MyList() MyList<Fruit> fruit= apple; //表面调用的是fruit里面的方法,实际上是new MyList().GetName(),返回的是Apple,//赋值时,此处作了隐式转换(Fruit)new MyList().GetName()Fruit result= fruit.GetName();Console.WriteLine(result.GetType().Name);  //输出 Apple

备注:泛型委托的协变原理也是一样的。

四、逆变(Contravariance)

1、in关键字(C#中)

对于泛型类型参数,in 关键字可指定类型参数是逆变的。可以在泛型接口和委托中使用 in 关键字。

/// <summary>/// 逆变/// </summary>/// <typeparam name="T"></typeparam>public interface Inverter<in T>{void GetName(T args);}/// <summary>/// 逆变接口实现/// </summary>public class Inverter : Inverter<Fruit>{public void GetName(Fruit fruit){Console.WriteLine(fruit.GetType().Name);}}

调用:

 //逆变Inverter<Fruit> fruit = new Inverter();//inApple 和fruit 指向同一对象:new Inverter()Inverter<Apple> inApple = fruit;//实际上调用的是new Inverter().GetName(),传参时,作了隐式转换,fruit.GetName方法中的参数是FruitinApple.GetName(new Apple());  //输AppleConsole.Read();

五、形式定义

在一门程序设计语言的类型系统中,一个类型规则或者类型构造器是:

  • 协变(covariant),如果它保持了子类型序关系≦。该序关系是:子类型≦基类型。

  • 逆变(contravariant),如果它逆转了子类型序关系。

  • 不变(invariant),如果上述两种均不适用。

首先考虑数组类型构造器: 从Animal类型,可以得到Animal[](“animal数组”)。 是否可以把它当作

  • 协变:一个Cat[]也是一个Animal[]

  • 逆变:一个Animal[]也是一个Cat[]

  • 以上二者均不是(不变)?

如果要避免类型错误,且数组支持对其元素的读、写操作,那么只有第3个选择是安全的。Animal[]并不是总能当作Cat[],因为当一个客户读取数组并期望得到一个Cat,但Animal[]中包含的可能是个Dog。所以逆变规则是不安全的。

反之,一个Cat[]也不能被当作一个Animal[]。因为总是可以把一个Dog放到Animal[]中。在协变数组,这就不能保证是安全的,因为背后的存储可以实际是Cat[]。因此协变规则也不是安全的—数组构造器应该是不变。注意,这仅是可写(mutable)数组的问题;对于不可写(只读)数组,协变规则是安全的。

这示例了一般现像。只读数据类型(源)是协变的;只写数据类型(汇/sink)是逆变的。可读可写类型应是“不变”的。

Java与C#中的协变数组

早期版本的Java与C#不包含泛型(generics,即参数化多态)。在这样的设置下,使数组为“不变”将导致许多有用的多态程序被排除。然而,如果数组类型被处理为“不变”,那么它仅能用于确切为Object[]类型的数组。对于字符串数组等就不能做重排操作了。所以,Java与C#把数组类型处理为协变。在C#中,string[]是object[]的子类型,在Java中,String[]是Object[]的子类型。这个方法的缺点是留下了运行时错误的可能,而一个更严格的类型系统本可以在编译时识别出该错误。这个方法还有损性能,因为在运行时要运行额外的类型检查。Java与C#有了泛型后,有了类型安全的编写这种多态函数。数组比较与重排可以给定参数类型,也可以强制C#方法只读方式访问一个集合,可以用界面IEnumerable<object>代替作为数组object[]。

六、一些疑问

1、协变、逆变 为什么只能针对泛型接口或者委托?而不能针对泛型类?

因为它们都只能定义方法成员(接口不能定义字段),而方法成员在创建对象的时候是不涉及到对象内存分配的,所以它们是类型(内存)安全的。

为什么不针对泛型?因为泛型类是模板类,而类成员是包含字段的,不同类型的字段是影响对象内存分配的,没有派生关系的类型它们是不兼容的,也是内存不安全的。

2、协变、逆变 为什么是类型安全的?

本质上是里氏替换原则,由里氏替换原则可知:派生程度小的是派生程度大的子集,所以子类替换父类的位置整个程序功能都不会发生改变。

3、为什么 in 、out 只能是单向的(输入或输出)?

因为若类型参数同时为输入参数和输出参数,则必然会有一种转换不符合里氏替换原则,即将父类型的变量赋值给子类型的变量,这是不安全的所以需要明确指定 in 或 out。

七、总结

1、协变和逆变一般应用于泛型接口和泛型委托中;

2、协变和逆变主要是为了保证某些类型在特殊数据结构中变型时的类型安全;

3、简单类型父类的变量能指向子类B的对象,此时,若IEnumerable<A>类型的变量能指向IEnumerable<B>的对象,为协变;若若IEnumerable<B>类型的变量能指向IEnumerable<A>的对象,为逆变;

4、协变”->”和谐的变”->”很自然的变化”->string->object :协变,类型参数只能作为输类型;“逆变”->”逆常的变”->”不正常的变化”->object->string 逆变,类型参数只能作为输入参数类型。

协变与逆变的简单理解(C#)相关推荐

  1. 对协变和逆变的简单理解

    毕业快一年了,边工作边学习,虽说对.net不算精通,但也算入门了,但一直以来对协变和逆变这个概念不是太了解,上学时候mark了一些文章,今天回过头看感觉更糊涂了,真验证本人一句口头禅"知道的 ...

  2. C# 泛型的协变和逆变

    1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用.如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量.协变和逆变是两个相互对立的概念: 如 ...

  3. Java泛型的协变与逆变

    泛型擦除 Java的泛型本质上不是真正的泛型,而是利用了类型擦除(type erasure),比如下面的代码就会出现错误: 报的错误是:both methods  have same erasure ...

  4. 深入理解 C# 协变和逆变【转】

    msdn 解释如下: "协变"是指能够使用与原始指定的派生类型相比,派生程度更大的类型. "逆变"则是指能够使用派生程度更小的类型. 解释的很正确,大致就是这样 ...

  5. 深入理解 C# 协变和逆变

    msdn 解释如下: "协变"是指能够使用与原始指定的派生类型相比,派生程度更大的类型. "逆变"则是指能够使用派生程度更小的类型. 解释的很正确,大致就是这样 ...

  6. Scala教程之:深入理解协变和逆变

    文章目录 函数的参数和返回值 可变类型的变异 在之前的文章中我们简单的介绍过scala中的协变和逆变,我们使用+ 来表示协变类型:使用-表示逆变类型:非转化类型不需要添加标记. 假如我们定义一个cla ...

  7. java协变 生产者理解_Java进阶知识点:协变与逆变

    一.背景 要搞懂Java中的协办与逆变,不得不从继承说起,如果没有继承,协变与逆变也天然不存在了. 我们知道,在Java的世界中,存在继承机制.比如MochaCoffee类是Coffee类的派生类,那 ...

  8. Scala中协变(+)、逆变(-)、上界(:)、下界(:)简单介绍

    对于一个带类型参数的类型,比如 List[T],如果对A及其子类型B,满足 List[B]也符合List[A]的子类型,那么就称为covariance(协变) , 如果 List[A]是 List[B ...

  9. 这一次,终于弄懂了协变和逆变

    一.前言 刘大胖决定向他的师傅灯笼法师请教什么是协变和逆变. 刘大胖:师傅,最近我在学习泛型接口的时候看到了协变和逆变,翻了很多资料,可还是不能完全弄懂. 灯笼法师:阿胖,你不要被这些概念弄混,编译器 ...

最新文章

  1. reentrantlock 使用
  2. layer,一个可以让你想到即可做到的javascript弹窗(层)解决方案
  3. 数据存储之 SQLite 数据库操作(二)
  4. bashrc文件中环境变量配置错误,导致linux命令无法正常使用的解决方案
  5. 如何编写兼容各主流邮箱的HTML邮件并发送
  6. Linux安装或升级openssh步骤和可能遇到的问题
  7. 获取Access表字段类型的自定义函数
  8. INNODB自增主键的一些问题 vs mysql获得自增字段下一个值
  9. 4-1 可复用性概述
  10. Informix 11.5 SQL 语句性能监控方法及实现
  11. Java笔记(十二) 文件基础技术
  12. 20191109每日一句
  13. Cloud 2.0时代,华为云EI助力内蒙煤焦化产业走向智能
  14. PHP 依赖注入 容器,PHP 依赖注入容器 Pimple 笔记
  15. 原生汇率计算器系统源代码
  16. C语言学习(十)C语言中的小数
  17. 计算机专业课程思政优秀案例,【转载】专业课程思政教学案例分享之《专业导论(计算机科学与技术)》...
  18. FTP协议(文件传输协议)
  19. 5、分组密码工作模式
  20. 4/20 Fizz Buzz(412)

热门文章

  1. javaScript快速入门之运算符
  2. 基于ssh的航空订票系统-飞机订票系统javaweb-机票订购课程设计java代码(源码+数据库文件+文档)
  3. 【CTR预估】简单介绍
  4. 专业不对口计算机成绩查询入口,高考成绩查询入口已开通
  5. VINS-Mono安装配置教程
  6. 新手面试官需要做好哪些工作
  7. Mysql 学生信息经典50题
  8. fiddler抓包,Iphone 设置代理后,app和其他任何东西都不能上网的解决方案
  9. 国外LEAD联盟固定IP的一些办法
  10. 电子书网站系统建设构想