C# 协变 逆变
看到过园子里面几篇协变和逆变的文章,但是总觉得写得不够清晰,文章这东西注重要是要把自己想表达的观点表达出来,这个过程应该是把复杂的东西消化出来从而简单化,清晰化,而不是故弄玄虚,反其道而行之,下面我们言归正传啦。
我们先来看一段MSDN原文给协变,逆变和变体下个定义:
A generic interface or delegate is called variant if its generic parameters are declared covariant or contravariant. Both C# and Visual Basic enable you to create your own variant interfaces and delegates.
如果泛型接口或委托的泛型参数声明为协变或逆变,则将该泛型接口或委托称为“变体”。 C# 和 Visual Basic 都允许您创建自己的变体接口和委托。
通俗解释:
变体定义:
带有协变或逆变参数的泛型接口或委托。也就是说协变和逆变主要关注点在泛型接口或委托。
那什么又是协变和逆变呢?
我们先来看下面一个来自MSDN的例子:
1 // 协变
2 IEnumerable<string>strings = new List<string>();
3 IEnumerable<object> objects = strings;
大家看到了么一个声明为IEnumerable<string>的 接口类型被赋给了一个更低 级别的IEnumerable<object>.
对,这就是协变。
再来看一个例子:
class Base
{
public static void PrintBases(IEnumerable<Base> bases)
{
foreach(Base b in bases)
{
Console.WriteLine(b);
}
}
}
class Derived : Base
{
public static void Main()
{
List<Derived> dlist = new List<Derived>();
Derived.PrintBases(dlist);
//由于IEnumerable<T>接口是协变的,所以PrintBases(IEnumerable<Base> bases)
//可以接收一个更加具体化的IEnumerable<Derived>作为其参数。
IEnumerable<Base> bIEnum = dlist;
}
}
下面给协变下个定义:
协变:让一个带有协变参数的泛型接口(或委托)可以接收类型更加精细化,具体化的泛型接口(或委托)作为参数,可以看成OO中多态的一个延伸。
OO:面向对象
// 逆变
// Assume that the following method is in the class:
// static void SetObject(object o) { }
Action<object> actObject = SetObject;
Action<string> actString = actObject;
//委托actString中以后要使用更加精细化的类型string不能再使用object啦!
string strHello(“Hello”);
actString(strHello);
大家看到了么?
一个声明为Action<object>的类型被赋给了一个Action<string>,大家都知道,Action<T>接收参数,没有返回值,所以其中的object和string是其参数,这个过程其实就是参数的约束更加强了,也就是说让参数类型更加精细化。
下面我们来给逆变下个定义:
逆变:让一个带有协变参数的泛型接口(或委托)可以接收粒度更粗的泛型接口或委托作为参数,这个过程实际上是参数类型更加精细化的过程。
一句话总结:
协变 让一个粗粒度接口(或委托)可以接收一个更加具体的接口(或委托)作为参数(或返回值);
逆变 让一个接口(或委托)的参数类型(或返回值)类型更加具体化,也就是参数类型更强,更明确。
通常,协变类型参数可用作委托的返回类型,而逆变类型参数可用作参数类型。
对于接口,协变类型参数可用作接口的方法的返回类型,而逆变类型参数可用作接口的方法的参数类型。
逆变(contravariant)与协变(covariant)是C#4新增的概念,许多书籍和博客都有讲解,我觉得都没有把它们讲清楚,搞明白了它们,可以更准确地去定义泛型委托和接口,这里我尝试画图详细解析逆变与协变。
http://msdn.microsoft.com/zh-cn/library/ee207183%28v=vs.100%29.aspx
变的概念
我们都知道.Net里或者说在OO的世界里,可以安全地把子类的引用赋给父类引用,例如:
1 2 3 |
|
而C#里又有泛型的概念,泛型是对类型系统的进一步抽象,比上面简单的类型高级,把上面的变化体现在泛型的参数上就是我们所说的逆变与协变的概念。通过在泛型参数上使用in或out关键字,可以得到逆变或协变的能力。下面是一些对比的例子:
协变(Foo<父类> = Foo<子类> ):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
逆变(Foo<子类> = Foo<父类>)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
来到这里我们看到有的能变,有的不能变,要知道以下几点:
以前的泛型系统(或者说没有in/out关键字时),是不能“变”的,无论是“逆”还是“顺(协)”。
当前仅支持接口和委托的逆变与协变,不支持类和方法。但数组也有协变性。
值类型不参与逆变与协变。
那么in/out是什么意思呢?
为什么加了它们就有了“变”的能力,是不是我们定义泛型委托或者接口都应该添加它们呢?
原来,在泛型参数上添加了in关键字作为泛型修饰符的话,那么那个泛型参数就只能用作方法的输入参数,或者只写属性的参数,不能作为方法返回值等,总之就是只能是“入”,不能出。out关键字反之。
当尝试编译下面这个把in泛型参数用作方法返回值的泛型接口时:
1 2 3 4 |
|
出现了如下编译错误:
错误 1 方差无效: 类型参数“T”必须为“CovarianceAndContravariance.IPlayB<T>.Test()”上有效的协变式。“T”为逆变。
到这里,我们大致知道了逆变与协变的相关概念,
那么为什么把泛型参数限制为in或者out就可以“变”呢?
下面尝试画图解释原理。
协变不是理所当然的,逆变也没有“逆”
我们先来看看不支持逆变与协变的泛型,把子类赋给父类,再执行父类方法的具体流程,对于这样一个简单的例子的Test方法:
1 2 3 4 5 6 7 8 9 10 |
|
它实际的流程是这样的:
即调用父类的方法,其实实际是调用子类的方法。
可以看到,这个方法能够安全的调用,需要两个条件:
1.变式(父)的方法参数能安全转为原式(子)的参数;
2.原式(子)的返回值能安全的转为变式的返回值。
不幸的是参数的流向跟返回值的流向是相反的,所以对于既是in,又是out的泛型参数来说,肯定是行不通的,其中一个方向必然不能安全转换的。
例如,对上面的例子,我们尝试“变”:
1 2 3 4 |
|
这里的“实际流程”如下,可以看到,参数那里是object是不能安全转换为string,所以编译失败:
看到这里如果都明白的话,我们不难得到逆变与协变的”实际流程图”(记住,它们是有in/out限制的):
可以看到,从”实际流程图”来看,逆变根本没有“逆”,都离不开只能安全地把子类的引用赋给父类引用这个根本。
来到这里应该基本理解逆变与协变了,不过装配脑袋的这篇文章有个更高级的问题,原文也有解答,这里我用上面画图的方式去理解它。
图解逆变与协变的相互作用
问题的提出,你知道那个正确吗?
1 2 3 4 5 6 7 8 9 10 11 |
|
答案是,如果是in的话,会编译失败,out才正确(当然不要泛型修饰符也能通过编译,但IFoo就没有协变能力了)。这里的意思就是说,一个有协变(逆变)能力的泛型(IBar),作为另一个泛型(IFoo)的参数时,影响到了它(IFoo)的泛型的定义。乍一看以为是in的其中一个陷阱是T是在 Test方法的参数里的,所以以为是in。但这里Test的参数根本不是T,而是IBar<T>。
我们画个图来理解它。既然out可以通过,那么它的“协变流程图”应该如下:
图跟前面那些大致一样,但理解它要跟问题相反(上面问题是先定义好IBar,再去定义IFoo)。1.我们定义好一个有协变能力的IFoo,这是前提。2.可以推出,上面的流程是成立的。3.这个流程重点是参数流向,要使整个流程成立,就必须使IBar<string> = IBar<object>成立,这不就是逆变吗?整个结论就是,有协变能力的IFoo要求它的泛型参数(IBar)有逆变能力。其实根据上面的箭头也可以理解,因为原式和变式的变向跟参数的变向是相反的,导致了它们要有相反的能力,这就是装配脑袋文章说的:方法参数的协变-反变互换原则。根据这个原理,也很容易得出,如果Test方法的返回值是IBar<T>,而不是参数,那么就要求IBar<T>要有协变能力,因为返回值的箭头与原式和变式的变向的箭头是同向的。
The End!
随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引入的泛型协变、反变性
刚才我们讲解概念的时候已经用了泛型接口的协变和反变,但在.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#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的新增功能。祝大家使用愉快!
关于协变(out)和逆变(in)
2013-03-04 16:37:54| 分类: 默认分类|举报|字号 订阅
在博客园看了几位大佬的关于变的文章,耐何自身修为浅薄,太冒然去练这么高深的秘籍了,虽然没有走火入魔但也练的头昏脑胀,血气逆行. 然后自己写了几行代码试试了,算是心得,也不知对没对
总结
in逆变只能做参数类型
IF<派生类>=new F<基类>
out 协变做返回类型
IF<基类>=new F<派生类>,IF为接口,F为实现了IF的类
没有变的IF IF<派生类>=new F<基类>与 IF<基类>=new F<派生类>都不支持
一般的类只可以基类=new 派生类
以下是代码,主要试类型的转变,已编译通过并运行正常在vs2010
public
ActionResult Index()
{
IA_in<object> a_obj=null;
IA_in<string> a_str = null;
a_str = a_obj; //逆变成功
IA_in<string> a1
= new A_in<object>(); //逆变成功
IB_out<object> b1
= new B_out<string>(); //协变成功
//以下编译错误
//a_obj = a_str;
//IA_in<object> a2 = new A_in<string>();
//IB_out<string> b2 = new B_out<object>();
//未变即不能逆也不能协
//IC<object> c1 = new C<string>();
//IC<string> c2 = new C<object>();
return View();
}
public
class A_in<T> : IA_in<T>{}
public class B_out<T> : IB_out<T>{}
public class C<T>:IC<T>{ }
public interface IA_in<in T>{}
public interface IB_out<out T>{}
public interface IC<T> { }
注意:泛型是2.0就有的,in out 约束是4.0才有的
再来几个图
1、成功类型
2、协变失败,注意委托主义时返回类型TResult 没有out约束了
3、逆变失败,注意定义委托时参数类型T1 没有in 约束
附件列表