C#我对协变和逆变的理解

C#协变和逆变

由于看书的时候对协变和逆变还是不太理解,因此从StackOverflow上看到了一个很好的回答:c# - still confused about covariance and contravariance & in/out - Stack Overflow,在这里我对这个回答进行汉化同时加上一点我自己的理解,帮助大家一起学习。

用到的类和接口

首先我们用如下的类进行介绍:

class Animal{} // 动物类

class Mammal: Animal{} // 哺乳动物类

class Dog:Mammal{} // 狗类

接下来用三个泛型接口来解释inout本人注:inout只有用于泛型接口和委托的时候才表示协变和逆变,用在其他普通的函数上只是表示仅作参数或者返回值,不记得从哪个博客还是视频里看到的了):

interface IInvariant<T> //不变的接口
{
    T get();// 通过
    void Set(T t);// 通过
}
interface IContravariant<in T>
{
    //T Get();// 编译错误,因为in表示作为参数,不能作为返回值
    void Set(T t);//通过,逆变的类型仅能作为参数
}
interface IConvariant<out T>
{
    T Get();// 通过,协变类型仅作为返回值
    //void Set(T t);// 编译错误,out表示作为返回值
}

不变Invariance(in和out都不要的情况)

考虑IInvariance<Mammal>

  • IInvariance<Mammal>.Get() - 返回一个Mammal
  • IInvariance<Mammal>.Set(Mammal)- 接受一个Mammal

如果我们尝试``IInvariance invariantMammal=(IInvariance)null`,会发生什么?

  • 不管是谁调用IInvariance<Mammal>.Get(),都希望获得一个Mammal(哺乳动物),但是IInvariance<Animal>.Get()返回一个Animal(动物),不是所有的动物都是哺乳动物,因此这个操作是不兼容的
  • 不管谁调用IInvariance<Mammal>.Set(Mammal)都期望接收一个Mammal(哺乳动物)当做参数,同时IInvariance<Animal>.Set(Animal)接收的任何Animal类型(包括Mammal),因此这个操作是兼容的
  • 总结:上述赋值是不兼容的。

如果我们尝试IInvariance<Mammal> invariantMammal=(IInvariance<Dog>)null会发生什么?

  • 不管谁调用IInvariance<Mammal>.Get(),都期望获得一个Mammal,IInvariance<Mammal>.Get()返回一个Dog,每一个Dog都是一个Mammal(狗都是哺乳动物),因此这个操作是兼容的。

  • 不管谁调用IInvariance<Mammal>.Set(Mammal)希望能接收一个Mammal参数,而IInvariance<Dog>.Set(Dog)只接收Dog,不是每一个Mammal都是Dog,因此这个操作是不兼容的。

  • 总结:上述赋值是不兼容的。

代码检查一下是否正确:

IInvariant<Animal> invariantAnimal1 = (IInvariant<Animal>)null; // ok
IInvariant<Animal> invariantAnimal2 = (IInvariant<Mammal>)null; // compilation error
IInvariant<Animal> invariantAnimal3 = (IInvariant<Dog>)null; // compilation error

IInvariant<Mammal> invariantMammal1 = (IInvariant<Animal>)null; // compilation error
IInvariant<Mammal> invariantMammal2 = (IInvariant<Mammal>)null; // ok
IInvariant<Mammal> invariantMammal3 = (IInvariant<Dog>)null; // compilation error

IInvariant<Dog> invariantDog1 = (IInvariant<Animal>)null; // compilation error
IInvariant<Dog> invariantDog2 = (IInvariant<Mammal>)null; // compilation error
IInvariant<Dog> invariantDog3 = (IInvariant<Dog>)null; // ok

**重点:**不管类型在类继承结构中是高还是低,泛型类型之间总是存在各种原因的不兼容。

协变 out

如果使用out来修饰ICovariant<Mammal>,那么它声明了两个东西:

  1. 方法返回Mammal类型。
  2. 所有方法都不接受Mammal类型——这是被out限制的。

当我们尝试ICovariant<Mammal> covariantMammal = (ICovariant<Animal>)null,会发生什么?

  • 不管谁调用ICovariant<Mammal>.Get(),都期望返回一个Mammal,但是 ICovariant<Animal>.Get()返回一个Animal。不是所有的Animal都是Mammal(动物不全是哺乳动物),因此这是不兼容的。
  • ICovariant.Set(Mammal) 不再可用了,因为out表示不能用作参数,只能用作返回值。
  • 总结:上述操作是不兼容的。

当我们尝试ICovariant<Mammal> covariantMammal = (ICovariant<Dog>)null会发生什么?

  • 不管谁调用ICovariant<Mammal>.Get()都期望返回一个Mammal,ICovariant<Dog>.Get()返回的是一个Dog,所有的Dog都是Mammal,所以这个操作是兼容的。
  • ~~ICovariant.Set(Mammal)~~这个不再是问题了,因为out进行了约束,只能当做返回值。
  • 总结:上述操作是兼容的。

可以看如下验证代码:

ICovariant<Animal> covariantAnimal1 = (ICovariant<Animal>)null; // ok
ICovariant<Animal> covariantAnimal2 = (ICovariant<Mammal>)null; // ok!!!
ICovariant<Animal> covariantAnimal3 = (ICovariant<Dog>)null; // ok!!!

ICovariant<Mammal> covariantMammal1 = (ICovariant<Animal>)null; // compilation error
ICovariant<Mammal> covariantMammal2 = (ICovariant<Mammal>)null; // ok
ICovariant<Mammal> covariantMammal3 = (ICovariant<Dog>)null; // ok!!!

ICovariant<Dog> covariantDog1 = (ICovariant<Animal>)null; // compilation error
ICovariant<Dog> covariantDog2 = (ICovariant<Mammal>)null; // compilation error
ICovariant<Dog> covariantDog3 = (ICovariant<Dog>)null; // ok

逆变 in

如果使用in来修饰IContravariant<Mammal>,那么它声明了两个东西:

  • 所有的方法接收一个Mammal。
  • 所有的方法都不能返回一个Mammal——这是被in限制的。

如果我们尝试IContravariant<Mammal> contravariantMammal = (IContravariant<Animal>)null会发生什么?

  • ~~IContravariant.Get()~~不再可行了,因为in限制了只能当做输入参数。
  • IContravariant<Mammal>.Set(Mammal)期望接收一个Mammal,IContravariant<Animal>.Set(Animal)期望接收一个Animal,由于所有的Mammal都是Animal,因此这个操作是兼容的。注意是Mammal->Animal!
  • 总结:以上操作是兼容的。

如果我们尝试IContravariant<Mammal> contravariantMammal = (IContravariant<Dog>)null会发生什么?

  • ~~IContravariant.Get()~~不存在了,因为in进行了约束不能当做返回值。
  • IContravariant<Mammal>.Set(Mammal)期望接收一个Mammal参数,IContravariant<Dog>.Set(Dog)接收一个Dog为参数,结果范围还变小了,也就是不是所有的Mammal都是Dog,因此是不兼容的。注意是Mammal->Dog!
  • 总结:上述操作是不兼容的。

**这里一定要理解为什么兼容和不兼容,主要就是方向问题!

下面是代码验证:

IContravariant<Animal> contravariantAnimal1 = (IContravariant<Animal>)null; // ok
IContravariant<Animal> contravariantAnimal2 = (IContravariant<Mammal>)null; // compilation error
IContravariant<Animal> contravariantAnimal3 = (IContravariant<Dog>)null; // compilation error

IContravariant<Mammal> contravariantMammal1 = (IContravariant<Animal>)null; // ok!!!
IContravariant<Mammal> contravariantMammal2 = (IContravariant<Mammal>)null; // ok
IContravariant<Mammal> contravariantMammal3 = (IContravariant<Dog>)null; // compilation error

IContravariant<Dog> contravariantDog1 = (IContravariant<Animal>)null; // ok!!!
IContravariant<Dog> contravariantDog2 = (IContravariant<Mammal>)null; // ok!!!
IContravariant<Dog> contravariantDog3 = (IContravariant<Dog>)null; // ok

为什么不同时使用in和out?

如果同时使用的话表示:

  • T类型不作为参数
  • T类型不作为返回值

那么泛型接口就一点都不泛型了。

怎么记忆?

  • covariant协变比contravaraint逆变短,正好和outin的长度相反。

  • 参数是逆变,返回值是协变

    对于方向来说也是,返回值是顺着看从右往左转型,参数是逆着看从左往右转型

    Interface<左> a=Interface<右>;

个人对于逆变和协变的理解

逆变不能理解主要是对方向问题没有理解,其实都是通用的,都要求是从派生类转换成基类

对于返回值来说,也就是协变,例如Func<object> foo2=()=>return string;,是从string转向object从右往左

对于参数来说,也就是逆变,例如Action<string> foo=(object o)=>o.ToString();,也是从string转向object从左往右

参考如下代码:

public class TTT
{
    public void Test(string str)
    {
        Console.WriteLine(str.GetType().ToString());
    }

    public void Test2(object o)
    {
        Console.WriteLine(o.GetType().ToString());
    }

    public void Func()
    {
        Action<object> foo;// in 逆变 Action<in T>
        foo = Test;// error 对string操作不一定都能对object进行

        Action<string> foo2;// in 逆变 Action<in T>
        foo2 = Test2;// ok 对object的操作一定都能对string进行操作
    }

}

而对于协变就更好理解了:

  public class TTT
  {
      public object Test()
      {
          return 1;
      }

      public string Test2()
      {
          return "111";
      }

      public void Func()
      { 
          Func<object> foo;// Func<out T> 协变
          foo = Test2;// ok 对object的操作都能对string操作

          Func<string> foo2;//Func<out T> 协变
          foo2 = Test();// error 对string的操作不一定都能对object进行
      }

  }
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值