随笔- 33  文章- 0  评论- 353 

Visual Studio 2010 CTP亮相的C#4VB10,虽然在支持语言新特性方面走了相当不一样的两条路:C#着重增加后期绑定和与动态语言相容的若干特性,VB10着重简化语言和提高抽象能力;但是两者都增加了一项功能:泛型类型的协变(covariant)和反变(contravariant)。许多人对其了解可能仅限于增加的in/out关键字,而对其诸多特性有所不知。下面我们就对此进行一些详细的解释,帮助大家正确使用该特性。

背景知识:协变和反变

很多人可能不不能很好地理解这些来自于物理和数学的名词。我们无需去了解他们的数学定义,但是至少应该能分清协变和反变。实际上这个词来源于类型和类型之间的绑定。我们从数组开始理解。数组其实就是一种和具体类型之间发生绑定的类型。数组类型Int32[]就对应于Int32这个原本的类型。任何类型T都有其对应的数组类型T[]。那么我们的问题就来了,如果两个类型TU之间存在一种安全的隐式转换,那么对应的数组类型T[]U[]之间是否也存在这种转换呢?这就牵扯到了将原本类型上存在的类型转换映射到他们的数组类型上的能力,这种能力就称为“可变性(Variance)”。在.NET世界中,唯一允许可变性的类型转换就是由继承关系带来的“子类引用->父类引用”转换。举个例子,就是String类型继承自Object类型,所以任何String的引用都可以安全地转换为Object引用。我们发现String[]数组类型的引用也继承了这种转换能力,它可以转换成Object[]数组类型的引用,数组这种与原始类型转换方向相同的可变性就称作协变(covariant

由于数组不支持反变性,我们无法用数组的例子来解释反变性,所以我们现在就来看看泛型接口和泛型委托的可变性。假设有这样两个类型:TSubTParent的子类,显然TSub型引用是可以安全转换为TParent型引用的。如果一个泛型接口IFoo<T>IFoo<TSub>可以转换为IFoo<TParent>的话,我们称这个过程为协变,而且说这个泛型接口支持对T的协变。而如果一个泛型接口IBar<T>IBar<TParent>可以转换为T<TSub>的话,我们称这个过程为反变(contravariant而且说这个接口支持对T的反变。因此很好理解,如果一个可变性和子类到父类转换的方向一样,就称作协变;而如果和子类到父类的转换方向相反,就叫反变性。你记住了吗?

.NET 4.0引入的泛型协变、反变性

刚才我们讲解概念的时候已经用了泛型接口的协变和反变,但在.NET 4.0之前,无论C#还是VB里都不支持泛型的这种可变性。不过它们都支持委托参数类型的协变和反变。由于委托参数类型的可变性理解起来抽象度较高,所以我们这里不准备讨论。已经完全能够理解这些概念的读者自己想必能够自己去理解委托参数类型的可变性。在.NET 4.0之前为什么不允许IFoo<T>进行协变或反变呢?因为对接口来讲,T这个类型参数既可以用于方法参数,也可以用于方法返回值。设想这样的接口

Interface IFoo(Of T)

    Sub Method1(ByVal param As T)

    Function Method2() As T

End Interface

interface IFoo<T>

{

    void Method1(T param);

    T Method2();

}

如果我们允许协变,从IFoo<TSub>IFoo<TParent>转换,那么IFoo.Method1(TSub)就会变成IFoo.Method1(TParent)。我们都知道TParent是不能安全转换成TSub的,所以Method1这个方法就会变得不安全。同样,如果我们允许反变IFoo<TParent>IFoo<TSub>,则TParent IFoo.Method2()方法就会变成TSub IFoo.Method2(),原本返回的TParent引用未必能够转换成TSub的引用,Method2的调用将是不安全的。有此可见,在没有额外机制的限制下,接口进行协变或反变都是类型不安全的。.NET 4.0改进了什么呢?它允许在类型参数的声明时增加一个额外的描述,以确定这个类型参数的使用范围。我们看到,如果一个类型参数仅仅能用于函数的返回值,那么这个类型参数就对协变相容。而相反,一个类型参数如果仅能用于方法参数,那么这个类型参数就对反变相容。如下所示:

Interface ICo(Of Out T)

    Function Method() As T

End Interface

 

Interface IContra(Of In T)

    Sub Method(ByVal param As T)

End Interface

interface ICo<out T>

{

    T Method();

}

 

interface IContra<in T>

{

    void Method(T param);

}

可以看到C#4VB10都提供了大同小异的语法,用Out来描述仅能作为返回值的类型参数,用In来描述仅能作为方法参数的类型参数。一个接口可以带多个类型参数,这些参数可以既有In也有Out,因此我们不能简单地说一个接口支持协变还是反变,只能说一个接口对某个具体的类型参数支持协变或反变。比如若有IBar<in T1, out T2>这样的接口,则它对T1支持反变而对T2支持协变。举个例子来说,IBar<object, string>能够转换成IBar<string, object>,这里既有协变又有反变。

.NET Framework中,许多接口都仅仅将类型参数用于参数或返回值。为了使用方便,在.NET Framework 4.0里这些接口将重新声明为允许协变或反变的版本。例如IComparable<T>就可以重新声明成IComparable<in T>,而IEnumerable<T>则可以重新声明为IEnumerable<out T>。不过某些接口IList<T>是不能声明为inout的,因此也就无法支持协变或反变。

下面提起几个泛型协变和反变容易忽略的注意事项:

1.       仅有泛型接口和泛型委托支持对类型参数的可变性,泛型类或泛型方法是不支持的。

2.       值类型不参与协变或反变,IFoo<int>永远无法变成IFoo<object>,不管有无声明out。因为.NET泛型,每个值类型会生成专属的封闭构造类型,与引用类型版本不兼容。

3.       声明属性时要注意,可读写的属性会将类型同时用于参数和返回值。因此只有只读属性才允许使用out类型参数,只写属性能够使用in参数。

协变和反变的相互作用

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

Interface IFoo(Of In T)

 

End Interface

 

Interface IBar(Of In T)

    Sub Test(ByVal foo As IFoo(Of T)) '对吗?

End Interface

interface IFoo<in T>

{

 

}

 

interface IBar<in T>

{

    void Test(IFoo<T> foo); //对吗?

}

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

Interface IFoo(Of In T)

 

End Interface

 

Interface IBar(Of Out T)

    Sub Test(ByVal foo As IFoo(Of T))

End Interface

interface IFoo<in T>

{

 

}

 

interface IBar<out T>

{

    void Test(IFoo<T> foo);

}

什么?明明是out参数,我们却要将其用于方法的参数才合法?初看起来的确会有一些惊奇。我们需要费一些周折来理解这个问题。现在我们考虑IBar<string>,它应该能够协变成IBar<object>,因为stringobject的子类。因此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(Of Out T)

 

End Interface

 

Interface IFooContra(Of In T)

 

End Interface

 

Interface IBar(Of Out T1, In T2)

    Function Test1() As IFooCo(Of T1)

    Function Test2() As IFooContra(Of T2)

End Interface

interface IFooCo<out T>

{

}

 

interface IFooContra<in T>

{

}

 

interface IBar<out T1, in T2>

{

    IFooCo<T1> Test1();

    IFooContra<T2> Test2();

}

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

总结

经过本文的讲解,大家应该已经初步了解的协变和反变的含义,能够分清协变、反变的过程。我们还讨论了.NET 4.0支持泛型接口、委托的协变和反变的新功能和新语法。最后我们还套了论的协变、反变与函数参数、返回值的相互作用原理,以及由此产生的奇妙写法。我希望大家看了我的文章后,能够将这些知识用于泛型程序设计当中,正确运用.NET 4.0的新增功能。祝大家使用愉快!

 posted on 2008-11-22 17:48 装配脑袋 阅读(3143) 评论(35)  编辑 收藏 网摘
Body:281.25,BeforeCate:0,15.625
#1楼    回复  引用    
 iiiiiiiiiii[未注册用户] | 2008-11-22 18:10
我以前一直有这样一个问题, 不知道和协变有没有关系比如
IList<object> objs= new List<string>();
在不对objs进行Add操作的情况下是有意义的.
但objs.Add(new object())非法.

所以IEnumerable<object> strs = new List<string>();
适合理的

不知道这个和协变有没有什么关系

#2楼[楼主]    回复  引用  查看    
 装配脑袋       | 2008-11-22 18:12
@iiiiiiiiiii

.NET 4.0协变、反变就是希望让这些合理的转换变成合法的写法:) 同时类型安全性必须得以满足。

#3楼    回复  引用  查看    
 木野狐(Neil Chen)       | 2008-11-22 19:00
先支持了慢慢看。。
C#的泛型目前似乎还不是图灵完备的

假如我们把继承关系看作X(a,b) 某个模板看作f
那么协变是 X(a,b)=>X(f(a),f(b))
逆变是 X(a,b)=>X(f(b),f(a))

貌似是这意思 跟乘法分配率差不多......

#5楼    回复  引用  查看    
 G yc {Son of VB.NET}       | 2008-11-22 20:52
啊~看的好乱~~~

虚机换完后, 立刻出来新文章啊~~

#6楼[楼主]    回复  引用  查看    
 装配脑袋       | 2008-11-22 21:32
@winter-cn,我也不能总是不登录是吧
基本上你说的这个就是协变的定义了。.NET泛型如果想要有一定的元编程能力,必须具有(JIT时)特化能力。哪怕是显式特化就已经能够提供超越现在很远的抽象能力了。

#7楼    回复  引用  查看    
 Justin Shen       | 2008-11-22 21:47
我后来又想了一下,我们那个visitor模式应该是可以运行的,只是要声明为out而已。有高阶函数的时候,out和in的涵义已经完全不对了。

另外,interface本身就是高阶的,我会写个博客说明一下 =。=
另外之后的另外:你星期一把dev10的VPC给我,我要试验一下 = =

#8楼    回复  引用  查看    
 Justin Shen       | 2008-11-22 21:54
哦 不过 有一个重载上的问题。。。
#9楼    回复  引用    
 FZZZ[未注册用户] | 2008-11-22 22:12
我 关心的是Net包装API完整了吗?性能提高了吗?而不去关心语法糖
#10楼    回复  引用  查看    
 Clark Zheng       | 2008-11-22 22:18
脑袋还是强人,过来支持一下
#11楼    回复  引用    
--引用--------------------------------------------------
装配脑袋: @winter-cn,我也不能总是不登录是吧
基本上你说的这个就是协变的定义了。.NET泛型如果想要有一定的元编程能力,必须具有(JIT时)特化能力。哪怕是显式特化就已经能够提供超越现在很远的抽象能力了。
--------------------------------------------------------
啊?需要JIT时特化么?
就像C++那样编译前处理不好么?
难道现在的C#泛型是JIT时做的?

#12楼    回复  引用  查看    
 xiaopohai_long       | 2008-11-22 23:29
乱啦。~~
#13楼    回复  引用  查看    
 Angel Lucifer       | 2008-11-22 23:56
老实说,这样的语法改进是带着镣铐跳舞。
非常的别扭。

而且,如果陷入其中,很容易出现编写 C++ 代码时的心智障碍。
因为每次写代码之前总要考虑这限制,考虑那限制的。

#14楼    回复  引用  查看    
 木野狐(Neil Chen)       | 2008-11-23 01:03
天哪,这样搞下去的话我看不如直接用 python 这样的动态语言得了。
说实话要运用太复杂了些。

#15楼    回复  引用  查看    
 greater       | 2008-11-23 07:52
不错的介绍!
#16楼    回复  引用  查看    
 Anders Liu       | 2008-11-23 09:13
讲得太清晰啦!多谢脑袋兄!

特别是 参数的协变-反变交换 和 返回值的协变-反变一致。

出现了可变性之后,是不是Cast<T>扩展方法就没用了呢?

@木野狐(Neil Chen)
--引用--------------------------------------------------
木野狐(Neil Chen): 天哪,这样搞下去的话我看不如直接用 python 这样的动态语言得了。
说实话要运用太复杂了些。

--------------------------------------------------------
其实泛型主要是针对强类型的 强类型就难免有这种问题啊

我倒是想过与其加这么多关键字来扩展语言
不如定义一种编译期脚本算了

C++那图灵完备的模板元其实真的是很诡异......

#19楼    回复  引用  查看    
 Justin Shen       | 2008-11-23 11:26
这不是语法改进。协变是从CLR的虚机层面支持的特性。
@Justin Shen
CLR级的? 那难道IL变了?

#21楼    回复  引用  查看    
 Justin Shen       | 2008-11-23 11:58
在2.0里面 IEnumerable<Stream>,和IEnumerable<FileStream>是完全没有关系的两个类,即使FileStream继承自Stream。所以更别提什么协变不协变了。

我还没有看过生成的IL,但除了in/out关键字之外,IL可能需要多的部分不会太多。反而是虚机本身会有很多变化。

#22楼    回复  引用  查看    
 Anders Liu       | 2008-11-23 12:02
@winter-cn,我也不能总是不登录是吧

呵呵,IL没变。IL自2.0之后就支持可变性了。只不过这一功能直到C# 4.0才暴露在高级语言中。

#23楼[楼主]    回复  引用  查看    
 装配脑袋       | 2008-11-23 12:17
@winter-cn未登录
所有封闭构造类型,也就是泛型的实例化类型都是JIT时建造的。这个太显而易见了,否则定义在mscorlib.dll中的List'1怎么你在C#可以生成属于你自己的封闭构造类型呢?CLR会给值类型的封闭构造类型生成专属的类型,也就是连MethodTable都有不同实例。而所有引用类型享用同一个MethodTable,但是每个封闭构造版本不共享静态字段。

#24楼    回复  引用    
--引用--------------------------------------------------
装配脑袋: @winter-cn未登录
所有封闭构造类型,也就是泛型的实例化类型都是JIT时建造的。这个太显而易见了,否则定义在mscorlib.dll中的List'1怎么你在C#可以生成属于你自己的封闭构造类型呢?CLR会给值类型的封闭构造类型生成专属的类型,也就是连MethodTable都有不同实例。而所有引用类型享用同一个MethodTable,但是每个封闭构造版本不共享静态字段。
--------------------------------------------------------
嗯 有道理啊 原来一直以为是C++那样的词法分析前的行为呢

现在只能看着C#留口水了 伤心啊

#25楼    回复  引用    
--引用--------------------------------------------------
Anders Liu: @winter-cn,我也不能总是不登录是吧

呵呵,IL没变。IL自2.0之后就支持可变性了。只不过这一功能直到C# 4.0才暴露在高级语言中。
--------------------------------------------------------
--引用--------------------------------------------------
Justin Shen: 在2.0里面 IEnumerable&lt;Stream&gt;,和IEnumerable&lt;FileStream&gt;是完全没有关系的两个类,即使FileStream继承自Stream。所以更别提什么协变不协变了。

我还没有看过生成的IL,但除了in/out关键字之外,IL可能需要多的部分不会太多。反而是虚机本身会有很多变化。
--------------------------------------------------------
Thanks. I got it.

#26楼    回复  引用    
 FZZZ[未注册用户] | 2008-11-23 16:42
我关心的是Net的Api封装进程(互操作太麻烦了有时狠不得直接用C++)和它的性能,语法糖我是不关心的,也很害人的
#27楼    回复  引用    
@FZZZ
--引用--------------------------------------------------
FZZZ: 我关心的是Net的Api封装进程(互操作太麻烦了有时狠不得直接用C++)和它的性能,语法糖我是不关心的,也很害人的
--------------------------------------------------------

我不明白语法糖这词是哪来的 不过高级语言本身就是语法糖 为啥大家都喜欢写
a+b 而不喜欢
MOV AX,a
ADD AX,b


#28楼    回复  引用  查看    
 xjb       | 2008-11-23 20:26
收藏学习一下
#29楼    回复  引用  查看    
 阿不       | 2008-11-23 22:39
变得我已经乱了
不过这个技术可以解决泛型存在的类型转换问题
就是.NET 4.0还没有着手进入个人计划中,了解一下代码就应该能明白了

#30楼    回复  引用    
 Google 老总[未注册用户] | 2008-11-24 08:17
--引用--------------------------------------------------
FZZZ: 我关心的是Net的Api封装进程(互操作太麻烦了有时狠不得直接用C++)和它的性能,语法糖我是不关心的,也很害人的
--------------------------------------------------------

就你那C++水平,写出代码也是很害人的。

#31楼    回复  引用  查看    
 代震军       | 2008-11-24 10:14
关注中。。。
#32楼    回复  引用    
 IIM[未注册用户] | 2008-12-20 10:54
关注一下
#33楼    回复  引用  查看    
 银河       | 2009-03-01 20:45
“背景知识:协变和反变”中的:

而如果一个泛型接口IBar<T>,IBar<TParent>可以转换为T<TSub>的话,我们称这个过程为反变(contravariant),而且说这个接口支持对T的反变。

应该是:

而如果一个泛型接口IBar<T>,IBar<TParent>可以转换为IBar<TSub>的话,我们称这个过程为反变(contravariant),而且说这个接口支持对T的反变。

#34楼    回复  引用    
 路过而以[未注册用户] | 2009-03-03 09:22
.net 变得太快了
两年一个版本
不知有几个人能跟得上
我现在用的是VS2005
用到的只是它的一点点
更别提将来的协变和反变了
我看根本用不上
人说.NET是傻瓜式的开发
我觉得一点也不错
的确
用.NET开发是很方便
但我接触到的客户都不太喜欢它
因为太耗资源了
[我开发的是WIN程序,WEB当然就没这问题了]

#35楼    回复  引用  查看    
 Windie Chai       | 2009-05-27 11:58
俺有点儿晕,不过写得很好。
发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 1339058 C1PYbBnz0jw=



相关文章:


相关搜索:
VB10 C#4 .NET4 泛型 协变 反变

相关链接: