随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
.NET 4.0引入的泛型协变、反变性
刚才我们讲解概念的时候已经用了泛型接口的协变和反变,但在.NET 4.0之前,无论C#还是VB里都不支持泛型的这种可变性。不过它们都支持委托参数类型的协变和反变。由于委托参数类型的可变性理解起来抽象度较高,所以我们这里不准备讨论。已经完全能够理解这些概念的读者自己想必能够自己去理解委托参数类型的可变性。在.NET 4.0之前为什么不允许IFoo
Interface IFoo(Of T) Sub Method1(ByVal param As T) Function Method2() As T End Interface |
interface IFoo { void Method1(T param); T Method2(); } |
如果我们允许协变,从IFoo
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
在.NET Framework中,许多接口都仅仅将类型参数用于参数或返回值。为了使用方便,在.NET Framework 4.0里这些接口将重新声明为允许协变或反变的版本。例如IComparable
下面提起几个泛型协变和反变容易忽略的注意事项:
1. 仅有泛型接口和泛型委托支持对类型参数的可变性,泛型类或泛型方法是不支持的。
2. 值类型不参与协变或反变,IFoo
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 } |
你能看出上述代码有什么问题吗?我声明了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 } |
什么?明明是out参数,我们却要将其用于方法的参数才合法?初看起来的确会有一些惊奇。我们需要费一些周折来理解这个问题。现在我们考虑IBar
刚才提到了方法参数上协变和反变的相互影响。那么方法的返回值会不会有同样的问题呢?我们看如下代码:
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 IFooContra } |
我们看到和刚刚正好相反,如果一个接口需要对T进行协变或反变,那么这个接口所有方法的返回值类型必须支持对T同样方向的协变或反变。这就是方法返回值的协变-反变一致原则。也就是说,即使in参数也可以用于方法的返回值类型,只要借助一个可以反变的类型作为桥梁即可。如果对这个过程还不是特别清楚,建议也是写一些代码来进行实验。至此我们发现协变和反变有许多有趣的特性,以至于在代码里in和out都不像他们字面意思那么好理解。当你看到in参数出现在返回值类型,out参数出现在参数类型时,千万别晕倒,用本文的知识即可破解其中奥妙。
总结
经过本文的讲解,大家应该已经初步了解的协变和反变的含义,能够分清协变、反变的过程。我们还讨论了.NET 4.0支持泛型接口、委托的协变和反变的新功能和新语法。最后我们还套了论的协变、反变与函数参数、返回值的相互作用原理,以及由此产生的奇妙写法。我希望大家看了我的文章后,能够将这些知识用于泛型程序设计当中,正确运用.NET 4.0的新增功能。祝大家使用愉快!