关于C#中,泛型协变与逆变的理解
以下内容仅为个人理解,如有错误的地方,还请各位大佬指正!
一:什么是协变与逆变,
二:协变与逆变是如何实现的
三:为什么会有泛型协变与逆变的限制
1.首先来说说什么是协变与逆变
- 如果有两个对象A、B,如果每个A类型的值都能转化为B类型,则认为拥有协变关系,从A到B的转换成为协变转换,简称协变
- 与第一条相反,从B到A的转换成为逆变转换,简称逆变
* 协变与逆变的在实际开发中比较常见的情况是:假如有class Person{ },class Man{ },Man派生之Person,则一个Man对象转化为Person对象的行为称为协变转换,而Person对象转化为Man对象成为逆变转化
- 泛型的协变与逆变,我们先来看下面代码
public class Animal { public string Name { get; set; } } public class Bird : Animal { public void Fly(Bird bird) { Console.WriteLine($"这只鸟的哥哥叫:{bird.Brother.Name}"); } public Bird Brother { get; set; } } public class Fish : Animal { public void Spit(Fish fish) { Console.WriteLine($"这只鱼的哥哥叫:{fish.Brother.Name}"); } public Fish Brother { get; set; } } public interface IAnimalOperate<T> { T GetAnimal(); void SetBrother(T t); } public class FishOperate : IAnimalOperate<Fish> { public Fish GetAnimal() { Fish f = new Fish() { Name = "Fish Brother" }; return new Fish() { Brother = f }; } public void SetBrother(Fish t) { t.Brother.Name = " Fish new Brother"; } } public class TestClass { public void Test() { IAnimalOperate<Fish> fish = new FishOperate(); IAnimalOperate<Animal> animal = fish; //error 无法将类型IGetAnimal<Fish> 转化为 IGetAnimal<Animal> } }
这段代码很好理解,Fish派生自Animal,但是 IAnimalOperate<Fish> 无法转化为 IAnimalOperate<Animal>,其实无论是从 IAnimalOperate<Fish> 转化为 IAnimalOperate<Animal>(泛型的协变转化)还是从 IAnimalOperate<Animal> 转化为IAnimalOperate<Fish>(泛型的逆变转换) ,目前的写法都是编译不通过的,至于原因以及解决方法,我们在后面继续讨论。
2.协变和逆变是如何实现的
- 协变的实现
实现泛型的协变转换,只需要在泛型接口的 IAnimalOperate 申明中加入out关键字即可,加入out关键字之后,可以实现从泛型 IAnimalOperate<Fish> 到 IAnimalOperate<Animal>的转换 即
public interface IAnimalOperate<out T> { T GetFish(); // void SetBrother(T t); }
- 逆变的实现
逆变的实现与协变相反,在泛型接口的 IAnimalOperate 申明中加入 in 关键字即可,加入in关键字之后,可以实现从泛型 IAnimalOperate<Animal> 到 IAnimalOperate<Fish> 的转换 即
public interface IAnimalOperate<in T> { // T GetFish(); void SetBrother(T t); }
* 泛型的协变逆变实现方式很简单,下面我们重点讨论原理,代码中注释的部分也会在下面做出解释
3.为什么会有泛型协变与逆变的限制
- 通常来说Fish类型派生自Animal类型,所以Fish类的对象可以转为为Animal对象,但是本例中 IAnimalOperate<Fish>并没有派生自 IAnimalOperate<Animal> ,不具备继承关系的对象,当然不能做这种隐式转换 ,但是这种看似正常的行为又有点奇怪,明明泛型接口的类型具有继承关系,下面我们重点从另一个方面来讨论这个事情
- 按照上面的例子我们来看一下,如果泛型接口没有限制,会发生什么情况
public class Animal { public string Name { get; set; } } public class Bird : Animal { public void Fly(Bird bird) { Console.WriteLine($"这只鸟的哥哥叫:{bird.Brother.Name}"); } public Bird Brother { get; set; } } public class Fish : Animal { public void Spit(Fish fish) { Console.WriteLine($"这只鱼的哥哥叫:{fish.Brother.Name}"); } public Fish Brother { get; set; } } public interface IAnimalOperate<T> { T GetFish(); void SetBrother(T t); } public class FishOperate : IAnimalOperate<Fish> { public Fish GetFish() { Fish f = new Fish() { Name = "Fish Brother" }; return new Fish() { Brother = f }; } public void SetBrother(Fish t) { t.Brother.Name = " Fish new Brother"; } } public class TestClass { public void Test() { IAnimalOperate<Fish> fish = new FishOperate(); IAnimalOperate<Animal> animal = fish; //假如这一段代码成立 Bird bird = new Bird() { Name = "b1" }; animal.SetBrother(bird); } }
我们重点看标红的两行代码,正常境况下这种代码在进行转化的时候就会被报错,因为协变会被限制,加入没有限制的话,我们可以看到,由IAnimalOperate<Fish> 转化为 IAnimalOperate<Animal> 之后,IAnimalOperate<Animal> 的 SetBrother 方法可以接受一个 Animal 对象了,然后我们把一个 Brid 对象设置给了 fish 的 Brother 属性,这显然是不合理的,所以泛型的协变逆变限制也避免了这种情况的发生。
但是,换个角度思考一下,如果我在泛型接口里面,不对泛型接口的类型进行操作,即不调用上述例子中的 SetBrother 方法,是不是就可以避免出现这个问题,所以,C#在这种泛型参数类型只读的情况下,允许对参数加上out 关键字,表示该泛型接口可以发生协变,这也解释了我上面在实现协变时,注释了这段代码的原因
public interface IAnimalOperate<T> { T GetFish(); void SetBrother(T t); }
下面我们来看看逆变,如果没有限制,逆变会发生什么情况,逆变与协变相反,类似与把基类的对象转化为派生类对象,所以我们这里修改一部分代码
public class Animal { public string Name { get; set; } } public class Bird : Animal { public void Fly(Bird bird) { Console.WriteLine($"这只鸟的哥哥叫:{bird.Brother.Name}"); } public Bird Brother { get; set; } } public class Fish : Animal { public void Spit(Fish fish) { Console.WriteLine($"这只鱼的哥哥叫:{fish.Brother.Name}"); } public Fish Brother { get; set; } } public interface IAnimalOperate<T> { T GetAnimal(); void SetBrother(T t); } public class AnimalOperate : IAnimalOperate<Animal> { public Animal GetAnimal() { return new Animal() { Name=" Hello " }; } public void SetBrother(Animal t) { t.Name = " World"; } } public class TestClass { public void Test() { IAnimalOperate<Animal> animal = new AnimalOperate(); IAnimalOperate<Fish> fish = animal; //假如这一段代码成立 Fish f1 = fish.GetAnimal(); } }
上述代码修改了 IAnimalOperate 的实现,同理,上述代码本是编译不通过的,我们现在假设没有泛型逆变的显示,编译可以通过,那么此时发生了一种情况 animal 在赋值给 fish 对象之后,fish.GetAnimal() 方法实质上返回的还是一个 Animal 对象,将一个 Animal对象赋值给Fish对象,显然是不符合我们的变成规范的,泛型的逆变限制就是为了避免这种情况。
但是假如我的泛型接口并不会返回新的数据,就不会出现上述的情况,所以C#的 in 关键字,支持对没有返回泛型类型的接口实现逆变,这也是我上面在实现逆变的时候注释一段代码的原因
public interface IAnimalOperate<in T> { // T GetAnimal(); void SetBrother(T t); }
其实仔细研究会发现,out 修饰的泛型接口类型,在发生协变之后,返回泛型类型T的这一过程,其实就是 类似与 Animal a = new Fish() 这一过程,而在 in 修饰的泛型接口中,在发生逆变之后,调用 SetBrother( T t ) 方法的时候,实质上还是将一个 子类对象 (Fish或Brid对象)隐式转化为 Animal 对象,说起来,其本质还是为了遵循里氏替换原则。

浙公网安备 33010602011771号