C#4.0 协变 和 抗变(逆变)
关于协变和逆变要从面向对象继承说起。继承关系是指子类和父类之间的关系;子类从父类继承所以子类的实例也就是父类的实例。比如说Animal是父类,Dog是从Animal继承的子类;如果一个对象的类型是Dog,那么他必然是Animal。 协变逆变正是利用继承关系 对不同参数类型或返回值类型 的委托或者泛型接口之间做转变。
如果一个方法要接受Dog参数,那么另一个接受Animal参数的方法肯定也可以接受这个方法的参数,这是Animal向Dog方向的转变是逆变。如果一个方法要求的返回值是Animal,那么返回Dog的方法肯定是可以满足其返回值要求的,这是Dog向Animal方向的转变是协变。
由子类向父类方向转变是协变 协变用于返回值类型用out关键字 由父类向子类方向转变是逆变 逆变用于方法的参数类型用in关键字
一、定义
一个可变性和子类到父类转换的方向一样,就称作协变;而如果和子类到父类的转换方向相反,就叫抗变!
那到底这个协变或者抗变有什么实际利用价值呢? 其价值就在于,在.net 4.0之前可以这么写:
1 2 |
Sharp sharp = <span class="hljs-keyword">new</span> Rectange(); |
但是却不能这么写:
1 2 |
IEnumerable<Sharp> sharps = <span class="hljs-keyword">new</span> List<Rectange>(); |
4.0之后,可以允许按上面的写法了,因为泛型接口IEnumerable<T>
被声明成如下:
1 2 |
public interface IEnumerable<out T> : IEnumerable |
数组不支持抗变。在.Net 4.0之后,支持协变和抗变的有两种类型:泛型接口和泛型委托。
二、泛型接口中的协变和抗变
接下来定义一个泛型接口:
1 2 |
public interface ICovariant<T> // Covariant 协变的 |
并且让上面的两个类各自继承一下该接口:
1 2 3 4 5 6 7 8 |
public class Sharp : ICovariant<Sharp> { } public class Rectange : Sharp,ICovariant<Rectange> { } |
编写测试代码:
1 2 3 4 5 6 7 8 |
<span class="hljs-function"><span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Main</span>(<span class="hljs-params"><span class="hljs-keyword">string</span>[] args</span>) </span>{ ICovariant<Sharp> isharp = <span class="hljs-keyword">new</span> Sharp(); ICovariant<Rectange> irect = <span class="hljs-keyword">new</span> Rectange(); isharp = irect; } |
编译并不能通过,原因是无法将ICovariant<Rectange>
隐式转化为ICovariant<Sharp>
!
再将接口修改为:
1 2 3 4 |
public interface ICovariant<out T> { } |
编译顺利通过。这里我为泛型接口的类型参数增加了一个修饰符out
,它表示这个泛型接口支持对类型T的协变。
即:如果一个泛型接口IFoo<T>
,IFoo<TSub>
可以转换为IFoo<TParent>
的话,我们称这个过程为协变,而且说“这个泛型接口支持对T的协变”。
那我如果反过来呢,考虑如下代码:
1 2 3 4 5 6 7 8 9 |
<span class="hljs-function"><span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Main</span>(<span class="hljs-params"><span class="hljs-keyword">string</span>[] args</span>) </span>{ ICovariant<Sharp> isharp = <span class="hljs-keyword">new</span> Sharp(); ICovariant<Rectange> irect = <span class="hljs-keyword">new</span> Rectange(); irect = isharp; <span class="hljs-comment">// isharp =irect; </span> } |
发现编译又不通过了, 原因是无法将 ICovariant<Sharp>
隐式转化为 ICovariant<Rectange>
!
将接口修改为:
1 2 3 4 |
public interface ICovariant<in T> { } |
编译顺利通过。这里我将泛型接口的类型参数T修饰符修改成in
,它表示这个泛型接口支持对类型参数T的抗变。 即:如果一个泛型接口IFoo<T>
,IFoo<TParent>
可以转换为IFoo<TSub>
的话,我们称这个过程为抗变(contravariant),而且说“这个泛型接口支持对T的抗变”!
泛型接口并不单单只有一个参数,所以我们不能简单地说一个接口支持协变还是抗变,只能说一个接口对某个具体的类型参数支持协变或抗变,如ICovariant<out T1,in T2>
说明该接口对类型参数T1支持协变,对T2支持抗变。
举个例子就是:ICovariant<Rectange,Sharp>
能够转化成ICovariant<Sharp,Rectange>
,这里既有协变也有抗变。
以上都是接口并没有属性或方法的情形,接下来给接口添加一些方法:
1 2 3 4 5 6 7 |
//这时候,无论如何修饰T,都不能编译通过 public interface ICovariant<out T> { T Method1(); void Method2(T param); } |
发现无论用out
还是in
修饰T参数,根本编译不通过。
原因是,我把仅有的一个类型参数T既用作函数的返回值类型,又用作函数的参数类型。
所以: 1)当我用out修饰时,即允许接口对类型参数T协变,也就是满足从ICovariant<Rectange>
到ICovariant<Sharp>
转换,Method1返回值Rectange到Sharp转换没有任何问题:
1 2 3 4 5 6 |
ICovariant<Sharp> isharp = <span class="hljs-keyword">new</span> Sharp(); ICovariant<Rectange> irect = <span class="hljs-keyword">new</span> Rectange(); isharp = irect; Sharp sharp = isharp.Method1(); |
但是对于把T作为参数类型的方法Method2(Rectange)会去替换Method2(Sharp):
1 2 3 4 5 6 |
ICovariant<Sharp> isharp = <span class="hljs-keyword">new</span> Sharp(); ICovariant<Rectange> irect = <span class="hljs-keyword">new</span> Rectange(); isharp = irect; isharp.Method2(<span class="hljs-keyword">new</span> Sharp()); |
即如果执行最后一行代码,会发现参数中,Sharp类型并不能安全转化成Rectange类型,因为Method2(Sharp)实际上已经被替换成 Method2(Rectange) !
2)同样,当我用in修饰时, 即允许接口对类型参数T抗变,也就是满足从ICovariant<Sharp>
到ICovariant<Rectange>
转换:
1 2 3 4 5 6 7 |
ICovariant<Sharp> isharp = <span class="hljs-keyword">new</span> Sharp(); ICovariant<Rectange> irect = <span class="hljs-keyword">new</span> Rectange(); <span class="hljs-comment">//isharp = irect; </span> irect = isharp; irect.Method2(<span class="hljs-keyword">new</span> Rectange()); |
Method2(Sharp)会去替换Method2(Rectange),所以上面的最后一句代码无论以Rectange类型还是Sharp类型为参数都没有任何问题; 但是Method1返回的将是Sharp类型:
1 2 3 4 5 6 7 |
ICovariant<Sharp> isharp = <span class="hljs-keyword">new</span> Sharp(); ICovariant<Rectange> irect = <span class="hljs-keyword">new</span> Rectange(); <span class="hljs-comment">//isharp = irect; </span> irect = isharp; Rectange rect = irect.Method1(); |
执行最后一句代码,同样将会是不安全的!
综上:在没有额外机制的限制下,接口进行协变或抗变都是类型不安全的。.NET 4.0有了改进,它允许在类型参数的声明时增加一个额外的描述,以确定这个类型参数的使用范围,这个额外的描述即in,out修饰符,它们俩的用法如下: 如果一个类型参数仅仅能用于函数的返回值,那么这个类型参数就对协变相容,用out修饰。而相反,一个类型参数如果仅能用于方法参数,那么这个类型参数就对抗变相容,用in修饰。
所以,需要将上面的接口拆成两个接口即可:
1 2 3 4 5 6 7 8 9 10 11 |
public interface ICovariant<out T> { T Method1(); } public interface IContravariant<in T> { void Method2(T param); } |
.net中很多接口都仅将参数用于函数返回类型或函数参数类型,如:
1 2 3 4 5 |
public interface IComparable<in T> public interface IEnumerable<out T> : IEnumerable |
几个重要的注意点: 1.仅有泛型接口和泛型委托支持对类型参数的可变性,泛型类或泛型方法是不支持的。 2.值类型不参与协变或抗变,IFoo<int>
永远无法协变成IFoo<object>
,不管有无声明out。因为.NET泛型,每个值类型会生成专属的封闭构造类型,与引用类型版本不兼容。 3.声明属性时要注意,可读写的属性会将类型同时用于参数和返回值。因此只有只读属性才允许使用out类型参数,只写属性能够使用in参数。
接下来将接口代码改成:
1 2 3 4 5 6 7 8 9 10 11 |
public interface ICovariant<out T> { T Method1(); void Method3(IContravariant<T> param); } public interface IContravariant<in T> { void Method2(T param); } |
同样是可以编译通过的.
我们需要费一些周折来理解这个问题。现在我们考虑ICovariant<Rectange>
,它应该能够协变成ICovariant<Sharp>
,因为Rectange是Sharp的子类。因此Method3(Rectange)也就协变成了Method3(Sharp)。当我们调用这个协变,Method3(Sharp)必须能够安全变成Method3(Rectange)才能满足原函数的需要(具体原因上面已经示例过了)。这里对Method3的参数类型要求是Sharp能够抗变成Rectange!也就是说,如果一个接口需要对类型参数T协变,那么这个接口所有方法的参数类型必须支持对类型参数T的抗变(如果T有作为某些方法的参数类型)。 同理我们也可以看出,如果接口要支持对T抗变,那么接口中方法的参数类型都必须支持对T协变才行。这就是方法参数的协变-抗变互换原则。所以,我们并不能简单地说out参数只能用于方法返回类型参数,它确实只能直接用于声明返回值类型,但是只要一个支持抗变的类型协助,out类型参数就也可以用于参数类型!(即上面的例子),换句话说,in除了直接声明方法参数类型支持抗变之外,也仅能借助支持协变的类型才能用于方法参数,仅支持对T抗变的类型作为方法参数类型也是不允许的。
既然方法类型参数协变和抗变有上面的互换影响。那么方法的返回值类型会不会有同样的问题呢? 将接口修改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public interface IContravariant<in T> { } public interface ICovariant<out T> { } public interface ITest<out T1, in T2> { ICovariant<T1> test1(); IContravariant<T2> test2(); } |
我们看到和刚刚正好相反,如果一个接口需要对类型参数T进行协变或抗变,那么这个接口所有方法的返回值类型必须支持对T同样方向的协变或抗变(如果有某些方法的返回值是T类型)。这就是方法返回值的协变-抗变一致原则。也就是说,即使in参数也可以用于方法的返回值类型,只要借助一个可以抗变的类型作为桥梁即可。
三、泛型委托中的协变和抗变
泛型委托的协变抗变,与泛型接口协变抗变类似。继续延用Sharp,Rectange类作为示例: 新建一个简单的泛型接口:
1 2 |
<span class="hljs-keyword">public</span> <span class="hljs-keyword">delegate</span> <span class="hljs-keyword">void</span> MyDelegate1<T>(); |
测试代码:
1 2 3 4 |
MyDelegate1<Sharp> sharp1 = <span class="hljs-keyword">new</span> MyDelegate1<Sharp>(MethodForParent1); MyDelegate1<Rectange> rect1 = <span class="hljs-keyword">new</span> MyDelegate1<Rectange>(MethodForChild1); sharp1 = rect1; |
其中两个方法为:
1 2 3 4 5 6 7 8 9 |
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">MethodForParent1</span>() </span>{ Console.WriteLine(<span class="hljs-string">"Test1"</span>); } <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">MethodForChild1</span>() </span>{ Console.WriteLine(<span class="hljs-string">"Test2"</span>); } |
编译并不能通过,因为无法将MyDelegate1<Rectange>
隐式转化为MyDelegate1<Sharp>
,接下来我将接口修改为支持对类型参数T协变,即加out修饰符:
1 2 |
<span class="hljs-keyword">public</span> <span class="hljs-keyword">delegate</span> <span class="hljs-keyword">void</span> MyDelegate1<<span class="hljs-keyword">out</span> T>(); |
编译顺利用过。 同样,如果反过来,对类型参数T进行抗变:
1 2 3 4 5 |
MyDelegate1<Sharp> sharp1 = <span class="hljs-keyword">new</span> MyDelegate1<Sharp>(MethodForParent1); MyDelegate1<Rectange> rect1 = <span class="hljs-keyword">new</span> MyDelegate1<Rectange>(MethodForChild1); <span class="hljs-comment">//sharp1 = rect1; </span> rect1 = sharp1; |
只需将修饰符改为in即可:
1 2 |
<span class="hljs-keyword">public</span> <span class="hljs-keyword">delegate</span> <span class="hljs-keyword">void</span> MyDelegate1<<span class="hljs-keyword">in</span> T>(); |
考虑第二个委托:
1 2 |
<span class="hljs-keyword">public</span> <span class="hljs-keyword">delegate</span> T MyDelegate2<<span class="hljs-keyword">out</span> T>(); |
测试代码:
1 2 3 4 |
MyDelegate2<Sharp> sharp2 = <span class="hljs-keyword">new</span> MyDelegate2<Sharp>(MethodForParent2); MyDelegate2<Rectange> rect2 = <span class="hljs-keyword">new</span> MyDelegate2<Rectange>(MethodForChild2); sharp2 = rect2; |
其中两个方法为:
1 2 3 4 5 6 7 8 9 |
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Sharp <span class="hljs-title">MethodForParent2</span>() </span>{ <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Sharp(); } <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Rectange <span class="hljs-title">MethodForChild2</span>() </span>{ <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Rectange(); } |
该委托对类型参数T进行协变没有任何问题,编译通过;如果我要对T进行抗变呢?是否只要将修饰符改成in就OK了? 测试如下:
1 2 3 4 5 6 7 |
<span class="hljs-keyword">public</span> <span class="hljs-keyword">delegate</span> T MyDelegate2<<span class="hljs-keyword">in</span> T>(); MyDelegate2<Sharp> sharp2 = <span class="hljs-keyword">new</span> MyDelegate2<Sharp>(MethodForParent2); MyDelegate2<Rectange> rect2 = <span class="hljs-keyword">new</span> MyDelegate2<Rectange>(MethodForChild2); <span class="hljs-comment">//sharp2 = rect2; </span> rect2 = sharp2; |
错误如下: 变体无效: 类型参数“T”必须为对于“MyDelegate2<T>.Invoke()
”有效的 协变式。“T”为 逆变。 意思就是:这里的类型参数T已经被声明成抗变,如果上面的最后一句有效,那么以后rect2()执行结果返回的将是一个Sharp类型的实例, 如果再出现这种代码:
1 2 |
Rectange rectange = rect2(); |
那么这将是一个从Sharp类到Rectange类的不安全的类型转换!所以如果类型参数T抗变,并且要用于方法返回类型,那么方法的返回类型也必须支持抗变。即上面所说的方法返回类型协变-抗变一致原则。
那么如何对上面的返回类型进行抗变呢?很简单,只要借助一个支持抗变的泛型委托作为方法返回类型即可:
1 2 3 |
<span class="hljs-keyword">public</span> <span class="hljs-keyword">delegate</span> Contra<T> MyDelegate2<<span class="hljs-keyword">in</span> T>(); <span class="hljs-keyword">public</span> <span class="hljs-keyword">delegate</span> <span class="hljs-keyword">void</span> Contra<<span class="hljs-keyword">in</span> T>(); |
具体的方法也需要对应着修改一下:
1 2 3 4 5 6 7 8 9 |
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Contra<Sharp> <span class="hljs-title">MethodForParent3</span>() </span>{ <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Contra<Sharp>(MethodForParent1); } <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Contra<Rectange> <span class="hljs-title">MethodForChild3</span>() </span>{ <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Contra<Rectange>(MethodForChild1); } |
测试代码:
1 2 3 4 |
MyDelegate2<Sharp> sharp2 = <span class="hljs-keyword">new</span> MyDelegate2<Sharp>(MethodForParent3); MyDelegate2<Rectange> rect2 = <span class="hljs-keyword">new</span> MyDelegate2<Rectange>(MethodForChild3); rect2 = sharp2; |
编译通过。
接下来考虑第三个委托:
1 2 |
<span class="hljs-keyword">public</span> <span class="hljs-keyword">delegate</span> T MyDelegate3<T>(T param); |
首先,对类型参数T进行协变:
1 2 |
<span class="hljs-keyword">public</span> <span class="hljs-keyword">delegate</span> T MyDelegate3<<span class="hljs-keyword">out</span> T>(T param); |
对应的方法及测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Sharp <span class="hljs-title">MethodForParent4</span>(<span class="hljs-params">Sharp param</span>) </span>{ <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Sharp(); } <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Rectange <span class="hljs-title">MethodForChild4</span>(<span class="hljs-params">Rectange param</span>) </span>{ <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Rectange(); } MyDelegate3<Sharp> sharp3 = <span class="hljs-keyword">new</span> MyDelegate3<Sharp>(MethodForParent4); MyDelegate3<Rectange> rect3 = <span class="hljs-keyword">new</span> MyDelegate3<Rectange>(MethodForChild4); sharp3 = rect3; |
和泛型接口类似,这里的委托类型参数T被同时用作方法返回类型和方法参数类型,不管修饰符改成in或out,编译都无法通过。所以如果用out修饰T,那么方法参数param的参数类型T就需借助一样东西来转换一下:一个对类型参数T能抗变的泛型委托。 即:
1 2 |
<span class="hljs-keyword">public</span> <span class="hljs-keyword">delegate</span> T MyDelegate3<<span class="hljs-keyword">out</span> T>(Contra<T> param); |
两个方法也需对应着修改:
1 2 3 4 5 6 7 8 9 |
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Sharp <span class="hljs-title">MethodForParent4</span>(<span class="hljs-params">Contra<Sharp> param</span>) </span>{ <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Sharp(); } <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Rectange <span class="hljs-title">MethodForChild4</span>(<span class="hljs-params">Contra<Rectange> param</span>) </span>{ <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Rectange(); } |
这就是上面所说的方法参数的协变-抗变互换原则
同理,如果对该委托类型参数T进行抗变,那么根据方法返回类型协变-抗变一致原则,方法返回参数也是要借助一个对类型参数能抗变的泛型委托:
1 2 |
<span class="hljs-keyword">public</span> <span class="hljs-keyword">delegate</span> Contra<T> MyDelegate3<<span class="hljs-keyword">in</span> T>(T param); |
两个方法也需对应着修改为:
1 2 3 4 5 6 7 8 9 |
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Contra<Sharp> <span class="hljs-title">MethodForParent4</span>(<span class="hljs-params">Sharp param</span>) </span>{ <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Contra<Sharp>(MethodForParent1); } <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Contra<Rectange> <span class="hljs-title">MethodForChild4</span>(<span class="hljs-params">Rectange param</span>) </span>{ <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Contra<Rectange>(MethodForChild1); } |
推广到一般的泛型委托:
1 2 |
<span class="hljs-keyword">public</span> <span class="hljs-keyword">delegate</span> T1 MyDelegate4<T1,T2,T3>(T2 param1,T3 param2); |
可能三个参数T1,T2,T3会有各自的抗变和协变,如:
1 2 |
<span class="hljs-keyword">public</span> <span class="hljs-keyword">delegate</span> T1 MyDelegate4<<span class="hljs-keyword">out</span> T1,<span class="hljs-keyword">in</span> T2,<span class="hljs-keyword">in</span> T3>(T2 param1,T3 param2); |
这是一种最理想的情况,T1支持协变,用于方法返回值;T2,T3支持抗变,用于方法参数。
但是如果变成:
1 2 |
<span class="hljs-keyword">public</span> <span class="hljs-keyword">delegate</span> T1 MyDelegate4<<span class="hljs-keyword">in</span> T1,<span class="hljs-keyword">out</span> T2,<span class="hljs-keyword">in</span> T3>(T2 param1,T3 param2); |
那么对应的T1,T2类型参数就会出问题,原因上面都已经分析过了。于是就需要修改T1对应的方法返回类型,T2对应的方法参数类型,如何修改?只要根据上面提到的: 1)方法返回类型的协变-抗变一致原则; 2)方法参数类型的协变-抗变互换原则!
对应本篇的例子,就可以修改成:
1 |
<span class="hljs-keyword">public</span> <span class="hljs-keyword">delegate</span> Contra<T1> MyDelegate4<<span class="hljs-keyword">in</span> T1, <span class="hljs-keyword">out</span> T2, <span class="hljs-keyword">in</span> T3>(Contra<T2> param1, T3 param2); |