尽管 Pharo 提供了类之间的单继承,但它支持一种称为 Trait 的机制,用于在不相关的类之间共享类片段(行为与状态)。Trait 是方法的集合,可以被多个不受继承关系约束的类重用。自 Pharo 7.0 起,Trait 也可以包含状态。
使用 Trait 可以在不同类之间共享代码而无需重复代码。这使得类可以轻松地在多个类之间重用有用的行为。
正如我们稍后将展示的,Trait 提供了一种以规范方式组合和解决冲突的方法。在 Pharo 中,冲突的解决方式并非像其他语言那样由最后加载的方法决定。相反,组合者(无论是类还是 Trait)始终具有优先权,并可以在其上下文中决定如何解决冲突:方法可以被排除,但在组合时仍然可以通过新名称访问。
11.1 一个简单的Trait
以下代码定义了一个 Trait。uses:
子句是一个空数组,表示该 Trait 不包含其他 Trait。
清单11-1 一个简单的Trait
Trait named: #TFlyingAbility
uses: {}
package: 'Traits-Example'
Trait可以定义方法。TraitTFlyingAbility
定义了单个方法fly
。
TFlyingAbility >> fly
^ 'I''m flying!'
现在我们定义一个名为Bird
的类。它使用了TraitTFlyingAbility
,因此类包含了fly
方法。
Object subclass: #Bird
uses: TFlyingAbility
instanceVariableNames: ''
classVariableNames: ''
package: 'Traits-Example'
Bird
类的实例可以执行fly
消息
Bird new fly
>>> 'I''m flying!'
11.2 使用必需的方法
Trait方法不必定义一个完整的行为。Trait方法可以调用使用它的类上可用的方法。
在这里,greeting
方法调用了并未在TraitTGreetable
中定义的name
方法。在这种情况下,使用Trait的类必须实现这样一个必需的方法。
Trait named: #TGreetable
uses: {}
package: 'Traits-Example'
TGreetable >> greeting
^ 'Hello ', self name
请注意,Trait中的self
代表消息的接收者。与类和默认方法相比,没有什么变化。
Object subclass: #Person
uses: TGreetable
instanceVariableNames: ''
classVariableNames: ''
package: 'Traits-Example'
我们定义了使用TraitTGreetable
的Person
类。在Person
类中,我们定义了name
方法。greeting
方法将调用它。
Person >> name
^ 'Bob'
Person new greeting
>>> 'Hello Bob'
11.3 Trait里的self代表消息接收者
人们可能会想,在Trait中self
指的是什么。但是,在类中定义的方法中使用的self
和在Trait中定义的方法之中使用的self
之间没有区别。self
总是代表着接收者。事实上,无论方法是在一个类或一个Trait中定义的,对self
没有影响。
我们定义了一个小Trait来证明这一点:方法WhisAmi
只返回self
。
Trait named: #TInspector
uses: {}
package: 'Traits-Example'
TInspector >> whoAmI
^ self
Object subclass #Foo
uses: TInspector
instanceVariableNames: ''
classVariableNames: ''
package: 'Traits-Example'
下面的代码片段显示self
是接收者,即使从Trait的方法中返回时也是如此。
| foo |
foo := Foo new.
foo whoAmI == foo
>>> true
11.4 Trait的状态
从Pharo 7.0开始,Trait也可以定义实例变量。这里,TraitTCounting
定义了一个名为#count
的实例变量。
Trait named: #TCounting
instanceVariableNames: 'count'
package: 'Traits-Example'
Trait可以通过定义一个后面跟着类名的initialize
方法来初始化其状态。请注意,这是一个编码约定。这里,TraitTCounting
定义了方法initializeTCounting
。
TCounting >> initializeTCounting
count := 0
TCounting >> increment
count := count + 1
^ count
Counter
类使用TraitTCounting
:它的实例将有一个名为count
的实例变量。
Object subclass: #Counter
uses: TCounting
instanceVariableNames: ''
classVariableNames: ''
package: 'Traits-Example'
若要正确地初始化计数器实例,Counter
类的initialize
方法应该调用先前定义的Trait方法initializeTCounting
。
Counter >> initialize
self initializeTCounting
下面的代码显示我们创建了一个计数器,并且它有一个初始化良好的实例变量。
Counter new increment; increment
>>> 2
11.5 一个类可以使用两个Trait
一个类并不局限于只能使用一个Trait。它可以使用多个Trait。假设我们定义了另一个称为TSpeakingAbility
的Trait。
Trait named: #TSpeakingAbility
uses: {}
package: 'Traits-Example'
该Trait定义了一个名为speak
的方法
TSpeakingAbility >> speak
^ 'I''m speaking!'
现在,我们定义了第二个TraitTFlyingAbility
Trait named: #TflyingAbility
instanceVariableNames: ''
package: 'Traits-Example'
这个Trait定义了一个fly
方法.
TFlyingAbility >> fly
^ 'I''m flying'
现在,Duck
类可以同时使用TFlyingAbility
和TSpeakingAbility
,如下所示:
Object subclass: #Duck
uses: TFlyingAbility + TSpeakingAbility
instanceVariableNames: ''
classVariableNames: ''
package: 'Traits-Example'
Duck
类的实例从这两个Trait中获得所有行为。
| d |
d := Duck new.
d speak
>>> 'I''m speaking!'
d fly
>>> 'I''m flying!'
11.6 重写方法优先于Trait方法
源自 Trait 的方法表现得就像它是在使用该 Trait 的类(或 Trait)中定义的一样。现在,Trait 的使用者(无论是类还是另一个 Trait)总是可以重新定义源自 Trait 的方法,并且在使用者中的重新定义优先于 从 Trait 继承的方法。
让我们举例说明一下。在Duck
类中,我们可以重新定义方法speak
来做其他事情,例如发送消息quack
。
Duck >> quack
^ 'QUACK'
Duck >> speak
^ self quack
这意味着
Trait方法
speak
不能再从类中访问,并且取而代之的是使用新方法,即使是使用了
speak
的方法也是如此。
Duck new speak
>>> 'QUACK'
我们定义了一个新的名为doubleSpeak
的方法如下:
TSpeakingAbility >> doubleSpeak
^ 'I double: ', self speak, ' ', self speak
下面的示例显示,Duck
类的方法speak
的本地重新定义版本优先于TraitTSqueakingAbility
的版本。
Duck new doubleSpeak
>>> 'I double: QUACK QUACK'
11.7 访问被重写的Trait方法
有时,您希望重写Trait中的方法,同时仍然希望能够访问被重写的方法。这可以通过使用@
和->
运算符在类Trait组合子句中为被覆盖的方法创建别名来实现,如下所示:
Object subclass: #Duck
uses: TFlyingAbility + TSpeakingAbility @{#originalSpeak -> #speak}
instanceVariableNames: ''
classVariableNames: ''
package: 'Traits-Example'
请注意,箭头表示新方法与旧方法相同:它只是有了一个新名字。在这里,我们说originalSpeak
是speak
的新名字。
与其他方法一样,可以通过发送包含新名字的消息来访问被重写的方法。在这里,我们定义了differentSpeak
方法,它发送的是originalSpeak
消息。
Duck >> differentSpeak
^ self originalSpeak, ' ', self speak
Duck new differentSpeak
>>> 'I''m speaking! QUACK'
请注意,别名不是完整的方法重命名。事实上,如果被重写的方法是递归的,它将不会调用新名称,而是调用旧的名称。别名只是为现有方法指定一个新名称,它不会更改其定义:方法体中的任何内容都不会更改。
11.8 处理冲突
在同一个类中使用的两个Trait可能定义了同名的方法。这种情况是一种冲突。要解决这样的冲突,有两种策略:
使用
exclude
操作符(-
),你可以从定义该方法的 Trait 中排除冲突的方法。在这种情况下,类中将使用另一个方法。在类中本地重新定义冲突的方法。在这种情况下,冲突的方法会被覆盖,新的重新定义的行为将成为类中可用的行为。请注意,被覆盖的方法可以通过之前提到的
@
操作符使其仍然可访问。
这里有一个例子。让我们定义另一个名为THighFlyingAbility
的Trait。
Trait named: #THighFlyingAbility
instanceVariableNames: ''
package: 'Traits-Example'
该Trait定义了一个fly
方法
THighFlyingAbility >> fly
^ 'I''m flying high'
当我们定义使用两个 Traits THighFlyingAbility
和 TFlyingAbility
的类 Eagle
时,发送 fly
消息时会发生冲突,因为运行时不知道要执行哪个方法。
Object subclass: #Eagle
uses: THighFlyingAbility + TFlyingAbility
instanceVariableNames: ''
classVariableNames: ''
package: 'Traits-Example'
Eagle new fly
>>> 'A class or trait does not properly resolve a conflict between
multiple traits it uses.'
11.9 解决冲突:排除方法
为了解决冲突,在合成过程中,我们可以简单地从TraitTFlyingAbility
中排除fly
方法,如下所示:
Object subclass: #Eagle
uses: THighFlyingAbility + (TFlyingAbility - #fly)
instanceVariableNames: ''
classVariableNames: ''
package: 'Traits-Example'
目前只有一个fly
方法,来自THighFlyingAbility
的方法。
Eagle new fly
>>> 'I''m flying high'
11.10 解决冲突:重新定义方法
解决冲突的另一种方法是使用Trait简单地重新定义类中冲突的方法。
Object subclass: #Eagle
uses: THighFlyingAbility + TFlyingAbility
instanceVariableNames: ''
classVariableNames: ''
package: 'Traits-Example'
Eagle >> fly
^ 'Flying and flying high'
现在只有一个fly
方法:在Eagle
类中定义的方法。
Eagle new fly
>>> 'Flying and flying high'
您还可以通过创建与Trait相关联的别名来访问被覆盖的方法,如前所述。
11.11 小结
Trait是可以在不同用户(类或Trait)中重用的一组方法和状态,支持在单一继承语言的上下文中以这种方式进行多重继承。一个类或Trait可以由几个Trait组成。Trait可以定义实例变量和方法。当类中使用的Trait定义具有相同名称的方法时,这会导致冲突。
有两种方法可以解决冲突:
用户(类或Trait)可以在本地重新定义冲突的方法:本地方法优先于Trait方法。
用户可以排除冲突的方法。
此外,可以通过Trait组合子句中定义的别名访问被覆盖的方法。