副标题#e#
传统的基于类的面向工具语言的一个主要特点就是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;
#p#副标题#e#
我们可以给出这样的工具范例界说:
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.
#p#分页标题#e#
(译者按,这里,我无论如何和作者找不到同感。基于布局的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++, 只支持简朴的没有范例约束的泛型)
#p#分页标题#e#
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只能吃他们自带的午餐。你不能让他们吃买来的午餐。