Covariance (协变), Contravariance (逆变) and Invariance (不变)
关于类型和子类型之间的关系,关于类型转换,决定方法是被重写还是重载。
只用于泛型接口或委托。
输出 out 协变,可用派生程度更大的子类替换父类型的参数,只能用于方法的返回值。
输入 in 逆变,可用派生程度更小的父类替换子类型参数, 只能用于方法的输入参数。
当是out参数的时候,那么它就要求实际的返回对象至少是父类,或者基于T的子类。
当是in参数的时候:那么他可以T对象实参,也能传T的父类对象实参进去。
赋值兼容性
在此之前先了解下“赋值兼容性”,无非就是派生类对象可以赋值给“基类对象”,反之不行。这个概念理解了以后过一段时间可能容易迷糊,其实只要从访问的安全性来理解就不容易忘。派生类是从基类继承而来,又有自己私人的东西,
把派生类对象Dog赋值给基类对象Animal,Animal对象能访问到的动物属性在Dog对象中都能访问到;
但是如果反过来,把Animal对象 赋给Dog对象的话,Dog对象能访问到的内存就比Animal对象多得多了,比如狗的鼻子很灵,动物不一定很灵,此时用Dog对象 访问狗腿这个属性,结果赋值给Dog对象的Animal对象 没有狗腿这个属性,也许访问到的是牛头不对马嘴,也许是一堆乱码,这时候这堆内存如果是属于人家Rabbit对象的,你让别的阿猫阿狗怎么想Rabbit的,它们又不是对象。
所以从安全性上来讲,为了狗腿不至于变成兔腿,为了保卫兔子家(内存)的安全,基类不能赋值给派生类。
协变
先看书上一段协变的代码:
class Animal { public int Legs = 4; }
class Dog : Animal { }
delegate T Factory<out T>();
class Program
{
static Dog MakeDog()
{
return new Dog();
}
static void main()
{
Factory<Dog> dogMaker = MakeDog;
Factory<Animal> animalMaker = dogMaker;
Console.WriteLine(animalMaker().Legs.ToString());
}
}
咋看之下dogMaker赋值给animalMaker不就是派生类赋值给基类吗?有啥好大惊小怪的,其实这里的主要矛盾并不在于赋值兼容性,而在于,Factory
这里dogMaker代理的函数返回Dog对象,animalMaker代理的函数返回Animal对象,从返回值的赋值兼容性上来说,animalMaker = dogMaker完全是可行,唯一的矛盾在于它们分别属于Factory
此时out 关键字就起到关键作用了,out 关键字告诉编译器,我带的这个小弟参数只是用来输出的,你不用管Factory
逆变
class Animall { public int NumberOfLegs = 4; }
class Dog : Animal { }
class Program
{
delegate void Action<in T>(T a);
static void ActionAnimal(Animal a) { Console.WriteLine(a.NumberOfLegs); }
static void main()
{
Action1<Animal> act1 = ActOnAnimal;
Action1<Dog> dog1 = act1;
dog1(new Dog());
}
}
从static void ActionAnimal(Animal a) { Console.WriteLine(a.NumberOfLegs); }这句代码来看,正好和协变相反,协变是强调只用作输出参数,而这里的Animal a却是不得不用于Console.WriteLine(a.NumberOfLegs);,这里只能用于输入。
所以此处in关键字就起到关键作用了,它告诉编译器,我带的小弟只用作输入参数,你不用管act1 和 dog1 是不是同种类型,你只管这两个委托代理的函数的输入参数是否符合赋值兼容性就行了。
总结
网上有这句话,我觉得不太对:
由子类向父类方向转变是协变 协变用于返回值类型用out关键字
由父类向子类方向转变是逆变 逆变用于方法的参数类型用in关键字
从上面的两段代码可以看到,无论是协变还是逆变,其本质都要符合类的赋值兼容性。
协变中参数只做输出参数,dogMaker返回的是Dog,赋值给animalMaker即将返回的Animal,即派生类赋值给基类,符合赋值兼容性;
Factory<Dog> dogMaker = MakeDog;
Factory<Animal> animalMaker = dogMaker;
Console.WriteLine(animalMaker().Legs.ToString());
逆变中参数只做输入参数,dog1(new Dog()) = act1(new Dog())= ActOnAnimal(new Dog());此处new 的Dog 就转化成Animal,也符合赋值兼容性;
Action1<Animal> act1 = ActOnAnimal;
Action1<Dog> dog1 = act1;
dog1(new Dog());
另外想想,能不能同时做输入参数和输出参数呢?看下面代码:
class Animal { public int Legs = 4; }
class Dog : Animal { }
delegate T Factory<T>(T t);
class Program
{
static Dog MakeDog(Dog dog)
{
Console.WriteLine($"{dog.legs}");
return new Dog();
}
static Animal MakeAnimal(Animal animal)
{
Console.WriteLine($"{animal.legs}");
return new Animal();
}
static void main()
{
Factory<Dog> dogMaker = MakeDog;
Factory<Animal> animalMaker = dogMaker; //无法将Factory<Dog>类型隐式转换为Factory<Animal>类型
Factory<Animal> a = MakeAnimal;
Factory<Dog> b = a; //无法将Factory<Animal>类型隐式转换为Factory<Dog>类型
Console.WriteLine(animalMaker().Legs.ToString());
}
}
这段代码无法通过编译,这样修改后,Factory委托中的泛型参数t,既做输入值又做返回值,此时无论是哪边赋值给哪边,都会遇到基类对象赋值给派生类对象的尴尬场面,不是发生在传入参数那里,就是发生在返回值那边。由此才能体会到为何要out(只做输出参数)和in(只做输入参数)这两个关键字了,因为你要能够转化,就得符合赋值兼容性。
当然,如果不给这两个关键字,而又刚好符合赋值兼容性呢?我觉得编译器没那么智能,你没告诉它,它没法加以判断。
不过这里我还存在一个疑问,就算用 out 和 in 限定了参数,但是Factory