Pharo 是一种非常简单但功能强大的语言。其部分力量不在于语言本身,而在于它的类库。为了在其中高效编程,你需要学习类库如何支持语言和环境。类库完全用 Pharo 编写,并且可以轻松扩展。(回想一下,一个包可以向类添加新功能,即使它没有定义这个类。)
我们的目标不是以繁琐的细节介绍整个 Pharo 类库,而是指出你需要使用(或子类化/重写)的关键类和方法,以便高效编程。在本章中,我们将介绍几乎所有应用程序都需要的基本类:Object
、Number
及其子类、Character
、String
、Symbol
和 Boolean
。
13.1 Object
无论如何,Object
都是继承层次结构的根。实际上,在Pharo中,层次结构真正的根是ProtoObject
,它用于定义伪装成对象的最小实体,但我们可以暂时忽略这一点。
Object
定义了将近400个方法(换句话说,您定义的每个类都会自动提供所有这些方法)。注意:你可以像这样计算一个类中方法的数量:
Object selectors size "实例方法的数量"
Object class selectors size "类方法的数量"
类 Object
提供了所有普通对象共有的默认行为,例如访问、复制、比较、错误处理、消息发送和反射。此外,所有对象都应响应的实用消息也在这里定义。Object
没有实例变量,也不应添加任何实例变量。这是因为有几类从 Object
继承的对象具有特殊的实现(例如 SmallInteger
和 UndefinedObject
),虚拟机(VM)知道这些实现并依赖于某些标准类的结构和布局。
Color >> printOn: aStream
| name |
(name := self name).
name = #unnamed
ifFalse: [
^ aStream
nextPutAll: 'Color';
nextPutAll: name ].
self storeOn: aStream
如果我们开始浏览Object
实例端的方法协议,我们将开始看到它提供的一些关键行为。
13.2 打印Object
每个对象都可以返回其打印形式。你可以在文本窗格中选择任何表达式并选择 Print it 菜单项:这将执行表达式并要求返回的对象打印自身。实际上,这会向返回的对象发送消息 printString
。方法 printString
是一个模板方法,其核心是向接收者发送消息 printOn:
。消息 printOn:
是一个可以专门化的钩子。
Object>>printOn:
方法很可能是您重写得最多的方法之一。该方法接受一个Stream作为参数,将对象的String表示写入stream。默认实现只是将类名前面加上a
或an
。Object>>printString
返回被写入的String。
例如,OpalCompiler
类没有重新定义方法printOn:
并且将消息printString
发送给实例将执行Object
中定义的方法。
OpalCompiler new printString
>>> 'an OpalCompiler'
Color
类显示了printOn:
专门化的一个例子。它输出类的名称,后跟用于生成该颜色的类方法的名称。
Color red printString
>>> 'Color red'
printOn: vs. displayStringOn:
你应该考虑到消息printOn:
是用来在开发时更好地描述你的对象。事实上,当你使用检查器或调试器时,查看对象的精确描述比查看通用描述要有效率得多。printOn:
不是用来在UI列表中很好地显示对象的,因为你通常想要显示不同种类的信息。出于此目的,你应该使用displayStringOn:
。displayStringOn:
的默认实现是调用printOn:
。
请注意,displayStringOn:
是最近才引入的,所以很多库仍然没有对此进行区分。这并不是一个真正的问题,但当您编写新代码时,您应该意识到这一点。
printOn: vs. storeOn:
注意,消息printOn:
与消息storeOn:
是不同的。消息storeOn:
将一个表达式写入其参数流,该表达式可用于重新创建接收方。该表达式在使用消息readFrom:
读取流时执行。另一方面,消息printOn:
只返回接收方的文本表示。当然,这种文本表示可能会将接收者表示为一个自求值的表达式。
10.3 表示和自求值表示
在函数式编程中,表达式在执行时返回值。在Pharo中,消息发送(表达式)返回对象(值)。有些对象有一个很好的属性,它们的值就是它们自己。例如,对象true
的值就是对象本身,即对象true
。我们称这种对象为自求值对象。当您在Playground
上打印对象时,您可以看到对象值的打印版本。下面是一些自求值的例子。
true
>>> true
3@4
>>> (3@4)
$a
>>> $a
#(1 2 3)
>>> #(1 2 3)
Color red
>>> Color red
注意,有些对象(如数组)可能是自求值的,也可能不是,取决于它们所包含的对象。例如,一个布尔值数组是自求值的,而一个person数组不是。下面的例子表明,只有在元素是自求值的情况下动态数组才是自求值的:
{ 10@10 . 100@100 }
>>> {(10@10) . (100@100)}
{OpalCompiler new . 10@100}
>>> an Array(an OpalCompiler (100@100))
记住,字面量数组只能包含字面量。因此,下面的数组包含的不是两个Point,而是六个字面量元素。
#(10@10 100@100)
>>> #(10 #@ 10 100 #@ 100)
许多printOn:
方法的专门化实现了自求值行为。Point>>printOn:
和Interval>>printOn:
的实现是自求值的。
Point >> printOn: aStream
"The receiver prints on aStream in terms of infix notation."
aStream nextPut: $(.
x printOn: aStream.
aStream nextPut: $@.
(y notNil and: [ y negative ])
ifTrue: [
"Avoid ambiguous @- construct"
aStream space ].
y printOn: aStream.
aStream nextPut: $).
Interval >> printOn: aStream
aStream nextPut: $(;
print: start;
nextPutAll: ' to ';
print: stop.
step ~= 1 ifTrue: [ aStream nextPutAll: ' by: '; print: step ].
aStream nextPut: $)
1 to: 10
>>> (1 to: 10) "区间是自求值的"
13.4 身份与相等
在Pharo中, 消息=
测试对象的相等性,而消息==
测试对象的身份。前者用于检查两个对象是否代表着相同的值,而后者用于检查两个表达式是否代表相同的对象。
对象相等性的默认实现是测试对象的身份:
你如你打算覆盖=
, 则应该考虑覆盖hash
。如果您的类的实例会被用作Dictionary
中的键,那么您应该确保被认为相等的实例具有相同的散列值。
==
永远不应该被覆盖,对象识别的语义对于所有类都是相同的。消息==
是ProtoObject
的基本方法。
请注意,与其他Smalltalk相比,Pharo有一些奇怪的相等行为。例如,符号和字符串可以是相等的。(我们认为这是一个bug, 而不是一个特性)
#'lulu' = 'lulu'
>>> true
'lulu' = #'lulu'
>>> true
13.5 类成员
有几个方法允许你查询对象的类。
class
你可以使用 class
消息查询任意对象的类信息。
1 class
>>> SmallInteger
isKindOf:
Object>>isKindOf:
回答接收者的类与参数的类是否相同,或者是参数的子类。
1 isKindOf: SmallInteger
>>> true
1 isKindOf: Integer
>>> true
1 isKindOf: Number
>>> true
1 isKindOf: Object
>>> true
1 isKindOf: String
>>> false
1/3 isKindOf: Number
>>> true
1/3 isKindOf: Integer
>>> false
1/3
是一个分数,是一种数字,因为数字类是分数类的超类,但1/3
不是一个整数。
respondsTo:
Object>>respondsTo:
回答接收者是否理解作为参数给出的消息选择器。
1 respondsTo: #,
>>> false
13.6 关于 isKindOf: 和 respondsTo:
关于 isKindOf:
和 respondsTo:
的使用说明。通常情况下,查询对象的类或询问它理解哪些消息是一个坏主意。与其基于对象的类做出决策,不如简单地发送消息给对象,并让它(基于其类)决定应该如何行为。对象的客户端不应查询对象以决定发送哪些消息。“不问,只告诉”是良好面向对象设计的重要基石。因此,如果你需要使用这些消息,就要小心了。
13.7 浅拷贝(shallowCopy)
拷贝对象会带来一个微妙的问题。由于实例变量是通过引用访问的,所以对象的浅拷贝会与原始对象共享实例变量的引用:
a1 := { { 'harry' } }.
a1
>>> #(#('harry'))
a2 := a1 shallowCopy.
a2
>>> #(#('harry'))
(a1 at: 1) at: 1 put: 'sally'.
a1
>>> #(#('sally'))
a2
>>> #(#('sally))
13.8 深拷贝
有两种方法可以解决由浅拷贝引起的共享问题:(1)使用deepCopy
(2)专门化postCopy
和copy
。
deepCopy
Object>>deepCopy
创建对象任意深度的副本。
a1 := { { { 'harry' } } }.
a2 := a1 deepCopy.
(a1 at: 1) at: 1 put: 'sally'.
a1
>>> #(#('sally'))
a2
>>> #(#(#('harry')))
deepCopy
的问题是,当应用到一个相互递归的结构时,它不会终止:
a1 := { 'harry' }.
a2 := { a1 }.
a1 at: 1 put: a2.
a1 deepCopy
>>> !''... does not terminate!''!
copy
另一个解决方案是使用消息copy
。在Object
上实现如下:
Object>>copy
"Answer another instance just like the receiver.
Subclasses typically override postCopy;
they typically do not override shallowCopy."
^ self shallowCopy postCopy
默认情况下,postCopy
返回self
。这意味着默认情况下,copy
和shallowCopy
是一样的,但是每个子类可以决定自定义postCopy
方法,它充当了一个hook.您应该重写postCopy
来复制任何不应该被共享的实例变量。此外,很有可能postCopy
应该始终执行super postCopy
,以确保父类的状态也被复制。
13.9 Debugging
Object
类还定义了一些与调试相关的方法。
halt
最重要的方法是halt
,要在方法中设置断点,只需在方法体的某个点插入表达式self halt
.注意,因为halt
是在Object
类中定义的,所以,你也可以写成1 halt
。
当halt
消息被发送后,执行将中断,并且程序中的调试器将在该点打开。
你可以使用Halt once
或者Halt if: aCondition
. 看一看Halt
类,它是一个专门用于调试的异常。
assert:
下一个重要的消息是assert:
,它期望一个block作为参数。如果block的计算结果为true
,则继续执行。否则将引发AssertionFailure
异常。如果未以其他方式捕获些异常,则调试器将在此位置打开。assert:
对于支持契约式设计特别有用。最典型的用法是检查对象的公共方法的重要前提条件。Stack>>pop
可以很容易地实现如下(注意,这个定义是一个假设的例子,不是在Pharo 8.0系统中的真实代码):
Stack >> pop
"Return the first element and remove it from the stack."
self assert: [ self isNotEmpay ].
^ self lindedList removeFirst element
不要混淆了Object>>assert:
和TestCase>>assert:
,前者期望一个block作为它的参数(实际上,它可以接受任意参数,只要参数可以理解value
消息,包括一个布尔值);后者出现在SUnit测试框架中,期望一个布尔值作为参数。虽然这两种方法都对调试有用,但它们的用途完全不同。
13.10 错误处理
此协议包含几个用于发送运行时信号的方法
doesNotUnderstand:
当消息查找失败时,会发送消息 doesNotUnderstand:
(在讨论中通常缩写为 DNU 或 MNU)。默认实现,即 Object>>doesNotUnderstand:
,将在此处触发调试器。重写 doesNotUnderstand:
以提供其他行为可能是有用的。
error
Object>>error
和Object>>error:
是可用于引发异常的通用方法。通常,更好的方法是引发您自己定义的异常,这样您就可以将代码中产生的错误与内核类产生的错误区分开来。
subclassResponsibility
抽象方法通过self subclassResponsibilyty
来实现。如果一个抽象类被意外实例化,那么对抽象方法的调用将导致Object>>subclassResponsibility
被执行。
Object >> subclassResponsibility
"This message sets up a framework for the behavior of the class'
subclasses.
Announce that the subclass should have implemented this message."
SubclassResponsibility signalFor: thisContext sender selector
Number new + 1
>>> !''Error: Number is an abstract class. Make a concrete
subclass.''!
shouldNotImplement
self shouldNotImplement
表示继承的方法不适合这个子类。这通常表明类层次结构设计得不太合理。然而,由于单继承的局限性,有时候很难避免这种变通方法。
一个典型的例子是Collection >> remove:
它被Dictionary
继承,但是被标记为未实现。相反,Dictionary
提供了removeKey:
方法。
deprecated: and related
发送 self deprecated:
表示如果已启用弃用,则不应再使用当前方法。你可以使用 Settings browser 在 Debugging 部分中打开/关闭它。参数应描述替代方案。查找消息 deprecated:
和其他相关消息的发送者以了解其用法。
13.11 测试
测试方法和SUnit没有任何关系!测试方法允许你询问有关接收者状态的问题,并返回一个布尔值。
Object
提供了许多测试方法,例如 isArray
、isBoolean
、isBlock
、isCollection
等。通常应避免使用此类方法,因为查询对象的类是一种违反封装的形式。它们通常用作 isKindOf:
的替代方案,但它们在设计中表现出相同的限制。与其测试对象的类,不如简单地发送消息并让对象决定如何处理它。
然而,不可否认的是,其中一些测试方法非常有用。最有用的可能是 ProtoObject>>isNil
和 Object>>notNil
。Null Object 设计模式 可以消除对这些方法的需求,但这通常是不可能的,也不是正确的选择。
13.12 初始化
最后一个不是出现在Object
中,而是出现在ProtoObject
中的关键方法是initialize
。
它之所以重要,是因为在Pharo中,系统为每一个类定义的new
方法为给每一个新创建的实例发送initialize
消息。
这意味着,只要覆盖hook方法initialize
,类的新实例就会自动初始化。initialize
方法通常应该执行一个super initialize
来为任何继承来的实例变量进行初始化。
13.13 数字
Pharo中的数字不是原始的数值,而是真实的对象。当然,数字在虚拟机中是高效实现的,但Number的层次结构与其他的类层次一样是完全可访问和可扩展的。
这个层次结构的根是Magnitude
,它表示支持比较操作符的所有类。Number
添加了各种算术运算符和其他运算符作为抽象方法。Float
和Fraction
分别表示浮点数和小数。Float
的子类(BoxedFloat64
和SmallFloat64
)代表特定架构上的Float
。例如,BoxedFloat64
只适用于64位系统。Integer
也是抽象的,区分了子类SmallInteger
, LargePositiveInteger
和LargeNegativeInteger
。在大多数情况下,用户不需要知道这三个整数类之间的区别,因为值会根据需要自动转换。
Object
└─ Magnitude
└─ Number
├─ Integer
│ ├─ SmallInteger
│ ├─ LargePositiveInteger
│ └─ LargeNegativeInteger
├─ Fraction
│ └─ ScaledDecimal
└─ Float
├─ SmallFloat64
└─ BoxedFloat64
13.14 Magnitude
Magnitude
不仅是Number
的父类,也是其他支持比较操作的类的父类,如Character
,Duration
和Timespan
。
<
和=
方法是抽象的,其它剩下的操作符是一般定义的,例如:
Magnitude >> < aMagnitude
"Answer whether the receiver is less than the argument."
^ self subclassResponsibility
Magnitude >> > aMagnitude
"Answer whether the receiver is greater than the argument."
^ aMagnitude < self
13.15 Number
类似地,Number
将+
, -
, *
, /
定义为抽象的,但所有其他算术运算符都是一般定义的。
所有 Number
对象都支持各种转换操作符,例如 asFloat
和 asInteger
。还有许多生成 Duration
的快捷构造函数方法,例如 hour
、day
和 week
。
Number
直接支持常见的数字函数,如sin
, log
, raiseTo:
, squared
, sqrt
等。
方法Number >> printOn:
根据抽象方法 Number >> printOn:base:
实现。(默认基数是10)
测试方法有even
, odd
, positive
和negative
。不出所料,Number
覆盖了isNumber
。更有趣的是,isInfinite
被定义为返回false
。
截断方法包括:floor
, ceiling
, integerPart
, fractionPart
等。
1 + 2.5
>>> 3.5
3.4 * 5
>>> 17.0
8 / 2
>>> 4
10 - 8.3
>>> 1.7
12 = 11
>>> false
12 ~= 11
>>> true
12 > 9
>>> true
12 >= 10
>>> true
12 < 10
>>> false
100@10
>>> 100@10
以下例子在Pharo中非常有效:
1000 factorial / 999 factorial
>>> 1000
注意,实际上是计算了1000 factorial
,在许多其他语言中,这可能很难计算。这是自动强制和精确处理数字的一个很好的例子。
13.16 浮点数
Float
类实现了Number
中关于浮点数的抽象方法。Float
类(即Float的类侧)提供了返回以下常量的方法:e
、infinity
、nan
和pi
。
Float pi
>>> 3.141592653589793
Float infinity
>>> Float infinity
Float infinity isInfinite
>>> true
13.17 分数
Fractions
由分子和分母的实例变量表示,它们应该是Integer
。分数通常使用整数除法构造,而不是构造方法Fraction>>numerator:denominator:
6/8
>>> (3/4)
(6/8) class
>>> Fraction
一个分数乘以一个整数或另外一个分数可以得到一个整数:
6/8 * 4
>>> 3
13.18 整数
Integer
是三个具体整数类实现的抽象父类。除了提供了许多Number
抽象方法的具体实现外,它还添加了一些特定于整数的方法,比如factorial
, atRandom
, isPrime
, gcd:
和许多别的方法。
SmallInteger
的特殊之处在于其实例以紧凑的方式表示——SmallInteger
不是存储为引用,而是直接使用原本用于保存引用的位来表示。对象引用的第一位指示该对象是否为 SmallInteger
。虚拟机为你抽象了这一点,因此在检查对象时你无法直接看到这一点。
类方法 minVal
和maxVal
可以告诉我们SmallInteger
的范围,注意它取决于image的架构,32位为(2 raisedTo: 30) - 1
, 64位为(2 raisedTo: 60) - 1
SmallInteger maxVal = ((2 raisedTo: 30) - 1)
>>> true
SmallInteger minVal = (2 raisedTo: 30) negated
>>> true
当SmallInteger
超出该范围后,根据需要自动转换为LargePositiveInteger
或LargeNegativeInteger
:
(SmallInteger maxVal + 1) class
>>> LargePositiveInteger
(SmallInteger minVal - 1) class
>>> LargeNegativeInteger
在适当的时候,大整数也可以转换加SmallInteger
。
在大多数编程语言中,整数可以用于指定抚今迭代行为。有一个专门的方法timesRepeat:
用于重复求值一个块。我们已经在“语法概略”一章中见过一个类似的例子。
| n |
n := 2.
3 timesRepeat: [ n := n * n ].
n
>>> 256
13.19 字符
Character
是Magnitude
的子类。可打印的字符在Pharo中表示为$<char>
。例如:
$a < $b
>>> true
不可打印的字符可以通过类方法生成。Character class>>value:
接受一个Unicode(或ASCII)整数值作为参数,返回相应的字符。accessing untypeable character
协议包含了很多方便的构造方法,例如backspace
, cr
, escape
, euro
, space
, tab
。
Character space = (Character value: Character space asciiValue)
>>> true
printOn:
方法非常聪明,它知道三种生成字符中的哪一种提供了最合适的表示:
Character value: 1
>>> Character home
Character value: 2
>>> Character value: 2
Character value: 32
>>> Character space
Character value: 97
>>> $a
内置了各种方便的测试方法:isAlphaNumeric
, isCharacter
, isDigit
, isLowercase
, isVowel
等。
要将一个字符转换为仅包含该字符的字符串,可以给该字符发送asString
消息。在这种情况下,asString
和printString
会产生不同的结果:
$a asString
>>> 'a'
$a
>>> $a
$a printString
>>> '$a'
与SmallInteger
一样Character
也是直接量,而不是对象引用。大多数时候你不会注意到这其中的区别,并且可以象其他对象一样使用Character
类的对象。但这意味着,两个相同的字符总是同一相对象:
(Character value: 97) == $a
>>> true
13.20 字符串
String
是一个已索引的只包含Character
的Collection
。
Object
└─ Collection
└─ SequenceableCollection
└─ ArrayedCollection
├─ Array
├─ Text
└─ String
├─ ByteString
└─ Symbol
事实上,String
是抽象的,而Pharo字符串实际上是ByteString
类的实例。
'hello world' class
>>> ByteString
String
的另一个重要子类是 Symbol
。关键区别在于,对于给定的值,Symbol
只有一个实例。(这有时称为唯一实例属性)。相比之下,两个单独构造的 String
,即使包含相同的字符序列,通常也是不同的对象。
'hel','lo' == 'hello'
>>> false
('hel','lo') asSymbol == #hello
>>> true
另一个重要的区别是,String
是可变的,而Symbol
是不可变的。请注意,改变字符串字面量不是一个好主意,因为它们可以在多个方法执行之间共享。[在Pharo10中,字符串已经不允许修改了]
'hello' at: 2 put: $u; yourself
>>> 'hullo'
#hello at: 2 put: $u
>>> Error: symbols can not be modified.
很容易忘记的一点是,字符串其实是Collection
, 所以,它们也可以理解Collection
能够理解的消息:
#hello indexOf: $o
>>> 5
虽然String
不是Magnitude
的子类,但是它也支持通常的比较方法,例如<
, =
等。此外,String>>match:
对于一些基本的全局风格的模式匹配也很有用:
'*or*' match: 'zorro'
>>> true
字符串支持相当多的转换方法。其中许多是其他类的便捷构造方法,如asDate
, asInteger
等。还有许多将字符串转换为另一个字符串的有用的方法,例如capitalized
和translateToLowercase
。
关于字符串和Collection的更多信息,参见章节:Collections
13.21 Booleans
Boolean
类提供了一个有趣的视角,展示了 Pharo 语言的多少部分已被推入类库中。Boolean
是单例类 True
和 False
的抽象超类。
布尔值的大多数行为可以通过思考方法ifTrue:ifalse:
来理解,该方法以两个block作为参数。
4 factorial > 20
ifTrue: [ 'bigger' ]
ifFalse: [ 'smaller' ]
>>> 'bigger'
方法ifTrue:ifFalse
是Boolean
类中的抽象方法。具体子类中的实现都很简单:
它们根据消息的接收者执行正确的块。事实上,这就是OOP的本质:当消息被发送给一个对象时,由对象本身来决定将使用哪个方法进行响应。在这种情况下,True
的实例只执行真分支,而False
的实例只执行假分支。对于True
和False
,所有的Boolean
抽象方法都是这样实现的。例如, 逻辑非(消息 not
)使用相同的方式来实现:
True >> not
"Negation--answer false since the receiver is true."
^ false
False >> not
"Negation--answer true since the receiver is false."
^ true
布尔值提供了一些有用的方便方法,比如ifTrue:
,ifalse:
和ifalse:ifTrue
。逻辑运算有短路与非短路版本。
在下面的第一个例子中,两个布尔表达式都会被求值,因为&
接受一个布尔参数。即使是(1 > 2)
明显为假,仍然会对(3 < 4)
进行求值。
( 1 > 2 ) & ( 3 < 4 )
>>> false "Eager, must evaluate both sides"
在下面的第二和第三个示例中,先对接收者表达式(1 > 2)
进行求值, 而不对作为参数的闭包进行求值。注意,消息and:
需要一个闭包作为参数。在第三个例子中,表达式[ 1 / 0 ]
不会被求值,也不会引发错误,因为and:
只有在接收者为true
的情况下才会对其参数求值。
( 1 > 2 ) and: [ 3 < 4 ]
>>> false
( 1 > 2 ) and: [ 1 / 0 ]
>>> false "参数块肯定不会执行,所以没有引发异常"
or:
也提供类似的行为。当接收者为false
的情况下,才会执行参数块。
试想一下and:
和or:
是如何实现的。检查Boolean
, True
和False
的实现。
13.22 本章总结
如果你想重写
=
,那么你应该同时重写hash
重写
postCopy
以实现正确的对象复制使用
self halt
设置一个断点返回
self subclassResponsibility
使用方法成为一个抽象方法定制对象的字符串表示需要重写
printOn:
重写
initialize
方法以正确地初始化实例Number
方法自动在Floats
,Fraction
和Integer
之间转换Fraction
代表有理数由不是浮点数每一个
Character
都是唯一的实例String
是可变的,而Symbol
不是。但是,注意不要修改字符串字面量!Symbol
是唯一的,String
不是String
和Symbol
是Collection
的子类,因此支持通常的Collection
方法