作为一个从接触 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. .net中的协变和逆变

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

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

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

  4. C#4.0中的协变和逆变

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

  5. Java语言中的协变和逆变(zz)

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

  6. C#4.0新增功能03 泛型中的协变和逆变

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

  7. Java中的协变与逆变

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

  8. .NET泛型中的协变与逆变

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

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

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

随机推荐

  1. Tomcat启动后加载两次web.xml的问题(因为spring定时任务执行了俩次,引出此问题)

    http://www.linuxidc.com/Linux/2011-07/38779.htmhttp://jingyan.baidu.com/article/48206aeaf9422e216ad6 ...

  2. GCD的一点理解

    大家都知道GCD 有两种队列:一种是串行队列,一种是并发队列.什么是串行队列?串行队列就是队列中的代码块一个一个按顺序执行,每当上一个代码块执行结束后下一个代码块才会执行.打个比方,如果队列是一些首尾 ...

  3. vector预分配空间溢出

    vector 当一个vector预分配的存储空间用完之后,为维护其连续的对象数组,它必须在另外一个地方重新分配大块新的(更大的)存储空间,并把以前已有的对象拷贝到新的存储空间中去. // A clas ...

  4. 嵌入式web server——Goahead移植要点

    前言 在嵌入式设备中,在没有液晶显示的情况下,可以使用web来访问设备,查看设备的运行状态以及进行参数设置,类似于路由器设置.网上有很多关于各种web server的优劣的评论,在此不讨论,只是介绍其 ...

  5. Pintos-斯坦福大学操作系统Project详解-Project1

    转载请注明出处. 前言:  本实验来自斯坦福大学cs140课程,只限于教学用途,以下是他们对于Pintos系统的介绍:  Pintos is a simple operating system fra ...

  6. winform C#屏幕右下角弹出消息框并自动消失

    ①弹出信息框后慢慢下降消失 在主窗体中新增按钮重命名为btnShowAndDisappearMessages,在click事件中写如下代码: private void btnShowAndDisapp ...

  7. wordpress4.0.1源码学习和摘录--项目设置

    1.静态变量日期 define( 'MINUTE_IN_SECONDS', 60 ); define( 'HOUR_IN_SECONDS', 60 * MINUTE_IN_SECONDS ); def ...

  8. PHP扩展Redis编译安装

    PHP扩展Redis编译安装 1.下载PHP官方Redis源码包  wget http://pecl.php.net/get/redis-2.2.4.tgz  注:我用的是Redhat系统,ubunt ...

  9. 一个简单的webservice调用

    我们先创建一个简单空web应用程序 然后添加新建项目 //我们创建一个peson对象,产生数据标识返回 using System; using System.Collections.Generic; ...

  10. MAC 使用Jetbrains's产品

    Jetbrains's MAC 使用 ./gradle fatjar 或者 ./gradlew.sh fatjar java -jar build/lib/xx.jar 链接: http://pan. ...