国内最全IT社区平台 联系我们 | 收藏本站
华晨云阿里云优惠2
您当前位置:首页 > php开源 > php教程 > Scala之“逆变”合理性的思考

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

来源:程序员人生   发布时间:2016-11-20 17:05:07 阅读次数:2280次

Scala之“逆变”公道性的思考

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

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

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.

在回顾完上述表述以后,我们来重新审视1下“逆变”存在的公道性。首先定义以下1个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种“复合”类型,它们的类型是由它们的参数和返回值的类型决定的,因此我们可以很自然的延展出这样1个规则:对具有相同参数列表类型和返回值类型的函数,如果传给函数1的参数类型一样可以传给函数2,而传给函数2的参数未必都能传给函数1,也就是说,只从参数部份考量,函数1可以被函数2替换,即函数1是父类,函数2 是子类。

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

让我们再延伸地思考1下,我们可以说:由于f2是“消费”(consume)1个较为“通用”的父类型,这使得函数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原则换1种方法表述为:

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

虽然我们仍然在使用里氏替换原则来分析和辨认“逆变”的场景,但是我们不能不承认这类解释仍然只是1种逻辑上的逆推,它的解释总是让人觉得不是那末“解痒”,在本文的最后,我试图从正面给出1种“逆变”公道性的解释:

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

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

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

  • 如果逆变类有1个类族,那末这个类族不会是自上而下的树状结构,而是多条单线继承的路径组合,比如:RemoteController[AirConditioner] <: RemoteController[Haier]; RemoteController[AirConditioner] <: RemoteController[Gree]等等
  • 逆变类的父类和子类虽然作为父类和子类有替换关系,但是却没有任何继承关系,逆变类的子类之所以能够替换父类常常是它涵盖了父类的功能(针对某个更具体形变类型实现功能),而不存在继承父类实现的动作,因此逆变类的子类实现起来反而更加复杂。
生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠
程序员人生
------分隔线----------------------------
分享到:
------分隔线----------------------------
关闭
程序员人生