第三章基于类的高级特性 (Advanced Class-Based Features)
传统的基于类的面向对象语言的一个主要特点就是inheritance, subclassing和subtyping之间的密不可分的联系。很多的面向对象语言的语法,概念,就是从这三者而来的。比如说,通过subclassing, 你可以继承父类的一些方法,而同时你又可以在子类中改写父类的方法。这个改写过的方法,通过subtyping, subsumption, 又可以从一个类型是父类的对象去调用。
但是,inheritance, subclassing, subtyping这三者并不是永远和睦相处的。在一些场合,这三者之间的纠缠不清会妨碍到通过继承或泛型得到的代码重用。因此,人们开始注意到把这三者分离开来的可能性。区分subclassing和subtyping已经很常见了。而其它的一些方法还处于研究的阶段。这一章我们将介绍这样一些方法。
一,对象类型
在早期的面向对象语言中(如Simula), 类型的定义是和方法的实现是混合在一起的。这种方式违反了我们今天已经被广泛认识到的把实现和规范(Specification) 分离的原则。这种分离得原则在开发是团队进行的时候尤其显得重要。
更近期一些的语言,通过引入不依赖于实现的对象类型来区分实现和规范。Modula-3以及其它如Java等的支持class和interface的语言都是采用的这种技术。
在本书中,我们开始引入InstanceTypeOf(cell)时,它代表的概念相当有限。看上去,它似乎只表示用new cell生成的对象的类型,于是,我们并不能用它来表示从其它类new出来的对象。但后来,当我们引入了subclassing, method overriding, subsumption和dynamic dispatch之后,事情变得不那么简单了。我们的InstanceTypeOf(cell)已经可以用来表示从cell的子类new出来的对象,这些对象可以包括不是cell类定义的属性和方法。
如此看来,让InstanceTypeOf(cell)依赖于一个具体的类似乎是不合理的。实际上,一个InstanceTypeOf(cell)类型的对象不一定会跟class cell扯上任何关系。
它和cell类的唯一共同之处只是它具有了所有cell类定义的方法的签名(signature).
基于这种考虑,我们可以引入对象类型的语法:
针对cell类和reCell类的定义:
代码:
class cell is
var contents: Integer :=0;
method get(): Integer is
return self.contents;
end;
method set(n:Integer) is
self.contents := n;
end;
end;
subclass reCell of cell is
var backup: Integer := 0;
override set(n: Integer) is
self.backup := self.contents;
super.set(n);
end;
method restore() is
self.contents := self.backup;
end;
end;
我们可以给出这样的对象类型定义:
代码:
ObjectType Cell is
var contents: Integer;
method get(): Integer;
method set(n:Integer);
end;
ObjectType ReCell is
var contents: Integer;
var backup: Integer;
method get(): Integer
method set(n: Integer);
method restore();
end;
这两个类型的定义包括了所有cell类和reCell类定义的属性和方法的类型,但却并不包括实现。这样,它们就可以被当作与实现细节无关的的接口以实现规范和实现的分离。两个完全无关的类c和c’, 可以具有相同的类型Cell, 而Cell类型的使用者不必关心它使用的是c类还是c’类。
注意,我们还可以加入额外的类似继承的语法来避免在ReCell里重写Cell里的方法签名。但那只是小节罢了。
二,分离Subclassing和Subtyping.
在我们上一章的讨论中,subtype的关系是建立在subclass关系的基础上的。但如果我们想要让type独立于class, 那么我们也需要定义独立于subclass的subtype.
在定义subtype时,我们又面临着几种选择:subtype是由类型的组成结构决定的呢?还是由名字决定呢?
由类型的组成结构决定的subtype是这样的:如果类型一具有了类型二的所有需要具备的属性和方法,我们就说类型一是类型二的subtype.
由类型名字决定的subtype是这样的:只有当类型一具有了类型二的所有需要具备的属性和方法, 并且类型一被明确声明为类型二的subtype时,我们才认可这种关系。
而如果我们的选择是一,那么那些属性和方法是subtype所必需具备的呢?哪些是可有可无的呢?
由组成结构决定的subtype能够在分布式环境和object persistence系统下进行类型匹配(译者注:对这点,我也不甚明了。看来,纸造得还是不够)。缺点是,如果两个类型碰巧具有了相同的结构,但实际上却风马牛不相及,那就会造成错误。不过,这种错误是可以用一些技术来避免的。
相比之下,基于名字的subtype不容易精确定义,而且也不支持基于结构的subtype.
(译者按,这里,我无论如何和作者找不到同感。基于结构的subtype的缺点是一目了然,不过完美的避免的方法我却看不出来。而基于名字的subtype为什么就不能精确定义呢?C++/Java/C#, 所有流行的OO语言都只支持基于名字的subtype, 也没有发现有什么不够灵活的地方。需要在不同名字但类似结构的类型之间架桥的话,adapter完全可以胜任嘛!)
目前,我们可以先定义一个简单的基于结构的subtype关系:
对两个类型O和O’,
O’ <: O 当 O’ 具有所有O类型的成员。O’可以有多于O的成员。
例如:ReCell <: Cell.
为了简明,这个定义没有考虑到方法的特化。
另外,当类型定义有递归存在的时候(类似于链表的定义),对subtype的定义需要额外地加小心。我们会在第九章及之后章节讲到递归的时候再详细说明。(译者按:第九章啊?饶了我吧!想累死我啊?)
因为我们不关心成员的顺序,这种subtype的定义自动地就支持多重的subtype.
比如说:
代码:
ObjectType ReInteger is
var contents: Integer;
var backup: Integer;
method restore();
end;
那么,我们就有如下的subtype的关系:
代码:
ReCell <: Cell
ReCell <: ReInteger
(译者按,作者的例子中没有考虑到象interface不能包含数据域这样的细节。实际上,如果我们支持对数据域的override, 而不支持shadowing -- 作者的基于结构的subtype语义确实隐含着这样的逻辑— 那么,interface里包含不包含数据域就无关紧要了,因为令人头疼的名字冲突问题已经不存在了)
从这个定义,我们可以得出:
如果c’是c的子类, 那么ObjectTypeOf(c’) <: ObjectTypeOf(c)
注意,这个定义的逆命题并不成立,也就是说:
即使c’和c之间没有subclass的关系,只要它们所定义的成员符合了我们subtype的定义,ObjectTypeOf(c’) <: ObjectTypeOf(c)仍然成立。
回过头再看看我们在前一章的subclass-is-subtyping:
InstanceTypeOf(c’) <: InstanceTypeOf(c) 当且仅当 c’是c的子类
在那个定义中,只有当c’是c的子类时,ObjectTypeOf(c’) <: ObjectTypeOf(c)才能成立。
相比之下,我们已经部分地把subclassing和subtyping分离开了。Subclassing仍然是subtyping, 但subtyping不再一定要求是subclassing了。我们把这种性质叫做“subclassing-implies-subtyping”而不是“subclass-is-subtyping”了。
三,泛型 (Type Parameters)
一般意义上来说,泛型是一种把相同的代码重用在不同的类型上的技术。它作为一个相对独立于其它面向对象特性的技术,在面向对象语言里已经变得越来越普遍了。我们这里之所以讨论泛型,一是因为泛型这种技术本身就很让人感兴趣,另外,也是因为泛型是一个被用来对付二元方法问题 (binary method problem) 的主要工具。
和subtyping共同使用,泛型可以用来解决一些在方法特化等场合由反协变带来的类型系统的困难。考虑这样一个例子:
我们有Person和Vegitarian两种类型,同时,我们有Vegitable和Food两种类型。而且,Vegitable <: Food.
代码:
ObjectType Person is
…
method eat(food: Food);
end;
ObjectType Vegetarian is
…
method eat(food: Vegitable);
end;
这里,从常识,我们知道一个Vegitarian是一个人。所以,我们希望可以有Vegetarian <: Person.
不幸的是,因为参数是反协变的,如果我们错误地认为Vegetarian <: Person, 根据subtype的subsumption原则,一个Vegetarian的对象就可以被当作Person来用。于是一个Vegetarian就可以错误地吃起肉来。
使用泛型技术,我们引入Type Operator (也就是,从一个类型导出另一个类型,概念上类似于对类型的函数)。
代码:
ObjectOperator PersonEating[F<:Food] is
…
method eat(food: F);
end;
ObjectOperator VegetarianEating[F<: Vegetable] is
…
method eat(food: F);
end;
这里使用的技术被称作Bounded Type Parameterization. (Trelli/Owl, Sather, Eiffel, PolyTOIL, Raptide以及Generic Java都支持Bounded Type Parameterization. 其它的语言,如C++, 只支持简单的没有类型约束的泛型)
F是一个类型参数,它可以被实例化成一个具体的类型。 类似于变量的类型定义,一个bound如F<:Vegitable限制了F只能被Vegitable及其子类型所实例化。所以,VegitarianEating[Vegitable], VegitarianEating[Carrot]都是合法的类型。而VegitarianEating[Beef]就不是一个合法的类型。类型VegitarianEating[Vegitable]是VegitarianEating的一个实例,同时它等价于类型Vegitarian. (我们用的是基于结构的subtype)
于是,我们有:
对任意F<:Vegitable, VegitarianEating[F] <: PersonEating[F]
对于原来的Vegitarian类型,我们有:
Vegetarian = VegetarianEating[Vegetable] <: PersonEating[Vegitable]
这种关系,正确地表达了“一个素食者是一个吃蔬菜的人”的概念。
除了Bounded Type Parameterization之外,还有一种类似的方法也可以解决这个素食者的问题。这种方法被叫做:Bounded Abstract Type
请看这个定义:
代码:
ObjectType Person is
Type F<: Food;
…
var lunch: F;
method eat(food: F);
end;
ObjectType Vegetarian is
Type F<: Vegitable;
…
var lunch: F;
method eat(food: F);
end;
这里,F<:Food的意思是,给定一个Person, 我们知道他能吃某种Food, 但我们不知道具体是哪一种。这个lunch的属性提供这个Person所吃的Food.
在创建Person对象时,我们可以先选定一个Food的subtype, 比如说,F=Dessert. 然后,用一个Dessert类型的变量赋给属性lunch. 最后再实现一个eat(food:Dessert)的方法。
这样,Vegetarian <: Person是安全的了。当你把一个Vegetarian当作一个Person处理时,这个Vegitarian可以安全地吃他自带的午餐,即使你不知道他吃的是肉还是菜。
这种方法的局限在于,Person, Vegitarian只能吃他们自带的午餐。你不能让他们吃买来的午餐。
四,彻底划清界限(继续分离Subclassing和Subtyping)
在第二节我们讨论了部分分离Subclassing和subtyping的方法,即subclassing-implies-subtyping. 现今的许多面向对象语言,如Java, C#都是采用了这种技术。除此之外,还有一种进一步分离Subclassing和subtyping的方法。这种被称作inheritance-is-not-subtyping的方法通过完全割裂subclassing和subtyping之间的联系而在更大程度上方便了代码的重用。
它的产生很大程度上是由于人们想要使用在反协变位置上的Self类型 (如Self类型的参数)。当然,增大继承的能力的代价是subsumption的灵活性降低了。当Self类型出现在反协变的位置上时,subclass不再意味着subtype, 因此,subsumption也就不存在了。
下面请考虑这样两个类型:
代码:
ObjectType Max is
var n: Integer;
method max(other:Max): Max;
end;
ObjectType MinMax is
var n: Integer;
method max(other:MinMax): MinMax;
method min(other:MinMax): MinMax;
end;
再考虑两个类:
代码:
class MaxClass is
var n:Integer :=0;
method max(other: Self): Self is
if self.n > other.n then return self else return other end;
end;
end;
subclass MinMaxClass of MaxClass is
method min(other: Self): Self is
if self.n < other.n then return self else return other end;
end;
end;
方法min和max是二元的,因为它操作两个对象:self和other. other的类型是一个出现在反协变位置上的Self类型。
注意,方法max有一个反协变的参数类型Self, 并且它被从类MaxClass继承到了MinMaxClass.
很直观地,类MaxClass对应着类型Max;类MinMaxClass对应着类型MinMax. 为了精确地表示这种对应关系,我们必须针对包含使用Self类型的成员的类重新定义ObjectTypeOf,以便得到ObjectTypeOf(MaxClass) = Max, ObjectTypeOf(MinMaxClass) = MinMax。
为了使以上的等式成立,我们把类中的Self类型映射到ObjectType中的类型名称本身。我们同时让Self类型在继承的时候特化。
在本例中,当我们映射MinMaxClass的类型时,我们把继承来的max方法中的Self类型映射到MinMax类型。而对MaxClass中max方法的Self类型,我们使用Max类型。
如此,我们可以得到,任何MaxClass生成的对象,都具备Max类型。而任何MinMaxClass生成的对象都具备MinMax类型。
虽然MinMaxClass是MaxClass的子类,但这里MinMax却不是Max的子类型(subtype).
举个例子,如果我们假设subtype在这种情况下成立,那么,对以下的这个类:
代码:
subclass MinMaxClass’ of MinMaxClass is
override max(other: Self): Self is
if other.min(self) = other then return self else return other end;
end;
end;
根据我们对Self类型的映射规则和基于结构的subtype规则,我们知道,ObjectTypeOf(MinMaxClass’) = MinMax, 所以,对任何MinMaxClass’生成的对象mm’ ,我们可以知道mm’ : MinMax.
而如果MinMax <: Max成立,根据subsumption, 我们就能推出mm’ : Max.
于是当我们调用mm’.max(m)的时候,m可以是任何Max类型的对象。但是,当max的方法体调用other.min(self)的时候,如果这个other不具有min方法,这个方法就会失败。
由此可见,MinMax <: Max并不成立。
子类(subclass) 在使用反协变的Self类型时就不再具有subtype的性质了。
五,对象协议 (Object Protocol)
从上一节的讨论,我们看到对使用反协变Self类型的类,subclass不再是subtype了。这是一个令人失望的结果,毕竟很多激动人心的面向对象的优点是通过subtype, subsumption来实现的。
不过,幸运的是,虽然失去了subtype, 我们还是可以从中挖掘出来一些可以作为补偿的有用的东西的。只不过,不象subtype, 我们不能享受subsumption了。
下面就让我们来研究这种新的关系。
在第四节的MinMax的例子中,subtype不再成立;简单地使用泛型,引入
ObjectOperator P[M <: Max] is … end; 也似乎没有什么用。P[Max]虽然成立,但P[MinMax]却是不合法的,因为MinMax <: Max不成立。
但是,直观上看,任何支持MinMax这种协议的对象,也支持Max协议的 (虽然我们还不知道这个“协议”到底是个什么东西)。于是,似乎隐隐约约地又一个叫做“子协议”(subprotocol)的家伙在向我们招手了。
为了发现这个子协议的关系,让我们先定义两个type operator (还记得吗?就是作用在类型上的函数):
代码:
ObjectOperator MaxProtocol[X] is
var n: Integer;
method max(other: X) :X;
end;
ObjectOperator MinMaxProtocol[X] is
var n:Integer;
method max(other: X):X;
method min(other: X):X;
end;
这样,Max = MaxProtocol[Max], MinMax = MinMaxProtocol[MinMax]
更一般地说,我们可以定义:
代码:
什么 = 什么-Protocol[什么]
还记得lamda-calculus里的fixpoint吗?给定一个函数F, F(fixpoint(F)) = fixpoint(F)
而在我们这个子协议的type operator里,如果我们认为type operator是作用于类型的函数的话, 那么这个“什么”,就是“什么-Protocol”函数的fixpoint啊!
也就是说:
代码:
什么= fixpoint (什么-Protocol).
除了以上的fixpoint的性质,我们还发现了存在于Max和MinMax之间的关系。
首先,MinMax是MaxProtocol的一个post-fixpoint,即:
代码:
MinMax <: MaxProtocol[MinMax]
其次,我们可以看出:
代码:
MinMaxProtocol[Max] <: MaxProtocol[Max]
MinMaxProtocol[MinMax] <: MaxProtocol[MinMax]
最后,如果我们用<::来表示一个更高阶的子类型关系:
代码:
P <:: P’ 当且仅当 P[T] <: P’[T]
那么,MinMaxProtocol <:: MaxProtocol.
对于子协议的定义,我们可以采取上面的<::的定义,即:
如果S-Protocol<::T-Protocol, 那么我们称类型S和类型T之间是子协议关系。 (1)
我们也可以不用这个高阶的关系,仍然使用<:这个subtype的关系:
如果S<:T-Protocol[S], 那么我们称类型S和类型T之间是子协议关系。 (2)
其实,第一个定义似乎更直观一点,它更明确地显示出子协议关系是作用于类型上的函数(type operator)之间的关系,而不是类型之间的关系。
使用泛型技术,如果我们的某一个类型需要一个实现MaxProtocol的类型来实例化的话,我们可以采用下面两种方法中的一种:
代码:
ObjectOperator P1[X <: MaxProtocol[X]] is … end; (1)
ObjectOperator P2[P <:: MaxProtocol] is … end; (2)
这两种方法在表达能力上是相同的。第一种方法叫做F-bounded parameterization. (译者按,Generic Java据说就采用了这个方法);第二种方法叫做 higher-order bounded parameterization.
对于具体语言的实现,为了方便,我们可以隐藏这个type operator. 语法上可以直接对类型支持subprotocol的关系(用<#来表示)。
对我们的MinMax的例子来说,我们就有:
MinMax <# Max.
(译者按,绕了一大圈,什么fixpoint啊,post-fixpoint啊,什么高阶关系啦,希望没把你绕晕。其实,你只需要记住MinMax <# Max, 就八九不离十了,呵呵)
<#这个关系并不具有subsumption的特性,所以,你不能指望从它身上得到传统OO里面的多态。但是,与泛型相结合,它却是非常有用的。我们可以对我们的generic class给出这样的约束:所有你用来当作类型参数传给我的模板(译者按,我这里使用模板这个不大精确的名词,但也许对大量的C++程序员会更易理解)的类型,必须符合如下规范:·%¥#……%#(((
思考题:
1. Java是支持Covariant的Array的。你可以把一个String[] 类型的对象当作一个Object[]类型来使用。
那么,Java的covariant array是类型安全的吗?为什么?
2. 大家都知道经典的矩形和正方形之间的关系吧?在支持get, set的正方形和矩形之间,并不存在subtype的关系,这是因为subsumption对get或set操作是不安全的。但是,是否正方形和矩形之间就不可能有subtype的关系呢?如果矩形的类型只支持get, 结果会是什么?如果正方形只支持set, 结果又是什么呢?
分享到:
相关推荐
本篇“面向对象方法导论PPT学习教案”旨在介绍面向对象技术及其在C++语言中的应用。 面向对象技术主要包括三个方面:面向对象分析(OOA)、面向对象设计(OOD)和面向对象编程(OOP)。面向对象分析主要关注如何...
第1章 面向对象方法导论.ppt,研究生课程,比较全面详细的概述
UML和模式应用-面向对象分析与设计导论第三版.rar
本书为高清晰版(眼神不好∩…∩),分成两部分,这是第二部分 本书论述运用UML(统一建模语言)和模式进行对象建模的方法和技巧,重点讨论了如何使用面向对象的分析和设计技术来建造一个健壮的和易于维护的系统...
UML和模式应用面向对象分析与设计导论UML和模式应用面向对象分析与设计导论
UML和模式应用面向对象分析与设计导论.pdf
《UML和模式应用——面向对象分析与设计导论(第三版)》是一本深入探讨UML(统一建模语言)以及面向对象分析与设计的权威著作。在信息技术领域,UML作为一种可视化建模语言,是软件开发过程中的重要工具,而面向...
Python基础入门教程 Python语言编程导论08 第八章 面向对象编程 (共114页).ppt Python基础入门教程 Python语言编程导论09 第九章 异常处理与程序调试 (共56页).ppt Python基础入门教程 Python语言编程导论10 ...
《UML与模式应用》是一本深入探讨面向对象分析与设计的著作,主要目标是帮助已经对面向对象技术有所理解的开发者进一步提升他们的专业技能。UML,全称为统一建模语言,是软件工程领域中一种重要的建模工具,用于可视...
UML和模式应用-面向对象分析与设计导论第三版chm.rar 分卷1
UML和模式应用-面向对象分析与设计导论(第三版)的第二部分。
2. 自然语言处理的基本概念:自然语言处理研究的对象是人类日常使用的语言,如中文、英文等,研究的目的是让计算机能够理解和生成自然语言。 3. 语言模型的重要性:大规模语言模型 GPT 是自然语言处理的重要组成...
2. 封装:封装是面向对象的核心特性之一,它限制了对对象内部数据的直接访问,通过提供公共接口来控制数据的操作。实验可能涉及设计私有属性和公有方法,以实现数据的封装。 3. 继承:继承允许一个类(子类)从另一...
标题《R语言导论_中文修正版》和描述表明,本文档是一个对R语言的入门指导材料,它提供了R语言的基础知识、概念和操作,旨在帮助初学者了解并掌握R语言的基本使用。文章的内容经过适当调整,适于初学者学习,并通过...