文章目录
  1. 1. 概念
  2. 2. 例子
  3. 3. 参考

WARNING: 以下内容涉及强烈的个人理解和偏见,限于作者所知和笔力,不保证词可达意和正确性,请读者注意

————————————————-手工分割机———————————————–

马者,所以名形也;白者,所以名色也。名形者非名色也。故曰:白马非马。求马,黄黑马皆可致。求白马,黄黑马不可致。……故黄黑马一也,而可以应有马,而不可以应有白马,是白马之非马审矣。马者,无去取于色,故黄黑皆所以应。白马者有去取于色,黄黑马皆所以色去,故惟白马独可以应耳。无去者,非有去也。故曰:白马非马.马故有色,故有白马。使马无色,由马如己耳。安取白马?故白者,非马也。白马者,马与白也,白与马也。故曰:白马非马也。 –公孙龙

开宗先明义. 简介中提到里氏代换原则,先温习一下什么是里氏代换:

里氏代换原则(Liskov Substitution Principle LSP): 任何基类可以出现的地方,子类一定可以出现.

里氏代换原则放在返回值上,就是返回基类的地方,也可以返回子类。放在参数值上,就是接受基类的函数,也可以接受子类。对于持有返回值的变量来说,就是声明为子类的变量可以被声明为父类的变量代替。对于函数本身来说,就是接受子类的函数可以被接受基类的函数代替。这里可以看出,里氏代换原则描述的事实上是对象。

再看看所谓“协变(Covariance)”“逆变(Contravariance)”这两个拗口的中文术语,其英文定义是什么:

Within the type system of a programming language, a typing rule or a type constructor is:

  • covariant if it preserves the ordering of types (≤), which orders types from more specific to more generic;
  • contravariant if it reverses this ordering;
  • bivariant if both of these apply (i.e., both I<A>I<B> and I<B>I<A> at the same time);
  • invariant or nonvariant if neither of these applies.
    wikipedia#Formal_definition)

英文定义不好理解没关系,这里还有更不好理解的中文定义:

协变(covariant):当委托方法的返回类型具有的派生程度比委托签名更大时,就称为协变委托方法。因为方法的返回类型比委托签名的返回类型更具体 ,所以可以对其进行隐式转换。这样该方法就可以用作委托。协变使得创建可被类和派生类同时使用的委托方法成为可能。

逆变(contravariant):当委托方法签名具有一个或多个参数,并且这些参数的类型派生自方法参数的类型时,就称为逆变委托方法。因为委托方法签名参数比方法参数更具体,因此可以在传递给处理程序方法时对它们进行隐式转换。这样,当创建可由大量类使用的更加通用的委托方法时,使用逆变就更为简单了。

– 以上出自百度百科协变 逆变

中文定义不好的地方在于,它高度概括,但对于想要从定义来理解的人可能毫无帮助。让我想起我那些线性代数教材。

概念

在刚知道这两个概念的时候想必也对它们的含义和作用有过猜测,可能也像我一样相当糊涂,看过定义和网上各种中文的解释说明之后可能更加糊涂了。

要理解这两个概念,还是从根源说起。这一段为了方便自己回忆,会非常啰嗦,如果想快速理解的话直接看例子

covariance也可被译作协方差,没错就是统计分析了里面那个。covariance根源于数学和物理领域,在wiki上的词条说它是用来描述两个变量变化时,它们之间的联系,统分就是采纳这个含义来描述统计量之间样本差异的联系。
协变和逆变放在一起,那应该可以猜测是一对”逆运算”或者说”反义词”,事实上确实是,只是根据需要,在编程语言中的使用位置不同。

接下来会用我擅长的”只有我自己可以理解的列举”来解释我对这两个概念的理解,基于wiki的形式化定义。

  • 首先有两个东西AB,然后有某种过程process,不失一般性,假设存在偏序关系A<=B,且具有传递性、自反性、非对称性.
    • 如果有process(A)<=process(B),就说这种偏序关系这个process上是协变的。
    • 如果有process(B)<=process(A),就说这种偏序关系这个process上是逆变的。

细心的我发现了我只是把wiki的解释翻译成了中文,而且还缺斤少两。

事实上看到这里,联系自己熟悉的编程语言(很多编程语言都实现了这种特性),那么相信你会隐约觉得——“这玩意我好像遇到过?”。你应该遇到过,如果你在声明的时候参数是类型B,但是赋值的时候参数是类型A,或者将A对象赋予给了B类型的变量,前者你遇到了逆变,后者你遇到了协变。

事实上参考中列出的某篇文章用更通俗的语言解释过这两个概念:

“协变”->”和谐的变”->”很自然的变化”->string->object 协变。
“逆变”->”逆常的变”->”不正常的变化”->object->string 逆变。

最初让我疑惑之处在于,在所有参考资料中都有举例在函数中协变和逆变的表现,但看起来好像差不多:同样都是声明的父类,传递的子类。这TM有什么区别?

还是自己姿势不够,应该多学习。另外这个栗子也不好,注意,我要开始举例了。
——前方高能——
许多地方涉及到协变和逆变,但归根结底它只是一种类型变换规则,所以咱们从类型声明和赋值着手。

class Parent{}
class Child extends Parent{}

Parent parent;// 也许你想成为一个父(母)亲
parent = new Child();// 但最初,你还只是个孩子。明明是个孩子,却要以家长的形式存在下去,这是协变。里氏代换说,任何孩子都可以成为家长。

Child child;// 但当你是个家长的时候
child = new Parent();// 你无法回复身份,让自己成为一个孩子。至少在Java中不行。这就是说,Java中你没法把更抽象的父类对象直接丢给一个更具体的子类变量。
// C#中把要丢给别人的协变变量用 out来修饰,也就是说,协变的变量可以用它的子类来代替(实际以子类形式存在的对象可以扔给父类形式声明的变量来管理)

IEat<T>{}  //吃
IEat<Parent> iep = new IEat<Parent>();// 作为家长可以吃
IEat<Child> iec = iep;// 作为家长,可以吃得跟孩子一样,这是逆变。里氏代换说,任何子类都可以代替作为参数的父类。
// C#中把这种只能被使用的逆变变量用 in来修饰,也即是说,逆变的变量可以用它的父类来代替(实际以父类形式存在的行为可以让子类形式声明的变量做出)

那么,简单一句话,孩子协变成家长,家长逆变成孩子。

好像这么一说更令人疑惑了,那么还是借助c#的定义再说一遍。可以看到c#用out修饰协变变量,用in修饰逆变变量。对于函数来说,就是in可以逆变,out可以协变。如果把函数C的调用看作A->C->B这样的数据流过程,那么可以看到,两个箭头的位置涉及到变型,而里氏代换原则告诉我们,子类对象可以代替父类对象,那么自然可以导出结论:在A和B不变的情况下,如果函数D将C的输入口放大到父类,而输出口缩小到子类,则D可以安全代替C。注意到,以上的描述中已经暗含了逆变和协变的定义。直接看好像是相反的意思,实则不然。
我们更具体一点定义D和C,如下:

CReturn C (CParameter)
DReturn extends CReturn D(DParameter super CParameter)

那么我们可以发现,当D<C的时候,DReturn<=CReturn,而且DParameter>=CParameter,即有:

  • D<C->DReturn<=CReturn 协变
  • D<C->DParameter>=CParameter 逆变

协变和逆变可以解释程序语言实现中的许多现象。
例如,按以上的理解,List<Object> l = new List<String>()是允许的,因为StringObject的子类,赋值时子类可以协变成父类。
事实上,在c#中确实可以(未验证)。
然而这句话在Java中无法编译。原因是Java中List接口被定义为泛型类型List<T>,而Java中的泛型类型都是invariant,因此你既不能协变定义泛型变量,也不能逆变使用泛型对象。
有人说那不对啊,我可以用List<? extends Object> l = new List<String>(),这不就是协变了吗?事实上,这叫做使用点变型(use-site variance),即在声明变量的时候告知编译器接受子类型或父类型的参数。extendssuper分别对应协变和逆变方式使用。
有些语言支持声明点变型(declaration-site variance),即在该类型的定义中就表明是协变还是逆变,例如C#和Scala,Java不支持。但是Java支持在类型定义中声明 使用点变型的泛型参数,我认为这可以替代声明点变型
有趣的是,以上的例子中,Java是支持class协变的。例如你可以这样写List<String> l = new ArrayList<String>()或者这样写AbstractList<String> l = new ArrayList<String>()。BTW,数组协变也支持,你可以这样做Object[] obj = new String[].
以下是一些验证代码。

List<Object> list1 = new List<String>();
List<? extends Object> list2 = new List<String>();
List<? super String> list3 = new List<Object>();
List<Object> list4 = new ArrayList<Object>();

好了,这时候已经涉及到了函数和类型的定义,正如上文所说,函数和类型的定义中是否支持协变和逆变,这个跟具体语言有关,可以从协变(Covariance)和逆变(Contravariance)的十万个为什么这篇文章了解到该作者对是否需要、以及什么时候需要协变和逆变的理解。文中提到,当函数返回值是普通类型时,通常以参数逆变、返回值协变为宜。
另一种情况,自然是当返回值为涉及泛型的类型时,这时候参数是协变还是逆变,返回值是协变还是逆变,都取决于其使用方式是否类型安全,例子可以看java泛型通配符-协变与逆变

在类型定义的时候,如果一个子类的函数可以比父类的函数接受更抽象的参数,并返回更具体的值,那么这个函数就可以安全替换父类中的同名函数,这就是里氏代换原则的体现,也是参数值逆变,返回值协变的体现。换句话说,通过支持协变和逆变,编译器保证了里氏代换原则不被打破。

总之,协变和逆变的理解可以总结如下:

  • 是一种类型变换规则,在Java中部分支持
  • 存在是为了“保证里氏代换原则不被打破”
  • 协变是子类作为父类做功,逆变是父类作为子类做功,或者可以这么说,协变是期待一个父类,实际得到子类,逆变是期待一个子类,实际得到父类。哪一个行为被支持,就说哪种变型被支持。Java中内置部分类型的协变,并用extendssuper支持泛型变量的协变和逆变
  • 函数定义中通常使返回值具有协变性,而使参数具有逆变性
  • 函数定义为支持协变和逆变时,将参数值和返回值的类型范围都放大了,但放大方向相反,可以接受比定义更抽象的参数类型,并返回比定义更具体的返回类型
  • Java中所有地方都支持内置的协变,但逆变只有在显式声明时才支持

例子

c# 协变

class Mammals{}
class Dogs : Mammals{}

class Program
{
    // Define the delegate.
    public delegate Mammals HandlerMethod();

    public static Mammals MammalsHandler()
    {
        return null;
    }

    public static Dogs DogsHandler()
    {
        return null;
    }

    static void Test()
    {
        HandlerMethod handlerMammals = MammalsHandler;

        // Covariance enables this assignment.
        HandlerMethod handlerDogs = DogsHandler;
    }
}

c# 逆变

// Event hander that accepts a parameter of the EventArgs type.
private void MultiHandler(object sender, System.EventArgs e)
{
    label1.Text = System.DateTime.Now.ToString();
}

public Form1()
{
    InitializeComponent();

    // You can use a method that has an EventArgs parameter,
    // although the event expects the KeyEventArgs parameter.
    this.button1.KeyDown += this.MultiHandler;

    // You can use the same method 
    // for an event that expects the MouseEventArgs parameter.
    this.button1.MouseClick += this.MultiHandler;

}

参考

Java 协变性 逆变性 学习笔记 - 2014
.NET 4.0中的泛型协变和反变 - 2008
再谈对协变和逆变的理解 - 2014
Java泛型的协变
深入理解 C# 协变和逆变
委托中的变体(C# 和 Visual Basic)
在委托中使用变体(C# 和 Visual Basic).aspx)
对 Func 和 Action 泛型委托使用变体(C# 和 Visual Basic)
协变(Covariance)和逆变(Contravariance)的十万个为什么
scala类型系统:15) 协变与逆变

文章目录
  1. 1. 概念
  2. 2. 例子
  3. 3. 参考