• 协变和逆变


    1.前言

    根据维基百科的定义,协变与逆变是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。

    初次看定义一定是一头雾水,不知道协变逆变的具体规则与实现是什么,下面我将会通过几个例子来解释什么叫做协变,什么叫做逆变。

    2.协变逆变的简单认知

    有字面解释来看,协变可以解释为和谐的变化,而逆变可以解释为逆反的变化,可以看出二者是相对应的关系。协变、逆变这一对关系首先出现在数学、物理领域,所以下面我将从数学的角度提供一个示例来解释协变与逆变。

    在数学中可以使用比较符号 > 来比较两个数字的大小,比如 3 > 2 是始终成立的,假设存在两个变量 x 和 y 来存储两个数值,现在定义一个 Double 方法 (x) => ( x * 2 ) (简化为Lambda表达式),可以很容易的得到以下推导式:

    x > y => Double(x) > Double(y)

    以上推导式是始终成立的,在推导式的左右两侧,比较符始终为 > 符号,这种比较符不变的投影就可以看作是协变的,它保留了之前的次序关系;

    再定义一个 Negative 方法 (x) => ( -x ),可以得到以下推导式:

    x > y => Negative(x) < Negative(y)

    以上推导式也是始终成立的,但是在推导式的右侧比较符变为了 < 符号,这种比较符变为相反的投影可以看作是逆变的,它反转了之前的次序关系;

    现在,再定义一个 Squared 方法 (x) => ( x * x ),以下推导式并不是始终成立的:

    x > y => Squared(x) > Squared(y)
    or
    x < y => Squared(x) < Squared(y)

    当 x = 0, y = -1 时,其比较符反转,而当 x = 1, y = 2 时,其比较符不变,在这种情况下这种投影可以看作是不变的,它无法保证是否反转次序。

    3.C#中的协变逆变

    根据上面的例子我们就可以大致地了解了什么是协变和逆变,下面就要转到 C# 中来讲了,在 C# 中这种是否反转地过程可以类比到是否兼容,比如 string 类型兼容于 object 类型,string数组化后依然兼容于object数组,那么可以看出数组是支持协变的,但是协变可能会导致类型不安全,如下例子:

    object[] array = new string[10];
    // error 
    // array[0] = 10;

    这里已经将 array 声明为 string[] 类型,无法再接受 int 类型。

    委托类型也支持协变。如下代码,string兼容于object,返回值为string的fun兼容于返回值为object的委托。

    public delegate object mydelege();
    static string fun2()
    {
        return "";
    } 
    static void Main(string[] args)
    {
        mydelege md1 = fun2; //string兼容于object,返回值为string的fun兼容于返回值为object的委托
    }

    Framework4.0后,支持协变的接口有很多。IEnumerable<T>、IEnumerator<T>、IQueryable<T> 和 IGrouping<TKey, TElement>

    委托类型支持逆变。如下代码:

    class Father{ }class Son : Father{ }class Program{
        public delegate void mydelege1(Father f);
        public delegate void mydelege2(Son s);
        static void fun1(Father s)
        {
        }
        static void fun2(Son s)
        {
        }
        static void Main(string[] args)
        {
            Father f = new Father();
            Son s = new Son();
            f = s;//ok,儿子可以赋值给父亲
            mydelege1 md1 = fun2;//error,输入参数不支持协变,儿子类型的方法不能赋值给父亲类型的委托        
            mydelege2 md2 = fun1;//ok,父亲类型的方法可以赋值给儿子类型的委托,逆变了。
        }
    }
    View Code

    对于一个委托mydelege1(Father f),定义的输入参数类型是Father。 可以看到son是可以赋值给father的,而经过委托定义这样一个投影,发现son类型为参数的方法不能赋值给father类型为参数的委托。即经过这样的委托投影,son类型方法无法赋值给father类型委托了。

    而相反的,father类型为参数的方法却可以赋值给son类型为参数的委托。即经过这样的委托投影,father类型的方法可以赋值给son类型的委托了,因此也就是逆变了。 简单的来看,即投影前Son可以赋值给Fahter,投影(转成委托类型)后Father可以赋值给Son,是一种逆变。

    为什么转成委托后,Son类型的方法不能赋值给Father类型的委托了,很简单,这个方法要接受的是Son的方法要处理的自然是Son类型的值,而Father为参数的委托可能接受Daughter类型的参数(假设Daughter和Son并列的继承了Father)。

    因此Son方法就无法处理了。因此不允许这样操作。 换成代码来说,假设这样的代码合法了:

    mydelege1 md1 = fun2;//假设是合法的,

    那么 md1(new Daughter())的代码要处理的时候,肯定无法处理了。所以,不允许存在这样的协变,而只允许逆变。通过上面的一些例子,可以看出对于委托,协变只存在与返回值中,逆变只存在与输入值中。

  • 相关阅读:
    Centos 卸载Docker
    若依微服务版本 Windows下开发环境搭建
    Iterator 其实很简单(最好理解的工厂模式的例子)
    MySQL replace into详解
    MYSQL事务隔离
    Linux下rpm卸载安装MySQL出现specifies multiple packages
    linux删除文件夹的时候出现设备或者资源忙
    IOS下的safari下localStorage不起作用的问题
    windows下nginx配置报错GetFileAttributesEx
    Linux下增加swap的方法
  • 原文地址:https://www.cnblogs.com/jizhiqiliao/p/9837735.html
Copyright © 2020-2023  润新知