C#泛谈 —— 变体(协变/逆变)
有如下四个类。
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#泛谈 —— 变体(协变/逆变)相关推荐
- 12:设计模式、泛型、上下界、视图界定、上下文界定、协变逆变不变
经典的 WordCount 的讲解 示例代码如下: package com.atguigu.chapter14.homework.wordcount/*val lines = List("a ...
- 大数据技术之_16_Scala学习_12_设计模式+泛型、上下界、视图界定、上下文界定、协变逆变不变
大数据技术之_16_Scala学习_12 第十七章 设计模式 17.1 学习设计模式的必要性 17.2 掌握设计模式的层次 17.3 设计模式的介绍 17.4 设计模式的类型 17.5 简单工厂模式( ...
- 协变逆变java_Java中的逆变与协变
什么是逆变与协变 协变(Covariance) 如果B是A的子类,并且F(B)也是F(A)的子类,那么F即为协变 逆变(Contravariance) 如果B是A的子类,并且F(B)成了F(A)的父类 ...
- 泛型型协变逆变_Java泛型类型简介:协变和逆变
泛型型协变逆变 by Fabian Terh 由Fabian Terh Java泛型类型简介:协变和逆变 (An introduction to generic types in Java: cova ...
- 协变逆变java_Java中的协变与逆变
Java作为面向对象的典型语言,相比于C++而言,对类的继承和派生有着更简洁的设计(比如单根继承). 在继承派生的过程中,是符合Liskov替换原则(LSP)的.LSP总结起来,就一句话: 所有引用基 ...
- 10天学会kotlin DAY7 接口 泛型 协变 逆变
kotlin 接口 泛型 协变 逆变 前言 1.接口的定义 2.抽象类 3.定义泛型类 4.泛型函数 5.泛型变换 6.泛型类型约束 7.vararg 关键字(动态参数) 8.[] 操作符 9.out ...
- Scala语言学习笔记——泛型、上下界、视图界定、上下文界定、协变逆变不变、闭包、柯里化
1.Scala泛型 应用案例1 /*** @author huleikai* @create 2019-05-27 11:23*/ object TestFanXing {def main(args: ...
- PCS储能逆变并网模型 逆变侧采用背靠背三电平设计 SVPWM控制算法
PCS储能逆变并网模型,包括: (1)逆变侧采用背靠背三电平设计,SVPWM控制算法,中点平衡算法,马鞍波. LC滤波. (2)DCDC采用BUCK/BOOST电路. 控制方法采用双闭环控制,电压环电 ...
- PCS储能逆变并网模型 逆变侧采用背靠背三电平设计,SVPWM控制算法
PCS储能逆变并网模型,包括: (1)逆变侧采用背靠背三电平设计,SVPWM控制算法,中点平衡算法,马鞍波. LC滤波. (2)DCDC采用BUCK/BOOST电路. 控制方法采用双闭环控制,电压环电 ...
最新文章
- 完美解决 向UILable 文字最后插入N张图片,支持向限制行数的UILable 最后一行插入,多余文字显示......
- Windows 10——安装Snort_2_9_16
- JQUERY的split
- 6行代码!用Python将PDF转为word
- 【Qt教程】Qt常用部件介绍
- lucene索引word/pdf/html/txt文件及检索(搜索引擎)
- 解决同一页面中两个iframe互相调用jquery,js函数
- java获取文件列表_java获取指定目录中的文件列表
- 云计算之路-黎明前的黑暗:20130424网站故障经过
- 2021奥运经济蓝皮书
- matlab生猪的出售时机,数学模型程序代码-Matlab-姜启源-第三章-简单的优化模型.doc...
- java 输出二进制文件_Java输出小端二进制文件
- SparkMLlib之二Basic Stastics
- 临床试验中edc录入_一文了解EDC临床试验数据采集系统
- 高数 | 旋转体体积计算方法汇总、二重积分计算旋转体体积
- ubuntu 18.04 设置静态IP地址
- 更改win10系统的默认图片打开方式为windows照片查看器
- RS码FEC机制的实现方法(基于Luigi Rizzo的代码)
- 电脑操作实用技巧60招(转)
- 慎用cv::fitLine