有如下四个类。

    public class Animal{}public class Mammal : Animal{}public class Dog : Mammal{public void EatBone(){}}public class Panda : Mammal{public void EatBamboo(){}}

    Animal animal = new Dog();

这样的赋值肯定是没问题的,但这只是多态。

变体的大概意思是:有T和U两个类型,并且T = U (此处的等号为赋值)成立,如果T和U经过某种操作之后分别得到T’和U’,并且T’ = U’也成立,则称此操作为协变;如果U’ = T’,则称此操作为逆变。

//以下代码能通过,则说明Operation是协变。T = U; //=表示赋值↓
Operation(T) = Operation(U);

//类似的,以下操作为逆变。T = U;
  ↓
Operation2(U) = Operation2(T);

一、特殊的协变——数组

我们常说协变和逆变是.net 4.0中引入的概念,但实际上并不是。其实只要符合上面定义的,都是变体。我们先来看一个.net 1.0中就包含的一个协变:

    Animal[] animalArray = new Dog[10];

这个不是多态,因为Dog[]的父类不是Animal[],而是object。
我们对照变体的定义来看一下,首先Animal = Dog,这个是成立的,因为Dog是Animal的子类;然后经过Array这个操作后,等式左右两边分别变成了Animal[]和dog[],并且这个等式仍然成立。这已经是满足协变的定义了。

可能有人会困惑,这为什么等号就成立了呢?
我们有一点要明确的是,因为C#语言规定了Array操作是协变,并且Compiler支持了,所以等式就成立了。变体都是人为定的,你甚至可以规定任何操作都是协变或者逆变,无非就是使编译和在运行期变体处的赋值通过。

我们再看一下Array的应用:

    Animal[] animalArray = new Dog[10]; //Line1animalArray[0] = new Bird(); //Line2

上面的代码能编译通过,Line1处也能运行通过,但是到了Line2处就会抛异常,所以说虽然Array这个操作是一个协变,但并不是安全的,在某些时候还是会出错。

至于说为什么要支持Array这样的协变,据Eric Lippert在Covariance and Contravariance in C#, Part Two: Array Covariance说,是为了兼容Java的语法,虽然他本人也不是很满意这样的设计。

二、委托中的变体

在.net 2.0中委托也支持了协变,不过暂时还只是支持方法的赋值。

考虑下面的代码

    //一个入参为Dog的委托。抓住了一只Dog,应该怎么处理?delegate void DogCatched(Dog d);   //定义两个方法void OnAnimalCatched(Animal animal) {}  //处理抓到的Animalvoid OnDogCatched(Dog dog) {}  //处理抓到的Dog
Catch catchDog = OnDogCatched; //把抓到的Dog交给处理Dog的方法catchDog = OnAnimalCatched;  //把抓到的Dog交给处理Animal的方法

以上两个赋值都可以成功,其中第一个为符合委托原型的赋值。第二个则可以看做是Operate(Dog) = Operate(Animal),那这是一个逆变。

同样的,下面就是一个协变。

    //一个返回值为Animal的委托,一个需要抓到一只Animal的任务delegate Animal AnimalCatching();//两个方法Animal CatchAnAnimal() { return new Animal(); } //抓到一个AnimalDog CatchADog() { return new Dog(); } //抓到一个Dog
    AnimalCatching animalCatching = CatchAnAnimal; //把抓Animal的任务交给能抓到Animal的方法animalCatching = CatchADog; //把抓Animal的任务交给能抓到Dog的方法

至于Action<T>和Func<TResult>(.net 3.5)等泛型委托,其实也是如此,同样只局限于方法给委托实例赋值,而不支持委托实例赋值给委托实例。下面的例子编译时会报错。

    Action<Animal> aa = animal => { };Action<Dog> ad = aa;  //编译错误

三、泛型中的变体

我们常说的协变和逆变,大多数指的是.net 4.0中引入的针对泛型委托和泛型接口的变体。

泛型委托

我们发现,到了.net 4.0,之前不能编译的这段代码通过了

    Action<Animal> aa = animal => { };Action<Dog> ad = aa;  //编译通过

其实是Action的签名变了,多了in这个关键字。

    public delegate void Action<T>(T obj); //.net 4 之前public delegate void Action<in T>(T obj); //.net 4

类似的,Func的签名也变了,多了out关键字

    public delegate TResult Func<TResult>(); //.net 4 之前public delegate TResult Func<out TResult>(); //.net 4

in和out就是C# 4.0中用于在泛型中显式的表示协变和逆变的关键字。in表示逆变参数,out表示协变参数。

对于泛型委托的变体这一块上,.net 4.0相对于之前的版本主要增强的就是委托实例赋值委托实例(方法赋值给委托实例是.net 2.0就支持的)。

泛型接口

在.net 4.0以前,Array是协变的(尽管它不安全),但IList<T>却不是,IEnumerable<T>也不是。而到了.net 4.0,我们终于可以这样干了:

    IEnumerable<Animal> animals = new List<Dog>();  //.net 4正确

不过以下的操作还是会造成编译失败:

    IList<Animal> a2 = new List<Dog>(); //错误

究其原因,当然还是因为IEnumerable<T>在.net 4.0中是协变的,IList<T>不是:

    public interface IEnumerable<out T> : IEnumerablepublic interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable

那泛型接口既然有协变的,同样也有逆变的,如IComparable<T>。

四、一些疑问

1,问:我们自定义的泛型接口和泛型委托是否可以随便加上in/out关键字,来表明它是逆变或者协变的?

答:这个当然是不可能的,编译器会校验。

一般来说,如果一个泛型接口中,所有用到T的方法,都只将其用于输入参数,则T可以是逆变参数;如果用到T的方法,都只将其用于返回值,则T可以是协变参数。

委托的输入参数可以是逆变参数;返回值可以是协变参数

2,问:既然in/out不能乱加,为什么还要加呢?完全由编译器来决定协变或者逆变的赋值不可以么?

答:这个理论上应该是可以的,不过in/out关键字就像是一个泛型委托和泛型接口定义者同使用者之间的契约,必须显式的指定使用方式,否则,程序中出现一些既不是多态,又没有标明是协变或逆变,却可以赋值成功的代码,看起来比较混乱。

3,问:是不是所有的泛型委托和接口都遵从输入参数是协变的,输出参数是逆变的这一规律呢?

答:我们定义一个泛型委托Operate<T>,它的输入参数是一个Action<T>

    delegate void Operate<T>(Action<T> action);
    //两个Action<T>的实例    Action<Mammal> MammalEat = mammal => Console.WriteLine("mammal eat");Action<Panda> PandaEat = panda => panda.EatBamboo();
//Operate<T>的实例Operate<Mammal> MammalOperation = action => action(new Dog()); //Action<T>是逆变,所以这里是允许的。

然后我们可以执行下面的操作

    //操作1    MammalOperation(MammalEat);

如果我们想让这个泛型委托是一个变体,按照我们通常的理解,T是用作输入参数的,那肯定就是逆变,应该加上in关键字。我们不考虑编译器的提示,假设定义成这样:

    delegate void Operate<in T>(Action<T> action);

因为是逆变,所以,我们可以将Operate<Mammal>赋给Operate<Panda>

    Operate<Panda> PandaOperate = MammalOperation;

由于上面这个Operate的T已经改成了Panda,所以其对应参数Action的T也应该改为Panda,所以上面的“操作1”可以改成这样:

    //操作2MammalOperation(PandaEat);

最终变成了PandaOperate = (new Dog()).EatBamboo()。这是个啥?完全不合常理。

实际上,当我们给Operate<T>加上in的时候,编译器就已经告诉我们,这是不对的了。写成out就可以了,说明这是一个协变,下面的操作也是可以的:

    Operate<Animal> AnimalOperate = MammalOperation;

上面这个例子似乎说明了,也并不是所有的输入参数都是逆变的?其实这已经不完全是一个输入参数了,由于有Action<T>的影响,似乎就变成了“逆逆得协”?如果把Action<T>换成Func<T>,则Operate<T>就应该用in关键字了。是不是比较费脑?还好平时工作中很少碰到这种情况,更何况还有编译器给我们把关。

以上内容参考自Eric Lippert的Covariance and Contravariance In C#系列,对.net中协变逆变的进化做了很详细的描述,有兴趣可以看一下。

转载于:https://www.cnblogs.com/joecheung/p/3139124.html

C#泛谈 —— 变体(协变/逆变)相关推荐

  1. 12:设计模式、泛型、上下界、视图界定、上下文界定、协变逆变不变

    经典的 WordCount 的讲解 示例代码如下: package com.atguigu.chapter14.homework.wordcount/*val lines = List("a ...

  2. 大数据技术之_16_Scala学习_12_设计模式+泛型、上下界、视图界定、上下文界定、协变逆变不变

    大数据技术之_16_Scala学习_12 第十七章 设计模式 17.1 学习设计模式的必要性 17.2 掌握设计模式的层次 17.3 设计模式的介绍 17.4 设计模式的类型 17.5 简单工厂模式( ...

  3. 协变逆变java_Java中的逆变与协变

    什么是逆变与协变 协变(Covariance) 如果B是A的子类,并且F(B)也是F(A)的子类,那么F即为协变 逆变(Contravariance) 如果B是A的子类,并且F(B)成了F(A)的父类 ...

  4. 泛型型协变逆变_Java泛型类型简介:协变和逆变

    泛型型协变逆变 by Fabian Terh 由Fabian Terh Java泛型类型简介:协变和逆变 (An introduction to generic types in Java: cova ...

  5. 协变逆变java_Java中的协变与逆变

    Java作为面向对象的典型语言,相比于C++而言,对类的继承和派生有着更简洁的设计(比如单根继承). 在继承派生的过程中,是符合Liskov替换原则(LSP)的.LSP总结起来,就一句话: 所有引用基 ...

  6. 10天学会kotlin DAY7 接口 泛型 协变 逆变

    kotlin 接口 泛型 协变 逆变 前言 1.接口的定义 2.抽象类 3.定义泛型类 4.泛型函数 5.泛型变换 6.泛型类型约束 7.vararg 关键字(动态参数) 8.[] 操作符 9.out ...

  7. Scala语言学习笔记——泛型、上下界、视图界定、上下文界定、协变逆变不变、闭包、柯里化

    1.Scala泛型 应用案例1 /*** @author huleikai* @create 2019-05-27 11:23*/ object TestFanXing {def main(args: ...

  8. PCS储能逆变并网模型 逆变侧采用背靠背三电平设计 SVPWM控制算法

    PCS储能逆变并网模型,包括: (1)逆变侧采用背靠背三电平设计,SVPWM控制算法,中点平衡算法,马鞍波. LC滤波. (2)DCDC采用BUCK/BOOST电路. 控制方法采用双闭环控制,电压环电 ...

  9. PCS储能逆变并网模型 逆变侧采用背靠背三电平设计,SVPWM控制算法

    PCS储能逆变并网模型,包括: (1)逆变侧采用背靠背三电平设计,SVPWM控制算法,中点平衡算法,马鞍波. LC滤波. (2)DCDC采用BUCK/BOOST电路. 控制方法采用双闭环控制,电压环电 ...

最新文章

  1. 完美解决 向UILable 文字最后插入N张图片,支持向限制行数的UILable 最后一行插入,多余文字显示......
  2. Windows 10——安装Snort_2_9_16
  3. JQUERY的split
  4. 6行代码!用Python将PDF转为word
  5. 【Qt教程】Qt常用部件介绍
  6. lucene索引word/pdf/html/txt文件及检索(搜索引擎)
  7. 解决同一页面中两个iframe互相调用jquery,js函数
  8. java获取文件列表_java获取指定目录中的文件列表
  9. 云计算之路-黎明前的黑暗:20130424网站故障经过
  10. 2021奥运经济蓝皮书
  11. matlab生猪的出售时机,数学模型程序代码-Matlab-姜启源-第三章-简单的优化模型.doc...
  12. java 输出二进制文件_Java输出小端二进制文件
  13. SparkMLlib之二Basic Stastics
  14. 临床试验中edc录入_一文了解EDC临床试验数据采集系统
  15. 高数 | 旋转体体积计算方法汇总、二重积分计算旋转体体积
  16. ubuntu 18.04 设置静态IP地址
  17. 更改win10系统的默认图片打开方式为windows照片查看器
  18. RS码FEC机制的实现方法(基于Luigi Rizzo的代码)
  19. 电脑操作实用技巧60招(转)
  20. 慎用cv::fitLine

热门文章

  1. QT计算器功能的实现
  2. 青蛙跳台阶问题(思路与蜂窝问题一致)
  3. 适合程序员的四大字体
  4. JVM_01 总体概述
  5. Hi3516A开发-- 板卡串口烧写
  6. js如何同时打开多个信息窗口 高德地图_高德地图显示单个窗体和显示多个窗体的方法...
  7. ios退款 怎么定位到是哪个用户_关于ios企业签名必须知道的几点
  8. GDB 调试多进程或者多线程应用
  9. 浅谈android hook技术
  10. Android安全教程(1)---Fiddler简易使用教程之配置环境