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

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]等等
  • 逆变类的父类和子类虽然作为父类和子类有替换关系,但是却没有任何继承关系,逆变类的子类之所以能够替换父类常常是它涵盖了父类的功能(针对某个更具体形变类型实现功能),而不存在继承父类实现的动作,因此逆变类的子类实现起来反而更加复杂。
波比源码 – 精品源码模版分享 | www.bobi11.com
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需!
7. 如遇到加密压缩包,请使用WINRAR解压,如遇到无法解压的请联系管理员!

波比源码 » Scala之“逆变”合理性的思考

发表评论

Hi, 如果你对这款模板有疑问,可以跟我联系哦!

联系站长
赞助VIP 享更多特权,建议使用 QQ 登录
喜欢我嘛?喜欢就按“ctrl+D”收藏我吧!♡