ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Covariance(协变), Contravariance(逆变) and Invariance(不变)

2019-12-31 18:54:31  阅读:258  来源: 互联网

标签:Covariance animalMaker Factory Dog Contravariance Animal 协变 参数 赋值


关于类型和子类型之间的关系,关于类型转换,决定方法是被重写还是重载。

只用于泛型接口或委托。

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( );

class Program
{
static Dog MakeDog()
{
return new Dog();
}
static void main()
{
Factory dogMaker = MakeDog;
Factory animalMaker = dogMaker;
Console.WriteLine(animalMaker().Legs.ToString());
}
}

咋看之下dogMaker赋值给animalMaker不就是派生类赋值给基类吗?有啥好大惊小怪的,其实这里的主要矛盾并不在于赋值兼容性,而在于,Factory和Factory不是同一种类型,dogMaker和animalMaker并不是Dog和Animal类。
这里dogMaker代理的函数返回Dog对象,animalMaker代理的函数返回Animal对象,从返回值的赋值兼容性上来说,animalMaker = dogMaker完全是可行,唯一的矛盾在于它们分别属于Factory和Factory这两个委托类型,两个平级之间怎么互相转化,就跟人和狗 是不能互相转化一样(人和狗均属于动物,但人 != (人)狗。
此时out 关键字就起到关键作用了,out 关键字告诉编译器,我带的这个小弟参数只是用来输出的,你不用管Factory和Factory这俩家伙,只要我带的小弟符合赋值兼容性就行了。

逆变
class Animall { public int NumberOfLegs = 4; }
class Dog : Animal{}
class Program
{
delegate void Action (T a);
static void ActionAnimal(Animal a) { Console.WriteLine(a.NumberOfLegs); }
static void main()
{
Action1 act1 = ActOnAnimal;
Action1 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 dogMaker = MakeDog;
Factory animalMaker = dogMaker;
Console.WriteLine(animalMaker().Legs.ToString());

逆变中参数只做输入参数,dog1(new Dog()) = act1(new Dog())= ActOnAnimal(new Dog());此处new 的Dog 就转化成Animal,也符合赋值兼容性;
Action1 act1 = ActOnAnimal;
Action1 dog1 = act1;
dog1(new Dog());

另外想想,能不能同时做输入参数和输出参数呢?看下面代码:

class Animal { public int Legs = 4;}
class Dog : Animal { }
delegate T Factory(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 dogMaker = MakeDog;
Factory animalMaker = dogMaker;//无法将Factory类型隐式转换为Factory类型

Factory a = MakeAnimal;
Factory b = a;//无法将Factory类型隐式转换为Factory类型
Console.WriteLine(animalMaker().Legs.ToString());
}
}

这段代码无法通过编译,这样修改后,Factory委托中的泛型参数t,既做输入值又做返回值,此时无论是哪边赋值给哪边,都会遇到基类对象赋值给派生类对象的尴尬场面,不是发生在传入参数那里,就是发生在返回值那边。由此才能体会到为何要out(只做输出参数)和in(只做输入参数)这两个关键字了,因为你要能够转化,就得符合赋值兼容性。
当然,如果不给这两个关键字,而又刚好符合赋值兼容性呢?我觉得编译器没那么智能,你没告诉它,它没法加以判断。

不过这里我还存在一个疑问,就算用 out 和 in 限定了参数,但是Factory和 Factory仍然不是同种类型啊,先存着这个问题。

标签:Covariance,animalMaker,Factory,Dog,Contravariance,Animal,协变,参数,赋值
来源: https://www.cnblogs.com/wesson2019-blog/p/12126441.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有