C#:协变和逆变

本文深入浅出地解释了协变和逆变的概念,通过具体代码示例展示了这两种类型变化如何遵循类的赋值兼容性原则。协变用于方法的返回类型,允许派生类对象作为基类对象使用;逆变应用于方法参数类型,确保基类引用可以接受派生类实例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

看到协变和逆变之后头大了几圈,现在理出点头绪,用浅显的话记录一下,以便后面回顾。

赋值兼容性

在此之前先了解下“赋值兼容性”,无非就是派生类对象可以赋值给“基类对象”,反之不行。这个概念理解了以后过一段时间可能容易迷糊,其实只要从访问的安全性来理解就不容易忘。派生类是从基类继承而来,又有自己私人的东西,

  1. 派生类对象Dog赋值给基类对象AnimalAnimal对象能访问到的动物属性在Dog对象中都能访问到;
  2. 但是如果反过来,把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>不是同一种类型,dogMakeranimalMaker并不是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>仍然不是同种类型啊,先存着这个问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值