Pharo By Example - 第九章 理解消息的语法

标签: pharo ; smalltalk ;


尽管Pharo的消息语法非常简单,但它并不符合常规,可能需要一些时间来适应。本章提供了一些指导,帮助您适应消息发送的语法。如果您已经熟悉了语法,您可以选择跳过本章,稍后再回过头来阅读。Pharo的语法与Smalltalk的语法相近,因此SmallTalk程序员会对Pharo的语法感到熟悉。

9.1 识别消息

在Pharo中,除了第8章中列出的语法元素以外(:= ^ . ; #() {} [:|]),一切都是消息发送。您可以为自己的类定义像+这样的运算符,但是所有的运算符,无论是已有的运算符还是自定义的运算符,都具有相同的优先级。事实上,在Pharo中并没有运算符!它们只是特定类型的消息:一元、二元或关键字消息。此外,您不能更改消息选择器的元数。-(减)始终是二元消息的选择器;永远不可能有一个一元消息叫做-

在Pharo中,消息的发送顺序由消息的类型决定。只有三种消息:一元消息、二元消息和关键字消息。一元消息总是先被发送,然后是二元消息,最后是关键字消息。与大多数语言一样,括号用于改变执行顺序。这些规则使Pharo代码尽可能地易于阅读。在大多数情况下,你都不需要考虑规则。

由于Pharo中的大多数计算都是通过消息传递完成的,因此正确识别消息至关重要。下面的术语对我们会有所帮助:

  • 消息由消息选择器和可选的消息参数组成。

  • 消息被发送给接收者

  • 消息及其接收者的组合统称为消息发送,如图9-1所示。

图9-1 由接收者、方法选择器和一组参数组成的两个消息发送

消息总是发送给接收者,接收者可以是单个的文本、块或者变量,也可以是另一条消息的求值结果。为了帮助您识别消息的接收者和发送顺序,我们给接收者添加了下划线。我们用一个椭圆和数字编号按照顺序将每一个消息发送圈了起来。

图9-2 两个消息:`Color yellow`和`aMorph color: Color yellow`

图9-2表示两个消息发送,Color yellowaMorph color: Color yellow,因此有两个椭圆。首先执行的是Color yellow,因此它的编号为1。有两个接收者:aMorph,它接收的消息是color: ..., 另一个接收者是Color, 消息为yellow。两个接收者都有下划线。

接收者可以是消息的第一个元素,例如100 + 200中的100,或者是Color yellow中的Color。然而,接收者也可以是其他消息的结果。例如,在消息Pen new go: 100中,消息go: 100的接收者是消息Pen new所返回的对象。在所有情况下,消息都被发送到一个被称为接收者的对象,该对象可能是另外一个消息发送的结果。

消息发送 消息类型 结果
Color yellow 一元消息 创建颜色yellow
aPen go: 100 关键字消息 钢笔向前移动100像素
100 + 20 二元消息 100 加上 20
Browser open 一元消息 打开一个新的浏览器
Pen new go: 100 一元消息和关键字消息 先创建一个钢笔的实例,然后向前移动100像素
aPen go: 100 + 20 关键字消息和二元消息 钢笔向前移动 120 像素

上表列出了几个消息发送的示例。您应该会注意到:

  • 并不是所有的消息都有参数,像open这样的一元消息就没有参数;

  • go: 100+ 20这样的单关键字消息和二元消息都有一个参数;

  • 消息有单一的,也有组合的。Color yellow100 + 20是单一消息,消息被发送给了一个对象。而aPen go: 100 + 20是两个消息的组合:+ 20被发送给100go:连同上一个消息的结果又被发送给了对象aPen

  • 接收者可以是一个表达式(如赋值、消息发送或字面量对象),只要它能够返回一个对象。在Pen new go: 100中,消息go: 100被发送到Pen new所产生的对象。

9.2 三种消息

Pharo定义了几个简单的规则来确定消息的发送顺序。这些规则基于3种不同类型的消息之间的区别:

  • 一元消息是发送给对象的单一消息,不包含任何其他信息。例如,在3 factorial中,factorial是一元消息。一元消息可以执行基本的一元运算或任意功能,但发送时始终不带参数。

  • 二元消息由运算符(通常是算术运算符)和执行基本二元操作的消息组成的消息。它们是二元的,因为它们总是只涉及两个对象:接收者和参数对象。例如,在10 + 20中,10是接收者,+是二元消息,20是参数。

  • 关键字消息是由一个或多个关键字组成的消息,每个关键字以冒号(:)结尾并带有一个参数。例如,在anArray at: 1 put: 10中,消息选择器是at:put:。关键字at:接受参数1,关键字put:接受参数10

必须注意的是:

  • 没有不带参数发送的关键字消息。所有不带参数发送的消息都是一元消息。

  • 仅有一个参数的关键字消息和有两个参数的关键字消息之间存在差异-基本上,关键字消息用冒号来标识它的每一个参数。

一元消息

一元消息是不需要任何参数的消息。它们遵循语法模板:Receiver MessageName。选择器只是由一系列不包含冒号()的字符组成,例如factorial, open, class

89 sin
>>> 0.860069405812453

3 sqrt
>>> 1.732050807568877

Float pi
>>> 3.141592653589793

'blop' size
>>> 4

true not
>>> false

Object class
>>> Object class "Object 的类是 Object class (BANG)"

重点 一元消息的语法模板是:接收者 选择器

二元消息

二元消息是只有一个参数,而且其选择器由下列的字符集合中的一个或多个组成的消息:+-*/&=>|<~@。请注意,出于解析原因,不允许使用--

100@100
>>> 100@100 "创建一个Point对象"

3 + 4
>>> 7

10 - 1
>>> 9

4 <= 3
>>> false

(4/3) * 3 == 4
>>> true "== 是一个二元消息,分数是精确的"

(3/4) == (3/4)
>>> false "虽然两个分数相等,但它们是不同的对象"

重点 二元消息遵循的语法模板是: 接收者 选择器 参数

关键字消息

关键字消息是需要一个或多个参数并且其选择器由一个或多个关键字组成的消息,每一个关键字都以冒号(:)结尾。

在下面的示例中,消息between:and:由两个关键字组成:between:and:。完整的消息选择器是between:and:,并将其发送给数字。

2 between: 0 and: 10
>>> true

每一个关键字都有一个参数。因此,r:g:b:是有三个参数的消息,playFileName:at:是有一个参数的消息,at:put:是有两个参数的消息。要创建Color类的实例,可以使用消息r:g:b:,就像Color r: 1 g: 0 b: 0一样,这将会创建红色。请注意,冒号是选择器的一部分。

Color r: 1 g: 0 b: 0
>>> Color red "创建一个新的颜色"

在类似Java的语法中,Pharo的消息发送Color r: 1 g: 0 b: 0将对应于方法调用Color.rgb(1,0,0)

1 to: 10
>>> (1 to: 10) "创建一个区间(Interval)"

| nums |
numbs := Array newFrom: (1 to: 5).
nums at: 1 put: 6.
nums
>>> #(6 2 3 4 5)

重点 关键字消息的语法模型是: 接收者 选择器单词1: 参数1 单词2: 参数2...单词n: 参数n

9.3 消息组合

三种消息有不同的优先级,这使得它们可以以一种优雅的方式进行组合。

  • 一元消息首先被发送,然后是二元消息,最后是关键字消息。

  • 括号中的消息优先级最高。

  • 相同类型的消息从左到右进行求值。

这些规则最终催生了一个非常自然的阅读顺序。现在,如果你想确保你的消息是按照你所期望的顺序发送的,你总是可以使用更多的圆括号,如图9-3所示。在此图中,yellow是一元消息,color:是关键字消息,因此消息Color yellow将会首先发送。然而,将(不必要的)括号放在Color yellow周围只是强调它将首先发送。这一节的其余部分将逐一说明这些要点。

图9-3 一元消息首先发送,所以`Color yellow`选被发送,返回一个颜色对象,再作为消息`aPan color:`的参数进行传递

一元消息 > 二元消息 > 关键字消息

首先发送一元消息,然后发送二元消息,最后发送关键字消息。我们说,一元消息比其他类型的消息拥有更高的优先级。

如下面的示例所示,Pharo的语法规则通常确保消息发送可以以自然的方式阅读:

1000 factorial / 999 factorial
>>> 1000

2 raisedTo: 1 + 3 factorial
>>> 128

遗憾的是,这些规则对于算术消息的发送来说有点过于简单,因此每当您想要施加比二元运算符更高的优先级时,都需要引入圆括号:

1 + 2 * 3
>>> 9

1 + (2 * 3)
>>> 7

我们将专门用一节来讨论算术不一致问题。

下面的示例稍微复杂一些(!),它提供了一个很好的说明,即使是复杂的表达式也可以以自然的方式阅读:

[:aClass | aClass methodDict keys select: [:aMethod |
    (aClass>>aMethod) isAbstract ]] value: Boolean
>>> an IdentitySet(#or: #| #and: #& #ifTrue: #ifTrue:ifFalse:
    #ifFalse: #not #ifFalse:ifTrue:)

这里我们想知道Boolean类的哪些方法是抽象的。我们向某个参数类aclass请求其方法词典的键,并选择该类的抽象方法。然后,我们将参数aClass绑定到具体的值Boolean。在将一元消息isAbstract发送到一个方法之前,我们只需要用括号来发送二元消息>>,它从一个类中选择一个方法。结果告诉我们有哪些方法是必须由Boolean的具体子类TrueFalse实现的。

事实上,我们也可以编写等价但更简单的表达式:Boolean methodDict select: [:each | each isAbstract] thenCollect: [:each | each selector]

示例 在消息aPen color: Color yellow中,有一个一元消息yellow发送给了Color类,一及一个关键字消息color:发送给了aPen。一元消息先被发送,因此Color yellow先被发送,返回值是一个颜色对象,该对象作为消息aPen color: aColor的参数被传递。图9-3以图形方式显示了消息是如何发送的。

示例 在消息aPen go: 100 + 20中,有一个二元消息+ 20和一个关键字消息go:。二元消息先被发送,因此+ 20先被发送给100,返回结果120,然后120作为关键字消息go:的参数再发送给aPen

图9-4 二元消息在关键字消息之前发送

示例 作为练习,我们让您分解消息Pen new go: 100 + 20的执行,它由一个一元消息、一个关键字消息和一个二元消息组成(参见图9-5)。

图9-5 `Pen new go: 100 + 20`的分解

括号优先

括号内的消息比其他消息先发送。

重点 (message) > 一元消息 > 二元消息 > 关键字消息

这是一些例子。

第一个示例显示,当运算顺序符合我们的需要时,不需要括号。也就是说,不管我们写不写括号,结果都是一样的。这里,我们先计算1.5 tan,然后四舍五入,再将结果转换为字符串。

1.5 tan rounded asString = (((1.5 tan) rounded) asString)
>>> true

第二个例子显示阶乘是在加法之前执行的,如果我们想先求3和4的和,我们应该使用圆括号,如下所示。

3 + 4 factorial
>>> 27

(3 + 4) factorial
>>> 5040

类似地,在下面的示例中,我们需要括号来强制lowMajorScaleOn:play之前先发送。

(FMSound lowMajorScaleOn: FMSound clarinet) play
"(1) 将消息 clarinet 发送给 FMSound 类,创建一个单簧管声音。
 (2) 将这个声音作为关键字消息 lowMajorScaleOn: 的参数发送给 FMSound。
 (3) 播放该声音"

示例 消息(65@325 extent: 134@100) center返回左上点为(65,325)且大小为134x100的矩形的中心。下面的示例显示如何分解和发送消息。首先发送括号之间的消息:它包含首先发送的两个二元消息65@325134@100并返回一个坐标点,以及关键字消息extent:,然后发送并返回一个矩形。最后,将一元消息center发送到矩形,并返回一个坐标点。求值不带括号的消息将导致错误,因为对象100不能理解center消息。

(65@325 extent: 134@100) center
"
(1) 65@325                        "二元消息"
>>> aPoint
(2)             134@100           "二元消息"
            >>> anotherPoint
(3) aPoint extent: anotherPoint   "关键字消息"
    >>> aRectangle
(4) aRectangle center             "一元消息"
    >>> 132@375
"

从左到右

现在我们知道了如何处理不同类型或优先级的消息。需要解决的最后一个问题是如何发送具有相同优先级的消息。它们是从左到右进行发送的。请注意,您已经在示例1.5 tan rounded asString中看到了这种行为,其中的所有一元消息从左到右发送,相当于(((1.5 tan) rounded) asString)

重点 当消息类型相同时,执行顺序为从左到右。

示例Pen new down中,所有消息都是一元消息,因此最左侧的消息Pen new最先发送。这会返回一个新创建的笔,第二条消息将发送给该笔,如图9-6所示。

算术的不一致性

消息组合规则很简单。没有数学优先级的概念,因为算术消息和其他消息一样都是消息。因此,当他们被执行时,他们的结果可能和想象的不一致。在这里,我们看到了需要额外括号的常见情况。

3 + 4 * 5
>>> 35

3 + (4 * 5)
>>> 23

1 + 1/3
>>> (2/3)

1 + (1/3)
>>> (4/3)

1/3 + 2/3
>>> (7/9)

(1/3) + (2/3)
>>> 1

20 + 2 * 5中,只有二元消息+*。然而,在Pharo中,操作+*没有特定的优先级。它们只是普通的二元消息,因此*没有比+的优先级高。在这里,最左边的消息+首先发送(1),然后将*发送到结果,如下所示。

图9-7 默认的执行顺序

20 + 2 * 5
"
(1) 20 + 2 >>> 22
(2) 22 * 5 >>> 110
"

如上例所示,该消息发送的结果不是30,而是110。这一结果可能出乎意料,但它直接遵循用于发送消息的规则。这是为了模型的简单性而付出的代价。为了得到正确的结果,我们应该使用括号。当消息括在括号中时,将首先计算它们的值。因此,消息 20 + (2 * 5)返回如下所示的结果。

图9-8 使用括号改变默认的执行顺序

图9-9 使用括号的等效消息

图9-10 使用括号的等效消息

20 + (2 * 5)
"
(1) (2 * 5) >>> 10
(2) 20 + 10 >>> 30
"

重点 在Pharo中,像+*这样的算术运算符没有不同的优先级。+*只是普通的二元消息,因此*没有比+更高的优先级。使用括号可获得所需的结果。

隐含的优先级 带显式括号的等价表达式
aPen color: Color yellow aPen color: (Color yellow)
aPen go: 100 + 20 aPen go: (100 + 20)
aPen penSize: aPen penSize + 2 aPen penSize: ((aPen penSize) + 2)
2 factorial + 4 (2 factorial) + 4

请注意,第一条规则规定了一元消息在二元消息和关键字消息之前发送,因此无需用显式圆括号将它们括起来。上表显示了按照规则编写的消息发送以及假设规则不存在的情况下等效的消息发送写法。两个消息发送的结果相同或返回相同的值。

9.4 识别关键字消息的提示

当初学者需要添加括号时,他们通常会在理解时遇到问题。让我们看看编译器是如何识别关键字消息的。

加括号或不加括号

字符[ ] ( )分隔不同的区域。在这样的区域内,关键字消息是以:结尾且没有被. , ;截断的最长的单词序列。当用[]()把某些单词括起来的时候,这些单词将参与该区域内的局部关键字消息。

在本例中,有两个截然不同的关键字消息:rotatedBy:magnify:smooth:at:put:

aDict
   at: (rotatingForm
          rotateBy: angle
          magnify: 2
          smoothing: 1)
   put: 3

提示 如果您对这些优先级规则有疑问,只想要区分具有相同优先级的两条消息,您可以简单地用括号括起来。

下面的代码不需要括号,因为消息isNil是一元的,因此它在关键字消息ifTrue:之前发送。

(x isNil)
   ifTrue: [...]

下面的代码需要括号,因为消息include:ifTrue:都是关键字消息。

ord := OrderedCollection new.
(ord includes: $a)
   ifTrue: [...]

如果省略掉括号,未知的消息includes:ifTrue:就会被发送给ord!

什么时候使用[]或()

在理解什么时候应该使用方括号而不是圆括号时,您可能也会遇到问题。基本的原则是,当您不知道一个表达式应该计算多少次(可能是零次)时,您应该使用[][expression]将会从expression创建一个块(闭包,也即,对象),根据上下文的不同,可以对其求值任意次(可能为零)。请注意,表达式可以是消息发送、变量、字面量、赋值或块。

因此,ifTrue:ifTrue:ifFalse:的条件分支需要块。遵循相同的原则,whileTrue:消息的接收者和参数都需要使用方括号,因为我们不知道接收者或参数都应该计算多少次。

另一方面,圆括号只影响发送消息的顺序。因此,在(expression)中,该表达式将始终只计算一次。

[ x isReady ] whileTrue: [ y doSomething ] "接收者和参数两者都必须是块"
4 timesRepeat: [ Beeper beep ]             "参数可能会被求值多次,所以必须是一个块"
(x isReady) ifTrue: [ y doSomething ]      "接收者只求值一次,所以不应该是一个块;参数有可能不会被求值,所以必须是一个块"

9.5 表达式序列

表达式(例如,消息发送、赋值等)以句点分隔的数据将按顺序求值。请注意,变量声明和下面的表达式之间没有句号。序列的值是通过对序列中的最后一个表达式求值而获得的值。除最后一个表达式外,所有表达式返回的值在末尾都被忽略。请注意,句点是分隔符,而不是终止符。因此,最后一个表达式的句点是可选的。

| box |
box := 20@30 corner: 60@90.
box containsPoint: 40@50
>>> true

9.6 消息级联

Pharo提供了一种将多条消息发送给同一个对象而不需要将该对象书写多次的简便方法,就是将消息用分号(;)分隔开。这在Pharo中称为消息级联。

从语法上讲,级联表示如下:

aReceiverExpression msg1; msg2; msg3

示例 您可以在不使用级联的情况下在Pharo中编程。只不过你将不得不重复书写消息的接收者。以下代码片段是等效的:

Transcript show: 'Pharo is '.
Transcript show: 'fun '.
Transcript cr.
Transcript
   show: 'Pharo is ';
   show: 'fun ';
   cr

事实上,所有级联消息的接收者是级联中涉及的第一个消息的接收方。请注意,接收级联消息的对象本身可以是消息发送的结果。在下面的示例中,第一个级联的消息是setX:setY:,因为它后面跟一个级联。级联消息setX:setY:的接收者是执行Point new新产生的坐标点,而不是Point本身。随后的消息isZero被发送给相同的接收者。

Point new setX: 25 setY: 35; isZero
>>> false

9.7 本章小结

  • 消息总是被发送到一个名为接收者的对象,它可能是其他消息发送的结果。

  • 一元消息是不需要任何参数的消息。它们的形式是:选择器;

  • 二元消息是涉及两个对象(接收者和另一个对象)的消息,其选择器由以下列表中的一个或多个字符组成:+、-、*、/、|、&、=、>、<、~和@。它们的形式如下:接收者 选择器 参数;

  • 关键字消息是涉及多个对象并且至少包含一个冒号字符(:)的消息。它们的形式是:接收器 选择器关键字1: 参数1 关键字2: 参数2...关键字n: 参数n;

  • 规则一 首先发送一元消息,然后发送二元消息,最后发送关键字消息;

  • 规则二 括号中的消息优先级最高;

  • 规则三 当消息类型相同时,执行顺序为从左到右;

  • 在Pharo中,像+*这样的传统算术运算符具有相同的优先级。+*只是普通的二元消息,因此*并没有比+优先级更高。您必须使用括号才能获得正确的结果。