协变和逆变

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

posted @ 2018-10-23 17:00  Kyle0418  阅读(442)  评论(0编辑  收藏  举报