Scala之“逆变”合理性的思考

#Scala之“逆变”合理性的思考

对于逆变的概念可以参考本系列的前一篇文章: Scala之类型参数化:Type Parameterization 本文的重点是要解释“逆变”的合理性。本文原文出处: http://blog.csdn.net/bluishglc/article/details/52585991 严禁任何形式的转载,否则将委托CSDN官方维护权益!

在思考“逆变”的合理性这个问题上,我们需要清晰地认识到一个前提,即父类与子类之间的关系实质,我们说:如果类A是类B的父类,那么所有出现类A声明的地方,我们都可以使用类B的实例进行替换,或者说所有适用于类A的操作同样适用于类B,简言之就是子类型可以透明无害地替换父类型(也就是里氏替换原则),因为子类型一定也是父类型,但父类型未必一定是子类型(有其他子类型),上述原则就是大家所熟知的里氏替换原则。

Liskov Substitution Principle (里氏替换原则)

It is safe to assume that a type T is a subtype of a type U if you can substitute a value of type T wherever a value of type U is required. 

The principle holds if T supports the same operations as U and all of T’s operations require less and provide more than the corresponding operations in U.

在回顾完上述表述之后,我们来重新审视一下“逆变”存在的合理性。首先定义如下一个Animal类族:

scala> class Animal
defined class Animal

scala> class Bird extends Animal 
defined class Bird

scala> class Dog extends Animal
defined class Dog

现在有f1,f2两个函数:

def f1(x: Bird): Unit // instance of Function1[Bird, Unit]
def f2(x: Animal): Unit // instance of Function1[Animal, Unit]

在这里,f1是f2的父类。为什么?我们知道,Function1的类型声明是Function1[-T1,+R],即函数是虽参数类型逆变,返回值类型协变的。其中随返回值类型协变是很容易理解的,随参数类型逆变往往让人费解,对此,我们同样使用前面提到的原则进行判定:父类可以被子类替换,反之则不可以,但是这里的情况会稍微有些复杂,因为我们要判断的是函数类型之间的可替换关系(即父子关系),我们可以认为函数是一种“复合”类型,它们的类型是由它们的参数和返回值的类型决定的,因此我们可以很自然的延展出这样一个规则:对于具有相同参数列表类型和返回值类型的函数,如果传给函数1的参数类型同样可以传给函数2,而传给函数2的参数未必都能传给函数1,也就是说,只从参数部分考量,函数1可以被函数2替换,即函数1是父类,函数2 是子类。

对于f2,我们说传给它一个Animal实例它可以工作,传给它一个Bird实例它仍然可以工作,在传给它一个Bird实例时,我们就要注意到,这时的f2(仅看参数部分)实例的类型实际上就已经变成f1了,这时所有声明使用f1类型的地方都可以用f2的实例去替换,但是反过来,所有声明了使用f2类型的地方我们是不能用f1的实例去替换的,因为对于f2来说,它可以接受Animal类型的任何其他子类型,比如Dog,但是Dog类型显然不适用于f1的。所以总结起来,f1可以被f2替换,但是f2不能被f1替换,所以f1是f2的父类型!

让我们再延伸地思考一下,我们可以说:因为f2是“消费”(consume)一个较为“通用”的父类型,这使得函数f2本身自然地能接纳和处理给定参数类型的所有子类型,也就意味着f2可以去替换或赋值给那些所有声明使用“具体”子类型为参数的函数,比如f1, 所以f1是父类,f2是子类!这种“消费”关系决定了逆变存在的理由,可以表述为PECS原理:

PECS stands for producer-extends, consumer-super.
In other words, if a parameterized type represents a T producer, use <? extends T>;
if it represents a T consumer, use <? super T>.

上述PECS原则换一种方法表述为:

G[+A]类似一个生产者,提供数据。(大部分情况下称G为容器类型)
G[-A] 是一个消费者,主要用来消费数据。(参考垃圾桶和垃圾的例子)

尽管我们依然在使用里氏替换原则来分析和识别“逆变”的场景,但是我们不能不承认这种解释依然只是一种逻辑上的逆推,它的解释总是让人觉得不是那么“解痒”,在本文的最后,我试图从正面给出一种“逆变”合理性的解释:

我们说在现实世界里,如果有一类物品专门针对另一类物品而存在,除了人们一般认为的伴随着被处理物品的细化,处理品本身需要不断地跟进细化,这是“协变”的场景,也确实有可能会存在另外一种完全相反的情形:即伴随着被处理物品的细化,在掌握了越来越多处被理物品的信息和特征的趋势下,处理物品本身却可以变的愈发的简单(处理面变窄),反倒是那些处理更通用物品的处理类复杂的多,因为它们要考虑的可能的情况更多更复杂,那么这种情形就是典型的“逆变”!

一个典型是例子是空调和遥控器,如果说遥控器是基于空调类型的范型类,那么它天然应该是逆变的,即:RemoteController[-T], 空调品牌和型号越细化,遥控器实际上越单一,实现起来也越简单,反倒是随着空调类型不断地向上抽象,遥控器会变得越加复杂,直到面向所有空调通用的遥控器RemoteController[AirConditioner]诞生,这也就是我们见到过的那种万能遥控器。万能遥控器可以替换任何品牌和型号的遥控器,因此它是它们的子类!

我们可以看到大多数的逆变类有如下一些特点:

  • 如果逆变类有一个类族,那么这个类族不会是自上而下的树状结构,而是多条单线继承的路径组合,比如:RemoteController[AirConditioner] <: RemoteController[Haier] RemoteController[AirConditioner] <: RemoteController[Gree]等等
  • 逆变类的父类和子类虽然作为父类和子类有替换关系,但是却没有任何继承关系,逆变类的子类之所以能够替换父类往往是它涵盖了父类的功能(针对某个更具体形变类型实现功能),而不存在继承父类实现的动作,因此逆变类的子类实现起来反而更加复杂。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Laurence 

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值