作者:Mike Ash,原文链接,原文日期:2015-11-20
译者:Cee;校对:千叶知风;定稿:numbbbbb

在现代的编程语言中,子类型(Subtypes)和超类型(Supertypes)已经成为了非常常见的一部分了。协变(Convariance)和逆变(Contravariance)则能告诉我们什么时候使用子类型或超类型会优于原来使用的类型。这在我们使用的大多数编程语言中非常的常见,但是很多开发者仍然对这些概念感到模糊不清。今天我们就来详细讨论一下。

子类型(Subtypes)和超类型(Supertypes)

我们都知道子类(Subclass)是什么。当你创建一个子类的时候,你就在创建一个子类型。用一个经典的例子来讲,就是用 Animal 的子类去创建一只 Cat

这让 Cat 成为了 Animal 的子类型,也就意味着所有的 Cat 都是 Animal。但并不意味着所有的 Animal 都是 Cat

子类型通常能够替代超类型。很明显懂一点编程知识的任何程序员都知道,在 Swift 中,下面的代码的第一行能够正常的运行,然而第二行则不能:

对于函数类型也是适用的:

这些在 Objective-C 下也能实现,只不过要用 block,而且语法上会显得比较丑。所以我坚定地使用 Swift。

注意,以下的代码是有问题的:

很困惑,不是吗?不用担心,整篇文章就是为了彻底了解为什么第一个版本是可行而第二个版本是不可行的。除此之外,我们在探索的过程中还会了解很多非常有用的东西。

重写(Override)方法

类似的事情在重写方法中也能正确地执行,想象一下有这样一个类:

现在我们建立它的子类,然后重写父类的方法,并改变返回值的类型:

这样做对吗?对。为什么呢?

Liskov 替换原则被用于指导何时该使用子类。简明扼要的来说,它指出任何子类的实例总是能够替代父类的实例。比如你有一个 Animal,你就能用 Cat 替代它;你也总是能够用 CrazyCatLady 替代 Person

下面是使用 Person 作为例子写的一段代码,接下来会有解释来解释清楚:

想象一下当 getAPerson 返回一位 CrazyCatLady。整段代码还可行吗?CrazyCatLady.purchaseAnimal 会返回一只 Cat。这个实例被放入了 animal 中。Cat 是 Animal 的一种,所以它也能够做 Animal 能够做的事情,包括 pet 方法。类似,CrazyCatLady 返回的 Cat 也是有效的。

我们这时把 pet 函数放入 Person 类中,所以我们能够知道一个人所养的特定的动物:

自然,CrazyCatLady 只拥有宠物猫:

现在这样对吗?不对!

为了理解为什么不对,我们来看一下使用这个方法的代码片段:

假设 getAPerson 方法返回了一位 CrazyCatLady,第一行非常的正确:

如果 getAnAnimal 方法返回了一只 Dog,它也是 Animal 的子类但是和 Cat 有截然不同的表现。接下来的一行看上去也非常的正确:

接下来我们的 person 变量中有一位 CrazyCatLady,以及在 animal 变量中有一只 Dog,然后执行了这一行:

爆炸了噜!CrazyCatLady 的 pet 方法期望参数是一只 Cat。对于这只 Dog 就显得无计可施。这个方法也有可能会访问其他的属性或者调用其他 Dog 类所不具备的方法。

这段代码原本是完全正确的。首先它得到 Person 和 Animal,然后调用 Person 中的方法让人拥有这个 Animal。上面的问题在于我们把 CrazyCatLady.pet 方法的参数类型变成了 Cat。这破坏了 Liskov 替换原则:此时的 CrazyCatLady 并不能在任意的地方替代 Person 的使用。

感谢编译器给我们留了一手。它明白使用子类型用于重写方法的参数类型是不正确的,会拒绝编译这个代码。

那在重写方法时使用不同的类型究竟对不对呢?对!事实上,你需要超类型(Supertype)。举一个例子,假设 Animal是 Thing 的子类,那么当我们重写 pet 方法时,参数类型变为 Thing

这保证了可替换性。如果是一个 Person,那么这个方法所传进来的参数类型始终是 Animal,这是 Thing 的一种。

有个重要的规则来了:函数的返回值可以换成原类型的子类型,在层级上了一级;反之函数的参数可以换成原类型的超类型,在层级上了一级。

单独的函数(Standalone functions)

这种子类型和超类型的关系我们已经在类上面了解得很清楚了。它能够通过类与类之间的层级关系直接推出。那么如果是单独的函数关系呢?

这种关系什么时候是对的,什么时候又是错的呢?

这可以被看做是 Liskov 替换原则的一种精简版本。 事实上,你可以把函数想象成是非常小的(mini-objects)、只有一个方法的对象。当你有两个不同的对象类型时,怎么做才能够让这两个对象也遵循我们的原则呢?只有当原对象类型是后者类型的子类型就可以了。那什么时候函数是另一个函数的子类型呢?正如上面所见,当前者的参数是后者的超类型并且返回值是后者的子类型即可。

把这个方法应用在这儿,上面的代码当 A 是 C 的超类型且 B 是 D 的子类型时可以正常的执行。用具体的例子来说:

参数和返回值的类型朝着相反的方向移动。可能不是你所想的那样,但是这就是能让函数正确执行的唯一方法。

这又是一个重要的规则:一个函数若是另外一个函数的子类型,那么它的参数是原函数参数的超类型,返回值是原函数返回值的子类型(译者注:又叫做 Robustness 原则)。

属性(Property)

如果是只读的属性那就很简单。子类的属性必须是父类属性的子类型。只读的属性本质上是一个不接收参数而返回成员值的函数,所以上述的规则依旧适用。

可读可写的属性其实也非常的简单。子类的属性必须和父类的属性类型相同。一个可读可写的属性其实由一对函数组成。Getter 是一个不接收参数而返回成员值的函数,Setter 则是一个需要传入一个参数但无需返回值的函数。看下面的例子:

正如我们之前得到的结论一样,函数的参数和返回值需要各自向上和向下改变一级。然而参数和返回值的类型却是固定的,所以它们的类型都不能被改变:

泛型(Generics)

那如果是泛型呢?给定泛型类型的参数,什么时候又是正确的呢?

理论上来说,这要看泛型参数是如何使用的。一个泛型类型参数本身并不做什么事情,但是它会被用作于属性的类型、函数方法的参数类型和返回类型。

如果泛型参数仅仅被用作函数返回值的类型和只读属性身上,那么 B 需要是 A 的超类型:

如果泛型参数仅被用作于函数方法的参数类型,那么 B 需要是 A 的子类型:

如果泛型参数在上述提到的两方面都被使用了,那么当且仅当 A 和 B 是相同类型的时候才是有效的。这也同样适用于当泛型参数作为可读可写属性的情况。

这就是理论部分,看上去有些复杂但其实很简短。与此同时,Swift 寻求到了其简便的解决之道。对于两个需要相互匹配的泛型类型,Swift 要求它们的泛型参数的类型也需要相同。子类型和超类型都是不被允许的,尽管理论上可行。

Objective-C 事实上比 Swift 更好一些。一个在 Objective-C 中的泛型参数可以在声明时增加 __covariant 关键字来表示它能够接受子类型,而在声明时增加 __contravariant 关键字来表示它能够接受超类型。这在 NSArray 和其他的类的接口中有所体现:

协变和逆变(Convariance and Contravariance)

那些细心的读者会注意到:在标题中提到的两个词至今为止我通篇未提。现在我们既然了解了这些概念,那就来谈一下这几个专业术语。

  • 协变(Convariance)指可接受子类型。重写只读的属性是「协变的」。

  • 逆变(Contravariance)指可接受超类型。重写方法中的参数是「逆变的」。

  • 不变(Invariance)指既不接受子类型,又不接受超类型。Swift 中泛型是「不变的」。

  • 双向协变(Bivariate)指既接受子类型,又接受超类型。我想不到在 Objective-C 或 Swift 中的任何例子。

你会发现这种专业术语非常难记。那就对了,因为这并不重要。只要你懂得子类型、超类型,以及什么时候在特定位置适用一个类的子类或者超类就够了。在需要用到术语的时候看一下就够了。

小结

协变和逆变决定了在特定位置该怎样使用子类型或超类型。通常出现在重写方法以及改变传入参数或者返回值类型的地方。这种情况下我们已经知道返回值必须是原来的子类型,而参数是原来的超类型。整个指导我们这么做的原则就叫做 Liskov 替换原则,意思是任何子类的实例总是能够使用在父类的实例所使用的地方。子类型和超类型就是从这条原则中衍生出来。

今天就到这儿了。记得回来探索更多有趣的事情;或者说就来探索有趣的事情。「更多」可能在这不适用,因为协变这件事并不是那么的令人激动。无论如何,我们的 Friday Q&A 都会听从读者的建议,所以有什么更高的建议或者文章的话,记得给我们写信


译者注:

  1. Swift 中的泛型的确是「不变的(Invariance)」,但是 Swift 标准库中的 Collection 类型通常情况下是「协变的(Convariance)」。举个例子:

  2. Swift 中的 Protocol 不支持这里的类型改变。如果某个协议是继承自另外一个协议而且尝试着「重写」父协议的方法,Swift 会把它当做是另外一个方法。举个例子:

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

Friday Q&A 2015-11-20:协变与逆变的更多相关文章

  1. [改善Java代码]警惕泛型是不能协变和逆变的

    什么叫做协变(covariance)和逆变(contravariance)? 在变成语言的类型框架中,协变和逆变是指宽类型和窄类型在某种情况下(如参数,泛型,返回值)替换或交换的特性,简单的说,协变是 ...

  2. scala学习笔记-类型参数中协变(+)、逆变(-)、类型上界(<:)和类型下界(>:)的使用

    转载自  fineqtbull   http://fineqtbull.iteye.com/blog/477994 有位je上的同学来短信向我问起了Scala类型参数中协变.逆变.类型上界和类型下界的 ...

  3. Scala中的协变,逆变,上界,下界等

    Scala中的协变,逆变,上界,下界等 目录 [−] Java中的协变和逆变 Scala的协变 Scala的逆变 下界lower bounds 上界upper bounds 综合协变,逆变,上界,下界 ...

  4. C#精粹--协变和逆变

    概念 协变和逆变来源于类型和类型之间的绑定,C#4.0开始在泛型的接口和委托上支持协变和逆变,不过在这个版本之前的委托也是支持协变和逆变的.比如数组就支持协变,但是这不是一个好的特性,这C#初期版本从 ...

  5. Scala 基础(十六):泛型、类型约束-上界(Upper Bounds)/下界(lower bounds)、视图界定(View bounds)、上下文界定(Context bounds)、协变、逆变和不变

    1 泛型 1)如果我们要求函数的参数可以接受任意类型.可以使用泛型,这个类型可以代表任意的数据类型. 2)例如 List,在创建 List 时,可以传入整型.字符串.浮点数等等任意类型.那是因为 Li ...

  6. C#4.0泛型的协变,逆变深入剖析

    C#4.0中有一个新特性:协变与逆变.可能很多人在开发过程中不常用到,但是深入的了解他们,肯定是有好处的. 协变和逆变体现在泛型的接口和委托上面,也就是对泛型参数的声明,可以声明为协变,或者逆变.什么 ...

  7. C#协变和逆变

    我们知道在C#中,是可以将派生类的实例赋值给基类对象的.

  8. C# 泛型的协变和逆变

    1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用.如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量.协变和逆变是两个相互对立的概念: 如 ...

  9. 不变(Invariant), 协变(Covarinat), 逆变(Contravariant) : 一个程序猿进化的故事

    阿袁工作的第1天: 不变(Invariant), 协变(Covarinat), 逆变(Contravariant)的初次约 阿袁,早!开始工作吧. 阿袁在笔记上写下今天工作清单: 实现一个scala类 ...

随机推荐

  1. Win10快捷键总结

    微软自发布Windows10以来,大部分的电脑系统都已更新.除了大量的新功能和界面改进,Windows 10中同样包含了一批新的键盘快捷键.熟练使用这些快捷键可以大大提高操作效率,很实用,推荐大家收藏 ...

  2. linux input子系统 — TP A/B(Slot)协议【转】

    本文转载自:http://blog.csdn.net/u012719256/article/details/53609906 将A/B协议这部分单独拿出来说一方面是因为这部分内容是比较容易忽视的,周围 ...

  3. website项目的reference问题

    right click on website project--> property pages dll dependency Check the type column,if you have ...

  4. AutoIT: FileReadLine可以对文件进行读行操作

    $fHandle= ) Then $line= FileReadLine($fHandle) Then ExitLoop Wend EndIf FileClose($fHandle)

  5. zoj 3865

    Superbot Time Limit: 2 Seconds      Memory Limit: 65536 KB Superbot is an interesting game which you ...

  6. package-lock.json到底是干嘛的?(转载)

    package-lock.json到底是干嘛的? https://mp.weixin.qq.com/s/NaVVljKrQAFmHMdbkaYmPg## nvm-windows https://git ...

  7. 在selenium中一些相对常用的JavaScript事件

    输入框输入: 1.找到输入框的id,然后进行输入操作 ordinal :输入框的id parameter :需要输入的内容 browser.execute_script("document. ...

  8. bzoj 4568: [Scoi2016]幸运数字【树链剖分+线段树+线性基】

    一眼做法,好处是好想好写坏处是常数大,容易被卡(bzoj loj 洛谷开O2 能AC,不开有90分-- 大概就是树剖之后维护线段树,在线段树的每个节点上上维护一个线性基,暴力\( 60^2 \)的合并 ...

  9. caj转pdf——包含下载链接

    很多人在知网上下载论文后,想转换成PDF格式,本片一站式教学,包含下载链接. 需要工具 1 caj格式的文件,即要转换的文件. 2 cajviewer,可以在知网的官网上面下载,下载地址参考这里. 3 ...

  10. 深入浅出索引--Mysql45讲笔记记录 打卡day3

    看了极客时间的mysql45讲记录一下自己理解的关于索引部分 为什么会有索引呢? 答:索引就像书的目录一样,可以让你快速知道你要看的部分在多少页.换句话说,索引就是为了提高数据库的查询效率. 索引的数 ...