看过几篇说协变与逆变的博客,虽然都是正确无误的,但是感觉都没有说得清晰明了,没有切中要害。
那么我也试着从我的理解角度来谈一谈协变与逆变吧。

什么是协变与逆变

MSDN的解释:
https://msdn.microsoft.com/zh-cn/library/dd799517.aspx

协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型。
泛型类型参数支持协变和逆变,可在分配和使用泛型类型方面提供更大的灵活性。

一开始我总是分不清协变和逆变,因为MSDN的解释实在是严谨有余而易读不足。
其实从中文的字面上来理解这两个概念就挺容易的了:

"协变"即"协调的转变","逆变"即"逆向的转变"。

为什么说"能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型"是协调的,而"能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型"是逆向的呢,看这两行代码:

object o = "";
string s = (string) o;

string类型到object类型,也就是派生类到基类,是可以隐式转换的,因为任何类型向基类的转换都是类型安全的,所以认为这一转变是协调的。
object类型到string类型,也就是基类到派生类,就只能是显式转换,因为对象o的实际类型不一定是string,强制转换不是类型安全的,所以认为这一转变是逆向的。

再看协变与逆变的常见场合:

IEnumerable<object> o = new List<string>();//协变
Action<string> s = new Action<object>((arg)=>{...});//逆变

上例的泛型参数就是分别发生了协调的与逆向的转变。

协变与逆变的作用对象

从定义中可以看到,协变与逆变都是针对的泛型参数,而且

在.NET Framework 4中,Variant类型参数仅限于泛型接口和泛型委托类型。

为什么是接口和委托?先看IEnumerable<T>和Action<T>的声明:

public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
} public delegate void Action<in T>(T obj);

IEnumerable中的out关键字给泛型参数提供了协变的能力,Action中的in关键字给泛型参数提供了逆变的能力。
这里的out和in是相对于谁的入和出?不是相对于接口和委托,而是相对于方法体!
看它们的实现:

class MyEnumerable<T> : IEnumerable<T>
{
public IEnumerator<T> GetEnumerator()
{
yield return default(T);
} IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
} Action<string> myAction = new Action<object>(
(o) =>
{
Console.WriteLine(o.ToString());
});

这样是不是能看出来泛型参数是怎么入和出的了?
那么接口和委托,它们和方法是什么关系呢,它们两个之间又是什么关系,以下纯属个人理解:

接口类型定义了一组方法签名,委托类型定义了一个方法结构(方法签名刨除方法名)。
接口实例和委托实例都包含了一组方法入口。

综上所述,协变与逆变的作用对象是方法体中的泛型参数。

为什么允许协变与逆变

协变和逆变都是类型发生了转换,一旦涉及到类型转换当然就要想类型安全的问题。
协变和逆变之所以可以正常的运转,就是因为这里所涉及到的所有类型转换都是类型安全的!
回头看最开始的四行代码:

 object o1 = "";//类型安全
string s1 = (string) o1;//非类型安全
IEnumerable<object> o2 = new List<string>();//协变
Action<string> s2 = new Action<object>((arg)=>{...});//逆变

显然第二行的object到string是非类型安全的,那为什么第四行的object到string就是类型安全的呢?
结合上一个方法体的示例,来看这段代码:

 Action<List<int>> myAction = new Action<IList<int>>(
(list) =>
{
Console.WriteLine(list.Count);
});
myAction(new List<int> {, , });

第一行貌似是把IList转换成了List,但是实际上是这样的:
第六行传入的实参是一个List,进入方法体,List被转换成了IList,然后使用了IList的Count属性。
所以传参的时候其实发生的是派生类到基类的转换,自然也就是类型安全的了。

List<string>到IEnumerable<object>的协变其实也是类似的过程:

 IEnumerable<Delegate> myEnumerable = new List<Action>
{
new Action(()=>Console.WriteLine()),
new Action(()=>Console.WriteLine()),
new Action(()=>Console.WriteLine()),
};
foreach (Delegate dlgt in myEnumerable)
{
dlgt.DynamicInvoke();
}

实参是三个Action,调用的是Delegate的DynamicInvoke方法,完全的类型安全转换。

最后想说的是,所有死记硬背来的知识,都远远不如充分理解的知识来得可靠。

[C#]浅谈协变与逆变的更多相关文章

  1. 在net中json序列化与反序列化 面向对象六大原则 (第一篇) 一步一步带你了解linq to Object 10分钟浅谈泛型协变与逆变

    在net中json序列化与反序列化   准备好饮料,我们一起来玩玩JSON,什么是Json:一种数据表示形式,JSON:JavaScript Object Notation对象表示法 Json语法规则 ...

  2. 再谈对协变和逆变的理解(Updated)

    去年写过一篇博客谈了下我自己对协变和逆变的理解,现在回头看发现当时还是太过“肤浅”,根本没理解.不久前还写过一篇“黑”Java泛型的博客,猛一回头又是“肤浅”,今天学习Java泛型的时候又看到了协变和 ...

  3. Friday Q&A 2015-11-20:协变与逆变

    作者:Mike Ash,原文链接,原文日期:2015-11-20译者:Cee:校对:千叶知风:定稿:numbbbbb 在现代的编程语言中,子类型(Subtypes)和超类型(Supertypes)已经 ...

  4. Java中的协变与逆变

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

  5. C#4.0泛型的协变,逆变深入剖析

    C#4.0中有一个新特性:协变与逆变.可能很多人在开发过程中不常用到,但是深入的了解他们,肯定是有好处的. 协变和逆变体现在泛型的接口和委托上面,也就是对泛型参数的声明,可以声明为协变,或者逆变.什么 ...

  6. C#协变和逆变

    我们知道在C#中,是可以将派生类的实例赋值给基类对象的.

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

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

  8. 不变(Invariant), 协变(Covarinat), 逆变(Contravariant) : 一个程序猿进化的故事

    阿袁工作的第1天: 不变(Invariant), 协变(Covarinat), 逆变(Contravariant)的初次约 阿袁,早!开始工作吧. 阿袁在笔记上写下今天工作清单: 实现一个scala类 ...

  9. 【转】c# 协变和逆变

    本文转自:http://www.cnblogs.com/rr163/p/4047404.html C#的协变和逆变 由子类向父类方向转变是协变,用out关键字标识,由父类向子类方向转变是逆变,用in关 ...

随机推荐

  1. jdk动态代理 案例

    import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflec ...

  2. Lucene.Net+盘古分词器(详细介绍)

    本章阅读概要1.Lucenne.Net简介2.介绍盘古分词器3.Lucene.Net实例分析4.结束语(Demo下载)Lucene.Net简介 Lucene.net是Lucene的.net移植版本,是 ...

  3. $.ajax()——超时设置,增加 loading 提升体验

    前端发送Ajax请求到服务器,服务器返回数据这一过程,因原因不同耗时长短也有差别,且这段时间内页面显示空白.如何优化这段时间内的交互体验,以及长时间内服务器仍未返回数据这一问题,是我们开发中不容忽视的 ...

  4. jmeter 之 BeanShell PostProcessor跨线程全局变量使用

    BeanShell PostProcessor是用户对一些变量的操作,操作方法很灵活,大概原理是通过parameters传回来对象,然后在script中对对象进行操作 场景:从登陆接口中获取token ...

  5. HEOI 十二省联考退役记

    Day -1 简要的说了些注意事项 一整天都在刷树套树的水题 退役的感觉近了 Day 0 早上收拾好东西去了火车站之后 火车站居然还没有开门 等了半天 我们是从衡水到德州再到秦皇岛 到了德州之后 去车 ...

  6. Java方法区

    方法区 在一个jvm实例的内部,类型信息被存储在一个称为方法区的内存逻辑区中.类型信息是由类加载器在类加载时从类文件中提取出来的.类(静态)变量也存储在方法区中. jvm实现的设计者决定了类型信息的内 ...

  7. 2. 需要对测试用的数据进行MD5加密

    import hashlib phone_num = open("D:/testdata/phone10.txt","r") out_file = open(& ...

  8. Python基础部分的疑惑解析(2)

    变量: 变量名由 字母.数字.下划线构成,数字不能做为开头 不能用关键字:另外一些内置的方法也别用 推荐使用下划线命名间两个单词user_id 变量在最后底层处理的时候没什么意义,但是在命名的时候有利 ...

  9. 在Azuer创建自己的Linux_VM

    ---恢复内容开始--- emm..就是想搭个自己的VPN去YouTube看看视屏找找资源什么的... (滑稽.jpg)然后发现似乎需要这个玩意儿 先去申请一个Azuer账户 然后根据要求一步步来就好 ...

  10. 从svn检出项目的注意事项

    提交到svn的时候,选择忽略.project,.settings,.classpath等文件,检出项目的时候就不能选择 [做为工作作为工作空间的项目检出].而应该选择做为新项目检出. 然后选择工程的类 ...