C# 中的协变和逆变

时间:2022-09-21 08:43:49

作为一个从接触 Unity 3D 才开始学习 C# 的人,我一直只了解一些最基本、最简单的语言特性。最近看了《C# in Depth》这本书,发现这里面东西还真不少,即使除去和 Windows 编程相关的内容,只是兼容 Unity 的部分就够好好消化一阵子的。其中,令我非常头大的一组概念,就是协变性和逆变性(统称可变性)。

一、可变性的概念

C# 一开始就支持数组的协变,听说是为了和 Java 竞争于是就把 Java 的这个不怎么样的特性给实现了。看如下代码,

object[] myArray = new string[] {
"abc",
"def",
"ghi",
// ...
};

我 new 了一个 string 类型的数组,却把它作为一个 object 类型数组的初始化式。这可以编译通过,也就是说,使用 object 数组的地方,都可以传入 string 数组。推广一下,就是使用基类数组的地方,都可以传入派生类数组。这样看来,协变的概念没什么深奥的,所谓协变性指的就是,在一个使用一般类型的地方,可以传入一个特殊类型的对象。那么,最基本的协变就是面向对象中的多态——使用基类型对象的地方,都可以传入派生类对象。

但是,上面这个数组的协变性还是很特别的。首先,在 C# 的类型系统中,派生类型的数组并不继承自基类型的数组(别的语言也是如此吧)。所以,它的确是一种新的协变性。其次,如果你做一个如下操作,你就会收到运行时错误,告诉你数组类型不匹配:

myArray[] = ;

也就是说,CLR 还是知道 myArray 到底是什么类型的,并且不许改变。这显得有些别扭,不过至少我通过它知道协变是什么了。于是逆变就是反过来的概念——在一个使用特殊类型的地方,可以传入一般类型的对象。

二、委托中的可变性

在 C# 1 中,如果我们定义一个委托类型,那么用于它的方法将必须在参数表和返回值方面严格匹配。但是 C# 2 里事情改变了,它支持对参数的逆变性和对返回值的协变性。假定有类型 Base 和 Derived,其中后者派生自前者,那么下面代码是合法的。

delegate Base VariantDelegate(Derived d);

public Derived MyFunc(Base b) {
// ...
} // ...
VariantDelegate d = new VariantDelegate(MyFunc);

委托 VariantDelegate 要求一个 Base 类型的返回值,但是我们可以给它一个 Derived 类型的返回值,即这返回值是协变的。类似的,这委托的参数是逆变的。为什么这样是合理的呢?

考虑使用这委托的地方,它最终会调用这个委托的实例,传入参数,处理返回值。由于使用者必须给这个委托实例传一个 Derived 类型(或者它的子类型)的参数,那么,如果这委托所调用的方法 MyFunc 本身需要一个 Base 类型的参数,不会有任何问题。因为所有的 Derived 对象可以被当做是 Base 类型的对象来使用。具体的说,如果 Base 是 object,而 Derived 是 string,那么本例中,委托的使用者会给委托一个 string 类型的参数,而 MyFunc 会把这 string 当做是 object 处理,当然是安全的。另一方面,对于这委托的返回值,使用者当它是个 Base 来处理,那么,MyFunc 实际返回的是 Derived 也就没有任何问题。这其实就是消费代码必须把传入对象当做是一个更加泛化(一般)的对象来处理。因此,如果让委托参数支持协变,返回值支持逆变,那么一定会死得很难看。

三、泛型中的可变性

一直到 C# 3,泛型类型、接口、委托的参数都是不可变的。基于和上述类似的逻辑,C# 4 终于决定在泛型接口和委托中支持类型参数的可变性。如果你想使用可变性,必须在类型参数前用 in 或者 out 修饰符来显式指定。和前面类似,如果一个类型参数仅用作接口方法或者委托中的(普通)参数,那么它可以被指定为逆变的(使用 in 来修饰);如果它只作为返回值,那么它可以被指定为协变的(使用 out 来修饰)。最常见的例子是下面两个委托:

delegate void Action<T>(T t);
delegate TResult Func<TResult>();

在 C# 4 中的定义变成了

delegate void Action<in T>(T t);
delegate TResult Func<out TResult>();

如果理解了上面介绍的关于(非泛型)委托参数和返回值的可变性,那么对这样的泛型委托也可以很容易的理解。但是,如果情况复杂了怎么办呢?考虑下面这个情况:

delegate void Action2<T>(Action<T> action);

这个委托如果要受可变性的恩泽,应该在 T 前面加什么修饰符呢?答案是 out,而书上的解释是模糊的:“作为一个便捷的规则,可以认为内嵌的逆变性反转了之前的可变性。”这话谁看得懂呢?还是来分析一下好了。

这个委托 Action2 的参数是 Action<T> 类型。基于委托参数的逆变性,我们可以传递一个比 Action<T> 更特殊(如果不是 Action<T> 本身)的类型。那么怎样的类型是比 Action<T> 特殊呢?由于委托参数的逆变性,Action<T> 需要一个比 T 特殊(如果不是 T 本身的话)的参数,也就是说,使用 Action<object> 的地方可以传一个 Action<string>。所以,我们可以将其理解为,Action<string> 是比 Action<object> 更泛化的类型。那好了,Action<object> 就是比 Action<string> 更窄化。这样,前面说需要 Action<T> 参数的地方,可以传一个 Action<T 的派生类>。也就是说,在 Action2<T> 中,要求类型 T 的时候可以传入 T 的派生类,所以 T 是协变的,应加 out 修饰符。从这个例子,也可以明白前面引用的《C# in depth》上的解释:object 本身是比 string 更泛化的类型,但逆变性使得 Action<string> 成了比 Action<object> 更泛化的类型。

值得注意的一点,就是 out 的意思。out 除了表示类型参数的协变性之外,还有一个作用,就是作为函数参数的修饰符,表示输出参数。如果泛型接口或者委托的类型参数用于输出参数,那么它本身是不可变的,也就不能用 out 来修饰了。为什么呢?我们假定 CLR 支持这种可变性(从语义上来看当然应该是协变性),看看会发生什么。考虑如下委托:

delegate void WrongDelegate<out T>(out T t); // Won't really compile

它有一个输出参数,是 T 类型的。我们标记它是协变的,那么需要 WrongDelegate<string> 的地方可以传入 WrongDelegate<object>。也就是说它可以从下面这样的方法实例化:

void MyFuncWithOutParam(out object o){
// Something will be assigned to o here
}

可是,使用 WrongDelegate<string> 的地方会传一个 string 类型的变量给它作为输出参数,而 MyFuncWithOutParam 认为传进来的东西是 object,不定会赋什么样的对象给它,后果不堪设想。至于如果 T 是逆变的会发生什么问题,我还没有想清楚,很可能是因为,输出参数是不能做类型转换的(比如编译器会告诉你out object不能转换为out string或者反过来)。但为什么输出参数不能做类型转换呢?

无论如何,输出参数和返回值还是很不一样的。输出参数毕竟还是参数。对 CLR 来说,输出参数是带有特别属性的引用参数,我理解和引用参数的区别并不大。而对于引用参数,人们可能更容易理解它为什么不能是可变的。


注意:这里说的可变性,是英文 Variance,协变和逆变分别是 Covariance 和 Contravariance,要和 mutability 区分。

C# 中的协变和逆变的更多相关文章

  1. Scala中的协变,逆变,上界,下界等

    Scala中的协变,逆变,上界,下界等 目录 [−] Java中的协变和逆变 Scala的协变 Scala的逆变 下界lower bounds 上界upper bounds 综合协变,逆变,上界,下界 ...

  2. &period;net中的协变和逆变

    百度:委托中的协变和逆变. 百度:.net中的协变和逆变. 协变是从子类转为父类. 逆变是从父类到子类. 这样理解不一定严谨或者正确.需要具体看代码研究.

  3. Java泛型中的协变和逆变

    Java泛型中的协变和逆变 一般我们看Java泛型好像是不支持协变或逆变的,比如前面提到的List<Object>和List<String>之间是不可变的.但当我们在Java泛 ...

  4. C&num;4&period;0中的协变和逆变

    原文地址 谈谈.Net中的协变和逆变 关于协变和逆变要从面向对象继承说起.继承关系是指子类和父类之间的关系:子类从父类继承所以子类的实例也就是父类的实例.比如说Animal是父类,Dog是从Anima ...

  5. Java语言中的协变和逆变&lpar;zz&rpar;

    转载声明: 本文转载至:http://swiftlet.net/archives/1950 协变和逆变指的是宽类型和窄类型在某种情况下的替换或交换的特性.简单的说,协变就是用一个窄类型替代宽类型,而逆 ...

  6. C&num;4&period;0新增功能03 泛型中的协变和逆变

    连载目录    [已更新最新开发文章,点击查看详细] 协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体 ...

  7. Java中的协变与逆变

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

  8. &period;NET泛型中的协变与逆变

    泛型的可变性:协变性和逆变性 实质上,可变性是以一种类型安全的方式,将一个对象作为另一个对象来使用. 我们已经习惯了普通继承中的可变性:例如,若某方法声明返回类型为Stream,在实现时可以返回一个M ...

  9. &period;Net中委托的协变和逆变详解

    关于协变和逆变要从面向对象继承说起.继承关系是指子类和父类之间的关系:子类从父类继承所以子类的实例也就是父类的实例.比如说Animal是父类,Dog是从Animal继承的子类:如果一个对象的类型是Do ...

随机推荐

  1. XidianOJ 1020 ACMer去刷题吧

    题目描述 刷题是每个ACMer必由之路,已知某oj上有n个题目,第i个题目小X能做对的概率为Pi(0<=Pi<=1,1<=i<=n) 求小X至少做对k道题的概率 输入 第一行输 ...

  2. Laravel教程 六:表单 Forms

    Laravel教程 六:表单 Forms 此文章为原创文章,未经同意,禁止转载. Form laravel 5.2 之后请使用 laravelcollective/html 替换 illuminate ...

  3. chmod

    0表示没有权限,1表示可执行权限,2表示可写权限,4表示可读权限数字与字符对应关系如下:r=4,w=2,x=1若要rwx属性则4+2+1=7若要rw-属性则4+2=6:若要r-x属性则4+1=5命令: ...

  4. linkinFrame--用maven搭项目结构

    OK,老早想写一套自己的web框架,然后也一直在看开源的一些框架源码.今天开始正式开始写自己的javaWeb框架,暂时就定义linkinFrame好了. 为什么要写一套自己的框架? 其实这是一个比较矛 ...

  5. Maven入门-运行struts项目进行测试&lpar;三&rpar;

    maven运行struts项目进行测试: 在入门二中已经导入struts的jar包. 此时的pom.xml文件 <project xmlns="http://maven.apache. ...

  6. 使用JSON实现分页

    使用JSON实现分页可直接用 Fenye.html <!DOCTYPE html> <html> <head> <title>JSON分页</ti ...

  7. 解决NSImage绘制的时候图像模糊

    Mac下NSImage绘制模糊的原因之一是draw到了非整数像素上,框架在渲染的时候就会模糊. 针对这一原因写了以下工具: /** * @brief 一劳永逸的解决NSImage绘制的时候绘到浮点值像 ...

  8. python学习——简介和入门

    一.Python简介: Python的创始人为吉多·范罗苏姆(Guido van Rossum).1989年的圣诞节期间,吉多·范罗苏姆为了在阿姆斯特丹打发时间,决心开发一个新的脚本解释程序,作为AB ...

  9. window&period;onload&equals;function&lpar;&rpar;&lbrace;&rcub;和&dollar;&lpar;function&lpar;&rpar;&lbrace;&rcub;&rpar;的区别

    1.执行的个数的不同: window.onload()只会执行最后一个,些多个也会被最后一个覆盖. $(function(){})可以写多个,也会执行多个,按照从上至下的顺讯执行 2.执行时间上的不同 ...

  10. 写sql语句统计各个学生各科成绩(case when用法)

    尊重原创:http://blog.csdn.net/love_java_cc/article/details/78268326 有如下一张表score: 建表语句: CREATE TABLE `sco ...