首页新闻招聘找找看知识库

泛型类多态?协变逆变?

0
悬赏园豆:50 [已解决问题]

在实现行为模式时,用继承和多态搞定是没问题,可在尝试使用泛型化的类来实现时,遇到了一些问题,自己也去看了协变逆变相关的知识,发现还是无法理解,特来向各位园友请求赐教,谢谢!

想达成的效果是,定义一个泛型基类集合,然后添加此泛型基类派生出来的子类至集合中,并在循环中调用基类的方法。可是,遇到了一些问题,是泛型参数不支持继承?

Car<IBrand>或ICar<IBrand> 并不能接受子类Car<Bmw> 的赋值, 看上去好像不仅仅像是协变逆变方面的问题,或是我哪里理解有误,不解中……

复制代码
    public interface IBrand { }

    public class Bmw : IBrand { }

    public interface ICar<out TBrand> where TBrand : IBrand
    {
        void StartEngine();
        //void Method(TBrand brand);
    }

    public abstract class Car<TBrand> : ICar<TBrand> where TBrand : IBrand
    {
        public void StartEngine() { }
    }

    public class Suv1 : Car<Bmw> { }
    public class Suv2 : ICar<Bmw>
    {
        public void StartEngine() { }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Car<IBrand> bmwSuv1 = new Suv1(); //不行
            Car<IBrand> bmwSuv2 = new Suv2(); //不行

            ICar<IBrand> bmwSuv3 = new Suv1(); //可以, 但如果含有void Method(TBrand brand)方法,则会要求TBrand支持逆变,就又不行了
            ICar<IBrand> bmwSuv4 = new Suv2(); //同上

// 想要的效果 var cars = new List<ICar<IBrand>>(); cars.Add(bmwSuv1); cars.Add(bmwSuv2); cars.Add(bmwSuv3); cars.Add(bmwSuv4); foreach (var car in cars) { car.StartEngine(); } } }
复制代码
guu的主页 guu | 初学一级 | 园豆:157
提问于:2017-12-07 16:32
< >
分享
最佳答案
1

逆变协变这个你换个思路来看

协变,从具体往抽象(从派生类往父类变动)

逆变,从抽象往具体(同上相反)

从关键字看

协变 out (直接可以看成返回)

逆变 in  (直接可以看为入参)

你写的代码的问题有两个

1.这里是协变(out)

2.class级别是不支持逆变和协变的(隐式支持协变,但这个是另一个话题了),所以你这边不可能直接从Car<T>中鼓捣出个类似于Cat<out T>的玩意出来,所以你第一行是没办法了

收获园豆:40
Daniel Cai | 大侠五级 |园豆:9048 | 2017-12-07 17:21

第一行那种果然是因为类不支持逆变协变引起…… 这个明白了

但是后两行呢,有没有解决办法?

guu | 园豆:157 (初学一级) | 2017-12-07 17:31

@guu: 你都已经看到了这两个关键字了

协变 out 只能做返回

逆变 in 只能做入参

如果你要支持void Method(TBrand brand)并保持协变的话那就会出问题了

本来协变支持往上变,如果你这个可行那么

ICar<IBrand> bmwSuv3 = new Suv1();这个没问题(建立在可行的基础上)

现在假设你的IBrand里面有个void Print();的方法,那么Method(TBrand brand)也应该可以调用这个方法对吧?假设这个Method(TBrand brand)里面调用的就是这个Print的方法。

如果这个时候在IBrand上还有个ISuperBrand(派生关系IBrand extend ISuperBrand),这个ISuperBrand里面可不一定有这个Print的方法,那么当我使用协变

ICar<ISuperBrand> item=new ...();

item.Method(isuperBrandInstance),这个时候就出问题了,ISuperBrand的实现对象中没有这个实现,而.net本身是保证编译安全性的,而你这块却不满足,因此是没办法在这里实现你的需求的。

Daniel Cai | 园豆:9048 (大侠五级) | 2017-12-07 17:44

@Daniel Cai: 这回弄明白了,谢谢!

guu | 园豆:157 (初学一级) | 2017-12-08 09:12

@Daniel Cai: 突然又有个疑问, “如果这个时候在IBrand上还有个ISuperBrand(派生关系IBrand extend ISuperBrand),这个ISuperBrand里面可不一定有这个Print的方法 .....”

因为我限定了ICar<IBrand>,泛型参数协变也只能变体到IBrand吧,而不是它的父接口ISuperBrand?或是我这里理解不对?

guu | 园豆:157 (初学一级) | 2017-12-08 09:39

@guu: 恩,那个例子是不恰当。换个姿势来一次吧。

如果你一个接口或委托是out的,那么就表示这块可以安全的向上转对吧?

反之如果你一个接口或委托是in的,那么就表示这块可以安全的向下转对吧?

基于上面两个结论看,如果你一个接口是out的,但你又有个方法是要接受一个in的东西,那么就会出现奇葩的在这个方法内部的这个泛型对象可以随心所欲的变化(因为有in语义,可以安全的向下转,也就会出现入参object,里面可以直接当string用这种场景了),这种谁hold住?

Daniel Cai | 园豆:9048 (大侠五级) | 2017-12-08 10:45

@Daniel Cai: 哈哈,这次直接了当,多谢多谢!

guu | 园豆:157 (初学一级) | 2017-12-08 10:53
其他回答(2)
0

Car<IBrand> bmwSuv1 = new Suv1(); //不行 Suv1不行,Car<Bmw>是抽象类,逆变协变只支持接口和委托
Car<IBrand> bmwSuv2 = new Suv2(); //不行 Car<IBrand>是继承ICar<TBrand>的抽象类,Suv2是继承ICar<TBrand>的类,两个都是派生类,不能直接转换

ICar<IBrand> bmwSuv3 = new Suv1(); //可以, 但如果含有void Method(TBrand brand)方法,则会要求TBrand支持逆变,就又不行了,因为void Method(TBrand brand)把TBrand当成了参数类型,那么必须支持逆变,(in逆变修饰参数类型,out协边修饰返回值)
ICar<IBrand> bmwSuv4 = new Suv2(); //同上

 

 

按照我个人的理解的。。。不知道对不

收获园豆:5
猫出没 | 园豆:235 (菜鸟二级) | 2017-12-07 17:39

基本上是这样的,但其实还有个协变逆变互换的方法,那就是通过一个接口

IConvariant<in T>把void Method(TBrand brand)改成void Method(IConvariant<TBrand> brand),

这样 ICar<out TBrand>就可以变成ICar<in TBrand>,从而达到我所想要的目的…… 但实际上,使用起来还是有问题

支持(0) 反对(0) guu | 园豆:157 (初学一级) | 2017-12-08 09:20
0

in 和 out 是行为约束
in 表示泛型类型 T ,只作为输入, 所以可以保证 T 的子类可以被当作T来使用.
out 表示泛型类型 T ,只作为输出, 所以可以保证 T 可以被当作 T 的父类来使用.
同时 in 和 out 只能使用在委托和接口上.

再说代码 你写了一个 void Method(TBrand brand);
那么问题来了,当 一个 Bmw 牌的 suv 车子 遇到 一个 Benz 的牌子 是什么意思呢...
是不是应该是 void Method(ICar<IBrand> car);这样好理解一下.

收获园豆:5
长蘑菇星人 | 园豆:1710 (小虾三级) | 2017-12-07 17:42

Method这个方法只是我随便举的一个例子,可能在这个例子中传品牌是不太妥当~~,我实际上只是要演示如何在有类似这样的参数传入时怎么协变的问题~

支持(0) 反对(0) guu | 园豆:157 (初学一级) | 2017-12-08 09:23
   您需要登录以后才能回答,未注册用户请先注册