原文标题:I Can Read C++ and Java But I Can’t Read Smalltalk
原文作者:Wilf LaLonde
原文链接:http://www.eli.sdsu.edu/courses/spring01/cs635/readingSmalltalk.pdf
简介
有很多人告诉我他很熟悉C++或Java,但是却完全读不懂Smalltalk的代码。对于他们来说,Smalltalk简直无法理解!对于这个问题我考虑了很久然后得到的结论是他们是对的。如果我随便挑出一些自己多年以前写的Smalltalk代码,然后假设我只明白Java去阅读的时候,我确信我是无法理解那些代码的。其实要读懂Smalltalk只须了解一些非常简单的概念,当然有些概念也是比较微妙。如果“Johnny读不懂Smalltalk代码”,我有办法。我的办法就是通过实际的例子来帮助新手快速理解Smalltalk的概念。我假设读者了解什么是面向对象变成,对于那些已经掌握Smalltalk的读者也请先假装一会没有学过smalltalk。
如此简单的文法
有些语法是很容易理解的,例如使用双引号来标识注视块;单引号标示字符串,单个字符前面加$(例如:$x标示字符“x”)。除此之外,还有symbol的概念,这是一个特殊的字符串,在整个内存中是唯一的。当源代码被编译的时候,编译器会搜索整个内存,如果发现相同的symbol存在则使用已存在的那个。有理数并不节省内存,但是相对于symbol而言处理速度更快(稍候解释)。
“this is a comment”
‘this is a string’
#’this is a symbol’
#thisIsASymbolToo
对于赋值操作和等于号,差别不是很大。
:= //Means assignment
= //Means equality comparison
== //Means identity comparison
如果你给我分别使用’a’和’b’命名的对象的引用,我可以告诉你他们是否是同样的对象(使用a == b, 命名等价)或看上去相同实质上不同的对象(使用 a = b构造等价)。直观的来说,== 比较两个被引用对象的地址是否相同,而 = 则比较两个对象的整个内容是否相同。
Smalltalk代码中很少用到逗号,因为他们不是Smalltalk文法组成的一部分。这就是为什么Smalltalk的数组是没有逗号的。例如:
#(1 2 3 4 5)
然而,都好是smalltalk的一个操作符。因此你会看到他被用来连接两个字符串,例如:
‘string1’,’string2’
无处不在的关键字
Smalltalk中关键字是无处不在的。他们的存在是为了帮助理解代码而不是增加混淆。要明白这到底是怎样一回事,让我们先来看看C++和Java的语法。例如,
t->rotate(a,v); //For C++
t.rotate(a,v); //For Java
上面的代码向对象t发送消息(注:就是调用类方法)rotate,并指定了参数a和v。为了理解这段代码,读者通常需要继续察看作为参数的变量的申明及其类型。让我们假设其有如下的申明:
Transformation t;
float a;
Vector v;
在Smalltalk中,变量可以引用任何对象,因此在申明变量的时候没有必要指定变量的类型。例如:
| t a v|
由于没有变量类型申明,好的Smalltalk程序员都会使用能暗示变量类型的变量名。因此,上面的申明我们通常如下表示:
| aTransformation angle aVector|
但是后面请允许我继续使用先前的变量名,因为杂志专栏给我可利用的版面很小。让我们进一步消除那些不必要的东西来继续“优化”C++和Java的语法。例如,下面的代码应该仍然很好理解:
t.rotate(a,v); //原文
t rotate(a,v); //有谁需要句点吗?(t和rotate中间的圆点)
t rotate a,v; //有谁需要括号吗?
为了进一步改进上面的语法,我们需要知道参数a 和 v 究竟表示什么。让我们假设整个示例的意思是“围绕端点v进行角度为a的旋转”。那么下一步可能如下:
t.rotate by a around v; //有谁需要逗号吗?
可是如何才能知道上面这个语句中每个单词的意思呢?我们知道,在这个例子当中,t 是一个变量,rotate 是一个类方法的名称,by 是一个分隔符,a 是一个变量,around 又是一个分隔符,最后的 v 是一个变量。为了消除歧义,我们可以假设如下的变换:在所有的分隔符后面添加一个冒号。那么我们就得到下面的句子:
t.rotate by: a around: v; //有谁需要歧义吗?
最终,我们要求所有的分隔符都是方法名称的一部分。也就是说,我们需要的方法的名称是“rotate by: around:”,最后让我们去掉空格,就成了“rotateby:around:”。我们最好将中间的单词开头大写,于是“rotateBy:around:”,因此我们的例子就变成了:
t.rotateBy: a around: v; //这就是Smalltalk
也就是说方法名被分成了好几个部分。幸运的是将这些分开的部分想象成一个整体的名字并不困难。当一个类方法被定义的时候,我们可能会写成下面这样:
self rotateBy: angle around: vector
|result|
result := COMPUTE ANWSER.
^result
在执行的时候,t和self,a和angle,v和vector有一对一的映射关系。需要注意的是^表示返回,相当于return关键字。变量self则相当于this。如果方法的最后没有显式的返回表达式,则却省为^self。也就是说,不写return语句也不会有什么问题。这同时也意味着无论调用者是否需要,类方法都将返回一个对象的引用。
事实上,Smalltalk的语法要求self不能出现在函数名的最前面,如下所示:
rotateBy: angle around: vector
|result|
result:= COMPUTE ANSWER.
^result
Smalltalk的这种关键字语法的优点就是,针对不同的方法可以使用不同的关键字。例如,我们可以像下面这样定义第二个函数:
t rotateAround: vector by: angle
没有必要刻意的去记住参数的顺序,因为关键字暗示了他们的顺序。当然,作为编程者也有可能滥用关键字。例如,如果我们像下面这样定义关键字:
t rotate: angle and: vector
很明显,使用这个函数的人无法通过关键字确定参数的顺序。这是很不好的编程风格。如果只有一个参数的话,则无所谓。但是我们仍然需要只有一个关键字的方法名:
t rotateAroundXBy: angle
t rotateAroundYBy: angle
我更倾向于将关键字理解为一种说明性的参数。但是如果没有参数的时候该怎么办呢?
t makeIdentity: //Can a colon at the end make sense?
如果关键字是说明性参数,而实际的参数却没有的话,那么我们就不是用关键字。因此一个没有关键字的消息(类方法)将写成下面这样:
t makeIdentity //This is Smalltalk
当然,二元操作符(也是类方法,类似C++语言的operator)的定义中也可以使用关键字,但是一元操作符不用(上面的makeIdentity是一元消息,不是一元操作符)。当很多消息同时使用的时候,我们可能得到下面这样一个表达式:
a negative | (b between: c and: d)
ifTrue: [a := c negated]
作为读者,我相信你现在知道消息negative被发送到对象a(无参数),然后true或者false将被返回;between: c and: d被发送到对象b,并返回true或者false。然后对这两个返回结果进行或运算,得到的结果对象被作为ifTrue:[a:= c negated]消息的接受者。这是一个if-then条件表达式,但是并不像C++或者Java那样需要使用特别的语法。在Smalltalk中,这跟其他的类方法调用的语法没有任何差别,只不过消息的接收者是一个布尔对象,而消息的关键字是ifTrue,参数是[a:= c negated](这实际上是一个block对象)。在Smalltalk中你将不会看到a:= -c这样的表达式,因为没有‘-’这样的一元操作符。但是可以写成 –5 这样的表达式,但是这里的负号被作为常量定义的组成部分。
因此,当你看到诸如“-3.5 negated truncated factorial”这样的表达式时,你应该立即认识到这里没有关键字。因此-3.5必须作为negated消息的接收者;而得到的结果3.5则被作为truncated消息的接收者;进一步得到的结果3将作为factorial消息的接收者,最终得到结果6。
当然,这里也存在优先级的问题,通常是从左到右,一次元消息优先级最高,多参数消息的优先级最低。对于编程的人来说这很重要,但是对于读者来说这并不影响其阅读和理解。哦,对了从左到右的优先级实际上意味着:
1+2*3 等于 9
操作符之间没有优先级之分。当然你也可能会看到下面这样的表达式,那是因为编写者知道不这样写可能会给不太熟悉Smalltalk的阅读者造成困扰:
(1+2)*3
尽管这里的括号并不必要。
分号与句号的不同
大多数的非Smalltalk读者可能会认为分号是表达式的终结符,但在Smalltalk中使用的是句号。因此我们不会写下面这样的表达式:
account deposit: 100 dollars;
collection add: transformation;
正确的写法应该是下面这样:
account deposit: 100 dollars.
collection add: transformation.
哦,你可能会很奇怪这里怎么可能有一个dollars消息,其实这没什么特别的。为了使这个表达式合法,Integer类中必须有一个dollars方法的定义(100将被当作Integer对象处理)。虽然在标准的Smalltalk环境中并没有这样的定义,但是我们可以自己添加。Smalltalk的基类可以简单的通过定义新的继承类来扩展。
因此,句号是表达式的终结符,但是在最后一行表达式后面也可以省略(因此你也可以把句号当成表达式之间的分隔符)。但是分号也是合法的,他用在层跌消息语法中(往一个对象中一次发送多个对象的语法)。例如:
| p |
p := Client new.
p name: 'Jack'.
p age: 32.
p address: 'Earth'.
对于上面这样一个表达式我们应该如下编写:
| p |
p := Client new.
p
name: 'Jack';
age: 32;
address: 'Earth'.
或者干脆写成:
| p |
p := Client new
name: 'Jack';
age: 32;
address: 'Earth'.
上面的例子中格式并没太大关系,只要愿意我们甚至可以把所有的句子都写成一行。很关键的分号指出了前一条消息发送到接受对象并使其状态发生变化之后,下一条消息将被继续发到这个接受对象。(而不是发到上一条消息的返回值对象,上条消息的返回值将被抛弃或者忽视)。
在最后一个例子当中,new被发送到类来生成一个实例(作为返回值)。然后“name:’Jack’”被发送到这个实例。第一个分号表示“name:’Jack’”消息返回的结果将被忽略,紧接着“age:32”被发送到先前的实例。第二个分号表示“age:32”返回的结果被忽略,紧接着“address:’Earth’”被发送到先前的实例。最后,“address:’Earth’”返回的结果被保存到变量p。修改接收者属性的类方法通常都返回接收者自身。因此变量p就最终被绑定到了被修改了好几次的新生成的类实例对象上。
我们可以将上面的分号替换成“AND ALSO”就会感觉很容易理解这段代码的意思了。在Smalltalk中类似这样向同一个对象连续发送消息的语法称作层跌消息。分号也可以在子表达式中使用,例如“p := (Client new name: 'Jack'; age: 32; address: 'Earth')”—注意这里的括号。
Get/Set方法使用与实例变量相同的名称
类似name,age,address这样的类实例变量在Smalltalk中全都属于私有变量。但是类方法可以提供对这些变量的访问。在C++中(Java也类似),例如,我们可以定义下面这样的访问类实例变量的方法:
long getAge () {return age;}
void setAge (long newAge) {age = newAge;}
如果你有好几打的类,并且你使用上面这样的编码约定,你将会发现最终你的代码中有一大堆的以get和set打头的类方法。当然,如果你碰巧决定通过去掉这些重复的前缀以便让类方法名短些,你会发现C++的编译器将会无法编译,因为他无法从名称区分变量和函数。但是Java的编译器对于下面这样的代码则没有什么问题。
long age () {return age;}
void age (long newAge) {age = newAge;}
你能区分变量age和消息age吗?你应该可以。当你使用这条消息的时候,你需要加上括号,就像“age()或者age(22)”;当你引用这些变量的时候,则不需要使用括号。同样的类方法在Smalltalk中可以写成下面这样:
age ^age
age: newAge age := newAge
不过,我们通常通过写成两行来让代码更可读:
age
^age
age: newAge
age := newAge
在Smalltalk中没有括号可以帮助你区分变量和消息,但是要区分也不是很难。如果你行的话,应该可以看出下面这句话中哪些是变量,哪些是方法:
age age age: age age + age age
好吧,答案是3;第一个age必然是一个变量,第四个也是(每个关键字后面都必须是一个子表达式;所有的表达式都必须以变量开头),还有第七个(在一个二元操作符后面,也必须是一个子表达式)。用更简单典型的例子应该更容易理解,如下:
name size //name 必然是一个变量
self name //name必然是一个类方法
Collection的广泛使用
序列化抽象无须创建新的class
结论