部分借鉴自:https://blog.csdn.net/sudazf/article/details/17148971
首先回顾一下已经学过的内容,我们可以将派生类对象的实例赋值给基类的变量,这叫做赋值兼容,例如以下代码。
class Animal
{
public int NumberOfLegs = 4;
}
class Dog:Animal
{
}
public class Program
{
static void Main(string[] args)
{
Animal a = new Dog();
}
}
在创建泛型类型的实例时,编译器会接收泛型类型声明以及类型参数来创建构造类型,通常会犯的一个错误就是将派生类型分配给基类型的变量,在非泛型的情况下,这是可以的,但是在泛型的情况下,类似的赋值会发生错误。
delegate T Factory<T>(); //作为返回值 如果想让编译不报错则需要对<T>加out修饰符
class Animal { public int Legs = 4; }
class Dog : Animal
{
static public Dog MakeDog() { return new Dog(); }
}
public class Program
{
static void Main(string[] args)
{
Factory<Dog> dogMaker = Dog.MakeDog; //创建委托对象
Factory<Animal> animalMaker = dogMaker; //尝试赋值委托对象 此时编译报错
}
}
如代码所示,尽管Dog是Animal的派生类,但是委托Factory<Dog>没有从委托Factory<Animal>派生,俩个委托对象是同级的,它们都从delegate类型派生,俩者没有相互之间的派生关系,因此赋值兼容性不适用。
协变与抗变只支持引用类型,只能用在泛型委托和泛型接口上
out、in关键字只能用于修饰泛型接口和泛型委托,不能用于类、方法,且泛型中的类型参数T必须是引用类型,而不能是值类型
协变(out泛型修饰符)
概念:协变(covariant),协变使你使用的类型可以比泛型参数指定的类型派生程度更大,例如:string->object,如果类型参数只是用于函数的返回值的时候,类型参数使用out修饰可以产生协变,out代表着返回,只能用作返回值,协变关系允许程度更大的派生类型处于返回位置。
逆变(in泛型修饰符)
概念:逆变(contravariant),逆变使你使用的类型可以比泛型参数指定的类型派生程度更小,例如:object->string,如果类型参数只能用于方法参数,那么可以使用in修饰类型参数产生逆变。in代表输入,代表着只能被使用,不能作为返回值。
泛型接口中的协变与抗变
public interface ICovariant<out T>{} //协变
//public interface ICovariant<in T> {} //逆变
public class Sharp : ICovariant<Sharp>{}
public class Rectange : Sharp, ICovariant<Rectange>{}
public class Program
{
static void Main(string[] args)
{
Rectange rect = new Rectange();
Sharp sharp = new Sharp();
sharp = rect;
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange();
isharp = irect; //如果不用out修饰T的话则会报错
//irect = isharp; //如果不用in修饰T的话则会报错
}
}
如果一个泛型接口IFoo<T>,IFoo<TSub>可以转换为IFoo<TParent>的话,我们称这个过程为协变,而且说“这个泛型接口支持对T的协变”。
如果一个泛型接口IFoo<T>,IFoo<TParent>可以转换为IFoo<TSub>的话,我们称这个过程为逆变,而且说“这个泛型接口支持对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);
}
这是无论用out还是in修饰T参数,都编译不通过,原因是我们把仅有的一个类型参数T既用作函数的返回值类型,又用作参数类型。
(1)所以当用out参数修饰时,即允许接口对类型参数T协变,也就是满足ICovariant<Rectange>到ICovariant<Sharp>转换,Method1返回值Rectange,到Sharp转换没有任何问题。当isharp被irect赋值过后,此时isharp为Rectange类型,所以调用isharp.Method1()时,Method1函数返回的类型为Rectange,因为是协变所以它可以正常转换为Sharp类型。
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange();
isharp = irect;
Sharp sharp = isharp.Method1();
(2)但是对于调用Method2函数时,此时该函数因为是被Rectange类型的接口引用isharp来调用,所以此时Method2函数的形参为Rectange,但是调用时的实参为Sharp类型,而Sharp类型并不能安全转化为Rectange类型,所以会报错。
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange();
isharp = irect;
isharp.Method2(new Sharp());
(3)同样,当用in修饰符时,即允许接口对类型参数T逆变,也就是满足从ICovariant<Sharp>到ICovariant<Rectange>转换,此时irect被赋值为Sharp类型,当调用irect.Method2时,该函数的形参为Sharp类型,而此时实参为Rectange类型,为Sharp类型的子类,所以理所当然的可以正常的转换。
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange();
//isharp = irect;
irect = isharp;
irect.Method2(new Rectange());
(4)当调用Method1时,因为是irect来调用,所以它会返回Sharp类型,而这里将要用这个返回值赋值给Rectange类型的变量,这是不安全的,所以会出错。
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange();
//isharp = irect;
irect = isharp;
Rectange rect = irect.Method1();
所以在没有额外机制的限制下,接口进行协变或逆变都是类型不安全的,NET4.0有了改进,它允许在类型参数的声明时增加一个额外的描述,以确定这个类型参数的使用范围,这个额外的描述就是in、out修饰符,它们的用法为,如果一个了类型参数仅仅能用于函数的返回值,那么这个类型参数就对协变相容,用out修饰。而相仿,一个类型参数如果仅能用于方法参数,那么这个类型参数就对抗变相容,用in修饰。
所以需要将上面的接口拆成俩个接口即可:
public interface ICovariant<out T>
{
T Method1();
}
public interface IContravariant<in T>
{
void Method2(T param);
}
总结要点:
(1)仅有泛型接口和泛型委托支持对类型参数的可变性,泛型类或泛型方法时不支持的
(2)值类型不参与协变或逆变,IFoo<int>永远无法协变成IFoo<object>,不管有无声明out。因为.NET泛型,每个值类型会生成专属的封闭构造类型,与引用类型版本不兼容。
(3)声明属性时要注意,可读写的属性会将类型同时用于参数和返回值,因此只有只读属性才允许使用out类型参数,只写属性能够使用in参数。
协变-逆变互换原则(out修饰类型作类型参数)
接下来将接口代码改成这样,同样是可以编译通过的。
public interface ICovariant<out T>
{
T Method1();
void Method3(IContravariant<T> param);
}
public interface IContravariant<in T>
{
void Method2(T param);
}
如代码所示,根据之前的分析,第二个为逆变接口,它可以将Sharp类型转换为Rectange类型,而对于第一个协变接口,参数T可以直接作为返回值,但是作为方法参数则满足不了,原因是不能将Sharp类型转换为Rectange类型,所以如果要满足这个函数的定义,对Method3的参数要求是Sharp能够逆变成Rectange,正好使用第二个接口作为参数即可满足,也就是说,如果一个接口需要对类型参数T协变,那么这个接口所有方法参数类型必须支持对类型参数T的逆变(如果T有作为某些方法的参数类型)。
所以我们并不能简单地说out参数只能用于方法返回类型参数,它确实只能直接用于声明返回值类型,但是只要一个支持逆变的类型协助,out类型参数就也可以用于参数类型。换句话说,in除了直接声明方法类型支持逆变外,也可以借助支持协变的类型用于方法参数,仅支持对T逆变的类型作为方法参数类型是不允许的。
协变-逆变一致原则(in修饰类型作返回值)
既然方法类型参数协变和逆变有互换影响,那么方法的返回值类型会不会有同样的问题呢?如下代码
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");
}
编译并不能通过,因为无法将MyDelegate1<Rectange>隐式转化为MyDelegate1<Sharp>,接下来我将接口修改为支持对类型参数T协变,即加out修饰符:
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;
只需要将修饰符改为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;
错误如下:
变体无效: 类型参数“T”必须为对于“MyDelegate2<T>.Invoke()”有效的 协变式。“T”为 逆变。意思就是:这里的类型参数T已经被声明成抗变,如果上面的最后一句有效,那么以后rect2()执行结果返回的将是一个Sharp类型的实例,如果再出现这种代码:
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();
}
这就是上面所说的方法参数的协变-抗变互换原则。
推广到一般的泛型委托:
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类型参数就会出问题,原因上面都已经分析过了。于是就需要修改T1对应的方法返回类型,T2对应的方法参数类型,如何修改?只要根据上面提到的:
1)方法返回类型的协变-抗变一致原则;
2)方法参数类型的协变-抗变互换原则!
对应本篇的例子,就可以修改成:
public delegate Contra<T1> MyDelegate4<in T1, out T2, in T3>(Contra<T2> param1, T3 param2);