协变和抗变
一.定义
public class Sharp
{
}
public class Rectange : Sharp
{
}
上面定义了两个简单的类,一个是图形类,一个是矩形类;它们之间有简单的继承关系。接下来是常见的一种正确写法:Sharp sharp = new Rectange();
就是说“子类引用可以直接转化成父类引用”,或者说Rectange类和Sharp类之间存在一种安全的隐式转换。Sharp[] sharps=new Rectange[3];
编译通过,这说明Rectange[]和Sharp[]之间存在安全的隐式转换。Rectange[] rectanges = new Sharp[3];
Sharp sharp = new Rectange();
IEnumerable<Sharp> sharps = new List<Rectange>();
public interface IEnumerable<out T> : IEnumerable
二.泛型接口中的协变和抗变
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;
}
编译并不能通过,原因是无法将ICovariant<Rectange>隐式转化为ICovariant<Sharp>! public interface ICovariant<out T>
{
}
编译顺利通过。这里我为泛型接口的类型参数增加了一个修饰符out,它表示这个泛型接口支持对类型T的协变。那我如果反过来呢,考虑如下代码:
static void Main(string[] args)
{
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange();
irect = isharp;
// isharp =irect;
}
发现编译又不通过了,
public interface ICovariant<in T>
{
}
编译顺利通过。这里我将泛型接口的类型参数T修饰符修改成in,它表示这个泛型接口支持对类型参数T的抗变。 //这时候,无论如何修饰T,都不能编译通过
public interface ICovariant<out T>
{
T Method1();
void Method2(T param);
}
发现无论用out还是in修饰T参数,根本编译不通过。 ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange();
isharp = irect;
Sharp sharp = isharp.Method1();
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange();
isharp = irect;
isharp.Method2(new Sharp());
即如果执行最后一行代码,会发现参数中,Sharp类型并不能安全转化成Rectange类型,因为Method2(Sharp)实际上已经被替换成
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange();
//isharp = irect;
irect = isharp;
irect.Method2(new Rectange());
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange();
//isharp = irect;
irect = isharp;
Rectange rect = irect.Method1();
执行最后一句代码,同样将会是不安全的! 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
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);
}
同样是可以编译通过的. 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类作为示例: 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>();
编译顺利用过。 MyDelegate1<Sharp> sharp1 = new MyDelegate1<Sharp>(MethodForParent1);
MyDelegate1<Rectange> rect1 = new MyDelegate1<Rectange>(MethodForChild1);
//sharp1 = rect1;
rect1 = sharp1;
只需将修饰符改为in即可: 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();
}
该委托对类型参数T进行协变没有任何问题,编译通过;如果我要对T进行抗变呢?是否只要将修饰符改成in就OK了? public delegate T MyDelegate2<in T>();
MyDelegate2<Sharp> sharp2 = new MyDelegate2<Sharp>(MethodForParent2);
MyDelegate2<Rectange> rect2 = new MyDelegate2<Rectange>(MethodForChild2);
//sharp2 = rect2;
rect2 = sharp2;
错误如下: Rectange rectange = rect2();
那么这将是一个从Sharp类到Rectange类的不安全的类型转换!所以如果类型参数T抗变,并且要用于方法返回类型,那么方法的返回类型也必须支持抗变。即上面所说的方法返回类型协变-抗变一致原则。 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);
首先,对类型参数T进行协变: 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;
和泛型接口类似,这里的委托类型参数T被同时用作方法返回类型和方法参数类型,不管修饰符改成in或out,编译都无法通过。所以如果用out修饰T,那么方法参数param的参数类型T就需借助一样东西来转换一下:一个对类型参数T能抗变的泛型委托。即:
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);
可能三个参数T1,T2,T3会有各自的抗变和协变,如: public delegate T1 MyDelegate4<out T1,in T2,in T3>(T2 param1,T3 param2);
这是一种最理想的情况,T1支持协变,用于方法返回值;T2,T3支持抗变,用于方法参数。 public delegate T1 MyDelegate4<in T1,out T2,in T3>(T2 param1,T3 param2);
那么对应的T1,T2类型参数就会出问题,原因上面都已经分析过了。于是就需要修改T1对应的方法返回类型,T2对应的方法参数类型,如何修改?只要根据上面提到的: public delegate Contra<T1> MyDelegate4<in T1, out T2, in T3>(Contra<T2> param1, T3 param2);
以上,协变和抗变记录到此。