随Visual Studio 2010 CTP亮相的C#4和VB10,虽然在支持语言新特性方面走了至关不同的两条路:C#着重增长后期绑定和与动态语言相容的若干特性,VB10着重简化语言和提升抽象能力;可是二者都增长了一项功能:泛型类型的协变(covariant)和反变(contravariant)。许多人对其了解可能仅限于增长的in/out关键字,而对其诸多特性有所不知。下面咱们就对此进行一些详细的解释,帮助你们正确使用该特性。数组
不少人可能不不能很好地理解这些来自于物理和数学的名词。咱们无需去了解他们的数学定义,可是至少应该能分清协变和反变。实际上这个词来源于类型和类型之间的绑定。咱们从数组开始理解。数组其实就是一种和具体类型之间发生绑定的类型。数组类型Int32[]就对应于Int32这个本来的类型。任何类型T都有其对应的数组类型T[]。那么咱们的问题就来了,若是两个类型T和U之间存在一种安全的隐式转换,那么对应的数组类型T[]和U[]之间是否也存在这种转换呢?这就牵扯到了将本来类型上存在的类型转换映射到他们的数组类型上的能力,这种能力就称为“可变性(Variance)”。在.NET世界中,惟一容许可变性的类型转换就是由继承关系带来的“子类引用->父类引用”转换。举个例子,就是String类型继承自Object类型,因此任何String的引用均可以安全地转换为Object引用。咱们发现String[]数组类型的引用也继承了这种转换能力,它能够转换成Object[]数组类型的引用,数组这种与原始类型转换方向相同的可变性就称做协变(covariant)。安全
因为数组不支持反变性,咱们没法用数组的例子来解释反变性,因此咱们如今就来看看泛型接口和泛型委托的可变性。假设有这样两个类型:TSub是TParent的子类,显然TSub型引用是能够安全转换为TParent型引用的。若是一个泛型接口IFoo<T>,IFoo<TSub>能够转换为IFoo<TParent>的话,咱们称这个过程为协变,并且说这个泛型接口支持对T的协变。而若是一个泛型接口IBar<T>,IBar<TParent>能够转换为T<TSub>的话,咱们称这个过程为反变(contravariant),并且说这个接口支持对T的反变。所以很好理解,若是一个可变性和子类到父类转换的方向同样,就称做协变;而若是和子类到父类的转换方向相反,就叫反变性。你记住了吗?函数
刚才咱们讲解概念的时候已经用了泛型接口的协变和反变,但在.NET 4.0以前,不管C#仍是VB里都不支持泛型的这种可变性。不过它们都支持委托参数类型的协变和反变。因为委托参数类型的可变性理解起来抽象度较高,因此咱们这里不许备讨论。已经彻底可以理解这些概念的读者本身想必可以本身去理解委托参数类型的可变性。在.NET 4.0以前为何不容许IFoo<T>进行协变或反变呢?由于对接口来说,T这个类型参数既能够用于方法参数,也能够用于方法返回值。设想这样的接口性能
Interface IFoo(Of T)spa Sub Method1(ByVal param As T)设计 Function Method2() As T继承 End Interface接口 |
interface IFoo<T>ci {数学 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#4和VB10都提供了大同小异的语法,用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>是不能声明为in或out的,所以也就没法支持协变或反变。
下面提起几个泛型协变和反变容易忽略的注意事项:
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>,由于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(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参数也能够用于方法的返回值类型,只要借助一个能够反变的类型做为桥梁便可。若是对这个过程还不是特别清楚,建议也是写一些代码来进行实验。至此咱们发现协变和反变有许多有趣的特性,以致于在代码里in和out都不像他们字面意思那么好理解。当你看到in参数出如今返回值类型,out参数出如今参数类型时,千万别晕倒,用本文的知识便可破解其中奥妙。
通过本文的讲解,你们应该已经初步了解的协变和反变的含义,可以分清协变、反变的过程。咱们还讨论了.NET 4.0支持泛型接口、委托的协变和反变的新功能和新语法。最后咱们还套了论的协变、反变与函数参数、返回值的相互做用原理,以及由此产生的奇妙写法。我但愿你们看了个人文章后,可以将这些知识用于泛型程序设计当中,正确运用.NET 4.0的新增功能。祝你们使用愉快!