关于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 对象,说起来,其本质还是为了遵循里氏替换原则。




 

posted @ 2021-07-06 21:41  咸鱼十三郎  阅读(213)  评论(0)    收藏  举报