概念

协变和逆变来源于类型和类型之间的绑定,C#4.0开始在泛型的接口和委托上支持协变和逆变,不过在这个版本之前的委托也是支持协变和逆变的。比如数组就支持协变,但是这不是一个好的特性,这C#初期版本从java引入的一个特性,当时的设计者认为C#应该尽可能的像java的一些特性靠拢,因为java当时太火了。

如果有类型Parent和其子类Sub,那么Parent p=new Sub();这种的类型转换是安全的。如果有一种类型和Parent类型进行了绑定,比如说Parent[]数组,如果Sub[]到Parent[]的转换是安全的,我们就说是协变,如果相反的方向上转换是安全的,我们就说是逆变了。

C#的协变和逆变没有完整的阐述这个数学和物理上的概念,C#为协变和逆变的实现捆绑了很多条件:

①必须是泛型的委托或接口

②协变只能支持返回类型

③逆变只能支持参数

而这些条件总归是实现了一个面向对象原则:里氏替换原则。也就是说,不管是协变还是逆变,最终都是一种类型安全的转换,没有不安全的转换发生。

有了上面这些条件的约束还不够,C#还明确的指定了两个关键字来让开发人员更加明确的知道声明的泛型类型参数到底用在了哪里:给协变对应的关键字是out,逆变对应的关键字是in,看一下示例:

IEnumerable<out T>//IEnumerable这个接口对T协变
Action<int TParameter>//Action这个委托对TParameter逆变

上面这个解释基本阐明了泛型的协变和逆变的概念,下面来看一些例子

示例

委托的协变和逆变

    public delegate object Test(string obj);
class Program
{ static void Main(string[] args)
{
Test test = TestMethod;
var result = test("hello world");
Console.ReadKey(); string TestMethod(object str)
{
Console.WriteLine(str);
return "hello world";
}
}
}

代码第一行定义了一个返回object,输入string的名为Test的委托。我们在main方法中定义了一个本地方法(C#7的功能)然后用这个方法来实例化一个Test委托(利用方法组转换特性),可以看到这个Test委托既表达了协变的意思(返回类型的协变:object=string,由一个object类型引用一个返回string类型的方法肯定没有问题),又表达了逆变的意思(参数类型的逆变:实际上还是object=string,为什么这么说呢?因为真正要执行的方法是TestMethod,这个方法需要一个object类型的参数,但是这个方法的地址指向了一个Test委托,这个委托代表了那种输入类型为string,输出类型为object的方法,当我们调用Test(...)这个委托时,很明显需要传入一个string类型的变量进去,但是真正执行的方法是TestMethod,而这个TestMethod需要传入一个object类型的参数,那么,这个string类型传入这个TestMethod方法内部肯定是没有问题的,同时,也说明了,逆变实际上还是一种安全转换,按这个例子来说(参数要求是object的,实际传入了一个string),还是从string类型转换成了object类型了嘛!)。

上面解释有点儿啰嗦,但也基本说明了原理了。

协变

接口的协变最著名的例子就是IEnumerable<T>:

IEnumerable<object> ss = new List<string>();

逆变

逆变的例子是一个Action<T>:

public static void Main(string[] args)
{
Action<string> action = Test;
void Test(object obj)
{
Console.WriteLine(obj);
}
}

这些关于协变和逆变的解释在上面通通都有,就不再解释了。

协变和逆变的相互作用

这是一个相当有趣的话题,我们先来看一个例子:

interface IFoo<in T>
{
}
interface IBar<in T>
{
void Test(IFoo<T> foo); //对吗?
}

你能看出上述代码有什么问题吗?我声明了in T,然后将他用于方法的参数了,一切正常。但出乎你意料的是,这段代码是无法编译通过的!反而是这样的代码通过了编译:

interface IFoo<out T>
{
}
interface IBar<in T>
{
void Test(IFoo<T> foo); //对吗?
}

什么?明明是out参数,我们却要将其用于方法的参数才合法?初看起来的确会有一些惊奇。我们需要费一些周折来理解这个问题。现在我们考虑IBar<string>,它应该能够协变成IBar<object>,因为string是object的子类。因此IBar.Test(IFoo<string>)也就协变成了IBar.Test(IFoo<object>)。当我们调用这个协变后方法时,将会传入一个IFoo<object>作为参数。想一想,这个方法是从IBar.Test(IFoo<string>)协变来的,所以参数IFoo<object>必须能够变成IFoo<string>才能满足原函数的需要。这里对IFoo<object>的要求是它能够反变成IFoo<string>!而不是协变。也就是说,如果一个接口需要对T协变,那么这个接口所有方法的参数类型必须支持对T的反变。同理我们也可以看出,如果接口要支持对T反变,那么接口中方法的参数类型都必须支持对T协变才行。这就是方法参数的协变-反变互换原则。所以,我们并不能简单地说out参数只能用于返回值,它确实只能直接用于声明返回值类型,但是只要一个支持反变的类型协助,out类型参数就也可以用于参数类型!换句话说,in参数除了直接声明方法参数之外,也仅能借助支持协变的类型才能用于方法参数,仅支持对T反变的类型作为方法参数也是不允许的。要想深刻理解这一概念,第一次看可能会有点绕,建议有条件的情况下多进行一些实验。

刚才提到了方法参数上协变和反变的相互影响。那么方法的返回值会不会有同样的问题呢?我们看如下代码:

interface IFooCo<out T>
{ }
interface IFooContra<in T>
{ }
interface IBar<out T1, in T2>
{
IFooCo<T1> Test1(); IFooContra<T2> Test2();
}

我们看到和刚刚正好相反,如果一个接口需要对T进行协变或反变,那么这个接口所有方法的返回值类型必须支持对T同样方向的协变或反变。这就是方法返回值的协变-反变一致原则。也就是说,即使in参数也可以用于方法的返回值类型,只要借助一个可以反变的类型作为桥梁即可。如果对这个过程还不是特别清楚,建议也是写一些代码来进行实验。至此我们发现协变和反变有许多有趣的特性,以至于在代码里in和out都不像他们字面意思那么好理解。当你看到in参数出现在返回值类型,out参数出现在参数类型时,千万别晕倒,用本文的知识即可破解其中奥妙。

文章部分引用自:https://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html

C#精粹--协变和逆变的更多相关文章

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

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

  2. C#协变和逆变

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

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

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

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

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

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

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

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

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

  7. .NET 4.0中的泛型的协变和逆变

    转自:http://www.cnblogs.com/jingzhongliumei/archive/2012/07/02/2573149.html 先做点准备工作,定义两个类:Animal类和其子类D ...

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

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

  9. Java用通配符 获得泛型的协变和逆变

    Java对应泛型的协变和逆变

随机推荐

  1. Nessus简单使用

    1.更新插件 上次搭建完后总觉得不踏实,因为老是提示插件多久没更新了,然后果断花了1.25美刀买了台vps,终于把最新的插件下载下来了,总共190M,需要的QQ私信我.

  2. CTF PHP反序列化

    目录 php反序列化 一.序列化 二.魔术方法 1.构造函数和析构函数 2.__sleep()和__wakeup() 3.__toString() 4.__set(), __get(), __isse ...

  3. CTF 文件包含

    目录 一.基本概念 二.本地文件包含 三.远程文件包含 四.具体场景 五.补充 一.基本概念 文件包含 将相同函数写入单独的文件中,需要使用时直接调用 文件包含漏洞 将被包含的文件设置为变量,导致客户 ...

  4. GIS地理工具案例教程——批量合并影像-批量镶嵌栅格

    GIS地理工具案例教程--批量合并影像-批量镶嵌栅格 商务合作,科技咨询,版权转让:向日葵,135-4855__4328,xiexiaokui#qq.com 关键词:批量.迭代.循环.自动.智能.地理 ...

  5. t420 win7 硬盘安装ubuntu 10.04 LTS 备忘

    http://zhangwen.sinaapp.com/?p=5 t420 win7 硬盘安装ubuntu 10.04 LTS 备忘 发表于 2011 年 10 月 25 日 对ubuntu的新版没有 ...

  6. odoo开发笔记 -- 异常处理in resolve_deps field = model

    场景描述: 更新代码,重启服务服务后,odoo后台报错,提示关键字:in resolve_deps field = model._fields[fname]  KeyError: 'entry_id' ...

  7. Python3基础 dict __len__ 统计键值对的数量

             Python : 3.7.3          OS : Ubuntu 18.04.2 LTS         IDE : pycharm-community-2019.1.3    ...

  8. 调用百度API接口 正解析地址和逆解析

    地址解析(结构化地址 解析得到 经纬度): public void SaveLocation(DataRequest<Location> request, DataResponse< ...

  9. Hadoop,Spark,Flink 相关KB

    Hive: https://stackoverflow.com/questions/17038414/difference-between-hive-internal-tables-and-exter ...

  10. zabbix详解

    官网地址 https://www.zabbix.com/documentation/3.0/manual/config/items/itemtypes/zabbix_agent 使用率