看到协变和逆变之后头大了几圈,现在理出点头绪,用浅显的话记录一下,以便后面回顾。
赋值兼容性
在此之前先了解下“赋值兼容性”,无非就是派生类对象可以赋值给“基类对象”,反之不行。这个概念理解了以后过一段时间可能容易迷糊,其实只要从访问的安全性来理解就不容易忘。派生类是从基类继承而来,又有自己私人的东西,
- 把派生类对象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<Dog>
和Factory<Animal>
不是同一种类型,dogMaker和animalMaker并不是Dog和Animal类。
这里dogMaker代理的函数返回Dog对象,animalMaker代理的函数返回Animal对象,从返回值的赋值兼容性上来说,animalMaker = dogMaker
完全是可行,唯一的矛盾在于它们分别属于Factory<Dog>
和Factory<Animal>
这两个委托类型,两个平级之间怎么互相转化,就跟人和狗 是不能互相转化一样(人和狗均属于动物,但人 != (人)狗
。
此时out 关键字就起到关键作用了,out 关键字告诉编译器,我带的这个小弟参数只是用来输出的,你不用管Factory<Dog>
和Factory<Animal>
这俩家伙,只要我带的小弟符合赋值兼容性就行了。
逆变
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<Animal>
和 Factory<Dog>
仍然不是同种类型啊,先存着这个问题。