概念

协变和逆变来源于类型和类型之间的绑定,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. Spring Boot 支持 HTTPS 如此简单,So easy!

    这里讲的是 Spring Boot 内嵌式 Server 打 jar 包运行的方式,打 WAR 包部署的就不存在要 Spring Boot 支持 HTTPS 了,需要去外部对应的 Server 配置. ...

  2. 微信小程序之如何定义页面标题

    效果图: 这个标题是在哪里定义的呢?type.js核心代码如下(通常这段代码放在onLoad函数体内): wx.setNavigationBarTitle({ title: "支出类型列表& ...

  3. Git创建与合并分支,撤销修改

    git回滚到指定版本并推送到远程分支(撤销已提交的修改,并已push) git reset --hard <commit ID号> git push -f git回滚到上一个版本并推送到远 ...

  4. 快速激活Navicat Premium 12

    Navicat Premium 12是一套数据库开发管理工具,支持链家MySQL.Oracle.SQL server等多种数据库,快速便捷创建.管理和维护数据库 一.下载 https://www.na ...

  5. MySQL8新特性(2)--mysql的升级过程

    在之前的版本中,安装新的版本mysql之后,mysql server在下一次启动时,会自动升级数据字典表,然后dba需要执行mysql_upgrade手动升级mysql schema中的系统表,以及其 ...

  6. 已知X,Y独立,那么X^2与Y也独立

    考虑离散情况,  P{X^2=k} => P{X=sqrt(k)} 由X,Y独立可知, P{X=Sqrt(k}  | Y=y} =P{X=Sqrt(x)}, P{X^2=k | Y=y} =P{ ...

  7. 爬虫urllib2中Handler处理器和自定义Opener

    Handler处理器 和 自定义Opener opener是 urllib2.OpenerDirector 的实例,urlopen是一个特殊的opener(也就是模块已经构建好的). 但是基本的url ...

  8. 软件定义网络基础---REST API概述

    一:什么是REST API REST API是北向接口的主流设计方式 API是应用程序编程接口,是预先定义好的函数,可以供应用程序或开发人员访问调用 年 Roy Thomas Fielding 的博士 ...

  9. [译]使用BeautifulSoup和Python从网页中提取文本

    如果您要花时间浏览网页,您可能遇到的一项任务就是从HTML中删除可见的文本内容. 如果您使用的是Python,我们可以使用BeautifulSoup来完成此任务. 设置提取 首先,我们需要获取一些HT ...

  10. Python - Django - 使用 Bootstrap 样式修改注册页

    reg2 函数: from django.shortcuts import render, HttpResponse from app01 import models def reg2(request ...