转自:http://blog.csdn.net/sudazf/article/details/17148971
协变和抗变
一.定义
在说定义之前,先看一个简单的例子:
- public class Sharp
- {
- }
- public class Rectange : Sharp
- {
- }
- Sharp sharp = new Rectange();
那问题就来了,既然Rectange类和Sharp类之间存在一种安全的隐式转换,那数组Rectange[]和Sharp[]之间是否也存在这种安全的隐式转换呢?
这就牵扯到了将原本类型上存在的类型转换映射到他们的数组类型上的能力,这种能力就称为“可变性(Variance)”。在.NET中,唯一允许可变性的类型转换就是由继承关系带来的“子类引用->父类引用”转换。也就是上面例子所满足的写法。
然后看下面这种写法:
- Sharp[] sharps=new Rectange[3];
像这种与原始类型转换方向相同的可变性就称作协变(covariant)
接下来试试这样写:
- Rectange[] rectanges = new Sharp[3];
发现编译不通过,即数组所对应的单一元素的父类引用不可以安全的转化为子类引用。数组也就自然不能依赖这种可变性,达到协变的目的。
所以与协变中子类引用转化为父类引用相反,将父类引用转化为子类引用的就称之为抗变。
即:一个可变性和子类到父类转换的方向一样,就称作协变;而如果和子类到父类的转换方向相反,就叫抗变!
当然可变性远远不只是针对映射到数组的能力,也有映射其它集合的能力如List<T>.
到这里,很多人就会问了,说了这么多,那到底这个协变或者抗变有什么实际利用价值呢?
其价值就在于,在.net 4.0之前可以这么写:
- Sharp sharp = new Rectange();
但是却不能这么写:
- IEnumerable<Sharp> sharps = new List<Rectange>();
4.0之后,可以允许按上面的写法了,因为泛型接口IEnumerable<T>被声明成如下:
- public interface IEnumerable<out T> : IEnumerable
上面提到了,数组不支持抗变。在.Net 4.0之后,支持协变和抗变的有两种类型:泛型接口和泛型委托。
二.泛型接口中的协变和抗变
接下来定义一个泛型接口:
- public interface ICovariant<T>
- {
- }
- public class Sharp : ICovariant<Sharp>
- {
- }
- public class Rectange : Sharp,ICovariant<Rectange>
- {
- }
- static void Main(string[] args)
- {
- ICovariant<Sharp> isharp = new Sharp();
- ICovariant<Rectange> irect = new Rectange();
- isharp = irect;
- }
再将接口修改为:
- public interface ICovariant<out T>
- {
- }
即:如果一个泛型接口
IFoo<T>
,
IFoo<TSub>
可以转换为
IFoo<TParent>
的话,我们称这个过程为协变,而且说“这个泛型接口支持对
T的协变”
。
那我如果反过来呢,考虑如下代码:
那我如果反过来呢,考虑如下代码:
- static void Main(string[] args)
- {
- ICovariant<Sharp> isharp = new Sharp();
- ICovariant<Rectange> irect = new Rectange();
- irect = isharp;
- // isharp =irect;
- }
原因是无法将
ICovariant<Sharp>
隐式转化为
ICovariant<Rectange>
!
将接口修改为:
- public interface ICovariant<in T>
- {
- }
即:如果一个泛型接口
IFoo<T>
,
IFoo<TParent>
可以转换为IFoo
<TSub>
的话,我们称这个过程为抗变(
contravariant)
,而且说“这个泛型接口支持对
T的抗变”!
泛型接口并不单单只有一个参数,所以我们不能简单地说一个接口支持协变还是抗变,只能说一个接口对某个具体的类型参数支持协变或抗变,如ICovariant<out T1,in T2>说明该接口对类型参数T1支持协变,对T2支持抗变。
举个例子就是:ICovariant<Rectange,Sharp>能够转化成
ICovariant<
Sharp
,
Rectange
>,这里既有协变也有抗变。
以上都是接口并没有属性或方法的情形,接下来给接口添加一些方法:
- //这时候,无论如何修饰T,都不能编译通过
- public interface ICovariant<out T>
- {
- T Method1();
- void Method2(T param);
- }
原因是,
我把仅有的一个类型参数T既用作函数的返回值类型,又用作函数的参数类型。
所以:
1)当我用out修饰时,即允许接口对类型参数T协变,也就是满足从ICovariant<Rectange>到ICovariant<Sharp>转换,Method1返回值Rectange到Sharp转换没有任何问题:
- ICovariant<Sharp> isharp = new Sharp();
- ICovariant<Rectange> irect = new Rectange();
- isharp = irect;
- Sharp sharp = isharp.Method1();
但是对于把T作为参数类型的方法Method2(Rectange)会去替换Method2(Sharp):
- ICovariant<Sharp> isharp = new Sharp();
- ICovariant<Rectange> irect = new Rectange();
- isharp = irect;
- isharp.Method2(new Sharp());
Method2(Rectange)
!
2)同样,当我用in修饰时,
即允许接口对类型参数T抗变,也就是满足从
ICovariant<Sharp>
到ICovariant<Rectange>转换:
- ICovariant<Sharp> isharp = new Sharp();
- ICovariant<Rectange> irect = new Rectange();
- //isharp = irect;
- irect = isharp;
- irect.Method2(new Rectange());
Method2(Sharp)会去替换Method2(Rectange),所以上面的最后一句代码无论以Rectange类型还是Sharp类型为参数都没有任何问题;
但是Method1返回的将是Sharp类型:
- ICovariant<Sharp> isharp = new Sharp();
- ICovariant<Rectange> irect = new Rectange();
- //isharp = irect;
- irect = isharp;
- Rectange rect = irect.Method1();
如果一个类型参数仅仅能用于函数的返回值,那么这个类型参数就对协变相容,用out修饰。而相反,一个类型参数如果仅能用于方法参数,那么这个类型参数就对抗变相容,用in修饰。
所以,需要将上面的接口拆成两个接口即可:
- public interface ICovariant<out T>
- {
- T Method1();
- }
- public interface IContravariant<in T>
- {
- void Method2(T param);
- }
.net中很多接口都仅将参数用于函数返回类型或函数参数类型,如:
- public interface IComparable<in T>
- public interface IEnumerable<out T> : IEnumerable
几个重要的注意点:
1.仅有泛型接口和泛型委托支持对类型参数的可变性,泛型类或泛型方法是不支持的。
2.值类型不参与协变或抗变,IFoo<int>永远无法协变成IFoo<object>,不管有无声明out。因为.NET泛型,每个值类型会生成专属的封闭构造类型,与引用类型版本不兼容。
3.声明属性时要注意,可读写的属性会将类型同时用于参数和返回值。因此只有只读属性才允许使用out类型参数,只写属性能够使用in参数。
2.值类型不参与协变或抗变,IFoo<int>永远无法协变成IFoo<object>,不管有无声明out。因为.NET泛型,每个值类型会生成专属的封闭构造类型,与引用类型版本不兼容。
3.声明属性时要注意,可读写的属性会将类型同时用于参数和返回值。因此只有只读属性才允许使用out类型参数,只写属性能够使用in参数。
接下来将接口代码改成:
- 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抗变的类型作为方法参数类型也是不允许的。
既然方法类型参数协变和抗变有上面的互换影响。那么方法的返回值类型会不会有同样的问题呢?
将接口修改为:
- 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 参数也可以用于方法的返回值类型,只要借助一个可以抗变的类型作为桥梁即可。
新建一个简单的泛型接口:
- public delegate void MyDelegate1<T>();
- MyDelegate1<Sharp> sharp1 = new MyDelegate1<Sharp>(MethodForParent1);
- MyDelegate1<Rectange> rect1 = new MyDelegate1<Rectange>(MethodForChild1);
- sharp1 = rect1;
其中两个方法为:
- public static void MethodForParent1()
- {
- Console.WriteLine("Test1");
- }
- public static void MethodForChild1()
- {
- Console.WriteLine("Test2");
- }
- public delegate void MyDelegate1<out T>();
同样,如果反过来,对类型参数T进行抗变:
- MyDelegate1<Sharp> sharp1 = new MyDelegate1<Sharp>(MethodForParent1);
- MyDelegate1<Rectange> rect1 = new MyDelegate1<Rectange>(MethodForChild1);
- //sharp1 = rect1;
- rect1 = sharp1;
- public delegate void MyDelegate1<in T>();
考虑第二个委托:
- public delegate T MyDelegate2<out T>();
- MyDelegate2<Sharp> sharp2 = new MyDelegate2<Sharp>(MethodForParent2);
- MyDelegate2<Rectange> rect2 = new MyDelegate2<Rectange>(MethodForChild2);
- sharp2 = rect2;
- public static Sharp MethodForParent2()
- {
- return new Sharp();
- }
- public static Rectange MethodForChild2()
- {
- return new Rectange();
- }
测试如下:
- public delegate T MyDelegate2<in T>();
- MyDelegate2<Sharp> sharp2 = new MyDelegate2<Sharp>(MethodForParent2);
- MyDelegate2<Rectange> rect2 = new MyDelegate2<Rectange>(MethodForChild2);
- //sharp2 = rect2;
- rect2 = sharp2;
变体无效: 类型参数“T”必须为对于“MyDelegate2<T>.Invoke()”有效的 协变式。“T”为 逆变。
意思就是:这里的类型参数T已经被声明成抗变,如果上面的最后一句有效,那么以后rect2()执行结果返回的将是一个Sharp类型的实例,
如果再出现这种代码:
- Rectange rectange = rect2();
那么如何对上面的返回类型进行抗变呢?很简单,只要借助一个支持抗变的泛型委托作为方法返回类型即可:
- public delegate Contra<T> MyDelegate2<in T>();
- public delegate void Contra<in T>();
- public static Contra<Sharp> MethodForParent3()
- {
- return new Contra<Sharp>(MethodForParent1);
- }
- public static Contra<Rectange> MethodForChild3()
- {
- return new Contra<Rectange>(MethodForChild1);
- }
- MyDelegate2<Sharp> sharp2 = new MyDelegate2<Sharp>(MethodForParent3);
- MyDelegate2<Rectange> rect2 = new MyDelegate2<Rectange>(MethodForChild3);
- rect2 = sharp2;
接下来考虑第三个委托:
- public delegate T MyDelegate3<T>(T param);
- public delegate T MyDelegate3<out T>(T param);
- public static Sharp MethodForParent4(Sharp param)
- {
- return new Sharp();
- }
- public static Rectange MethodForChild4(Rectange param)
- {
- return new Rectange();
- }
- MyDelegate3<Sharp> sharp3 = new MyDelegate3<Sharp>(MethodForParent4);
- MyDelegate3<Rectange> rect3 = new MyDelegate3<Rectange>(MethodForChild4);
- sharp3 = rect3;
即:
- public delegate T MyDelegate3<out T>(Contra<T> param);
- public static Sharp MethodForParent4(Contra<Sharp> param)
- {
- return new Sharp();
- }
- public static Rectange MethodForChild4(Contra<Rectange> param)
- {
- return new Rectange();
- }
同理,如果对该委托类型参数T进行抗变,那么根据方法返回类型协变-抗变一致原则,方法返回参数也是要借助一个对类型参数能抗变的泛型委托:
- public delegate Contra<T> MyDelegate3<in T>(T param);
- public static Contra<Sharp> MethodForParent4(Sharp param)
- {
- return new Contra<Sharp>(MethodForParent1);
- }
- public static Contra<Rectange> MethodForChild4(Rectange param)
- {
- return new Contra<Rectange>(MethodForChild1);
- }
- public delegate T1 MyDelegate4<T1,T2,T3>(T2 param1,T3 param2);
- public delegate T1 MyDelegate4<out T1,in T2,in T3>(T2 param1,T3 param2);
但是如果变成:
- public delegate T1 MyDelegate4<in T1,out T2,in T3>(T2 param1,T3 param2);
1)方法返回类型的协变-抗变一致原则;
2)方法参数类型的协变-抗变互换原则!
对应本篇的例子,就可以修改成:
- public delegate Contra<T1> MyDelegate4<in T1, out T2, in T3>(Contra<T2> param1, T3 param2);
以上,协变和抗变记录到此。