学习GNU Smalltalk

Table of Contents

1 前言

Smalltalk是完全面向对象的,少量易学的语法结合面向对象思想实现了整套语言,比如没有if语法,而是以ifTrue和ifFalse这两个方法代替。

警告:我非专业Smalltalk程序员。

1.1 许可协议

本作品采用知识共享 署名 4.0 国际 许可协议进行许可。访问 http://creativecommons.org/licenses/by/4.0/ 查看该许可协议。

2 环境

2.1 Smalltalk实现

常见如下:

  • Squeak:由Smalltalk-80小组创建。
  • Pharo:基于Squeak的繁衍版。
  • GNU Smalltalk:GNU Smalltalk只实现了Smalltalk语法解析及提供了一个交互式环境。

2.2 开发环境

Smalltalk本身就集成了一套开发环境:虚拟机(VM)、IDE和镜像(Image),所有对象(Object)都保存在Image里的,开发应用程序就是在Image上不断创建和修改对象,最后把这个Image供给别人用载入到他的VM中使用。

在Smalltalk集成环境中的修改过程是可以所见即所得的,这种编程模式称为Live Coding。现在一些前端开发的编辑器实现了类似功能,以实时展现任何变动。

我只专注在语言本身的美,所以用的GNU Smalltalk,它只包含Smalltalk的语法解析器,以减少集成环境的学习成本。

2.3 使用GNU Smalltalk

下载并安装GNU Smalltalk:http://smalltalk.gnu.org/download

运行gst命令,进入交互式模式:

$  gst
GNU Smalltalk ready

st>

“st>”是提示符,后面可以键入代码,如:

st> 1 + 1
2

按Ctrl+d终止进程。

2.3.1 执行Smalltalk脚本

gst命令加上文件名参数:

$ gst hello.st
'hello world'

一般脚本文件后缀名为.st。

3 Hello World

在gst里键入:

'Hello World!' printNl

执行结果如下:

 st> 'Hello World!' printNl
'Hello World!' ①
'Hello World!' ②

位置①是一个字符串对象调用printNl方法输出的内容。printNl作为“消息”传递给“Hello World!”这个字符串对象,这就是消息传递机制,关于消息传递我会在下章介绍。

位置②是执行的返回值。

这行代码理解为:给“Hello World!”这个字符串对象发送一条消息,让它把自身打印出来。

注意,Smalltalk里字符串是用单引号包围。双引号包围是注释:

st> 'Hello World!' printNl "这里是注释,什么也不做"
'Hello World!'
'Hello World!'

另还可用Transcript的show的方法,给它发送个消息,让它把”Hello World!“给打印出来:

st> Transcript show: 'Hello World!'
Hello World!Transcript

Transcript show:和printNl不同的是,Transcript show:不会输出单引号,并且没有换行。

下节,我将介绍下消息传递机制。

4 消息传递

4.1 类和对象

“类”和“对象”是面向对象里的两个重要术语。

类是一个对象的“模板”,基于这个“模板”来创建“对象”,对象有自己的内部状态——实例变量。

比如字符串“Hi”是一个字符串对象,它用String这个类模板创建。通过调用“class”方法便可获得对象所属的类名:

st> 'Hi' class
String
st> 1 class
SmallInteger
st> 1.0 class
FloatD

方法(Method)是定义用来完成某些事情的函数。比如:1 + 1,在其他大多数语言中,“+”是一个操作符。而在Smalltalk中,“+”是一个方法。不仅把运算符定义为方法,甚至逻辑判断、循环都是通过传递消息来完成的。

4.2 消息传递

对象之间的沟通是通过”消息传递“完成。”消息“(Message)在其他语言中被称为”方法“,如1 + 2意思是:给1这个对象发送消息+,同时它包含参数对象2。

一条消息,由 选择器(Selector)消息参数 组成,消息接收对象被称为 接收者(Receiver) ,如:1 + 2

  • 接收者是1
  • Selector是+
  • 参数是2

Smalltalk有三种消息:

  1. 一元消息(Unary Message):不带任何参数的消息,如:'hello' size,它的执行结果只涉及到一个对象。
  2. 二元消息(Binary Message):两个对象参与,如:1 + 2。
  3. 关键字消息(Keyword Messages):带一个或多个参数、一个或多个选择器。如:
3 bitAt: 1 "带一个参数"
3 bitAt: 1 put: 1 "多个选择器"

当给一个对象发送消息时,Smalltalk会顺着类的继承关系一直向父类寻找,直到找到同名的消息,否则会报错,提示did not understand ××,如:

st> 1 test
Object: 1 error: did not understand #test
MessageNotUnderstood(Exception)>>signal (ExcHandling.st:254)
SmallInteger(Object)>>doesNotUnderstand: #test (SysExcept.st:1448)
UndefinedObject>>executeStatements (a String:1)
nil

4.3 消息的优先级

Smalltalk在发送多个消息时,一定会存在优先级问题。牢记一点:Smalltalk是根据消息来分优先级的,否则会出现习惯性的错误,如:

st> 3 + 2 * 10
50

结果之所以是50是因为Smalltalk并不是按运算符优先级(Smalltalk里没有运算符),“+”和“*”的优先级是相同的,所以这里先发送消息”+“给3,计算结果会产生新的对象,然后再在这个对象上发送”*”消息。

优先级从高到低如下:

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

改变优先级要用括号包围:

st> 3 + (2 * 10)
23

例:(3 + 5 bitAt: 3 put: 1) printNl,执行结果如下:

st> (3 + 5 bitAt: 3 put: 1) printNl
12

这里首先执行:3 + 5,然后执行:bitAt:3 put :1,最后执行:printNl。

4.4 消息链(Message chaining)

表达式为:objectName message1 message2 message3 …

首先message1消息发送给objectName,然后message2发送给 objectName message1 返回的结果,如此循环。

如:

st> 'hello' reverse asUppercase
'OLLEH'

4.5 消息级联(Message Cascading)

用途:将多个消息发送给一个对象。

语法如下:

objectName Message1; Message2

和消息链的区别就在每个消息后面多了一个分号。

例:

st> 'hello' reverse; asUppercase
'HELLO'
  1. 首先reverse消息发送给字符串hello,
  2. 然后将asUppercase消息发送给字符串hello。

所以最后结果是HELLO。

还记得Transcript show:默认不会打印换行符吗,可以这样解决:

st> Transcript show: 'Hello world'; cr
Hello world
Transcript

5 常用类

5.1 变量

创建变量的语法如下:

variableName := object

示例:

st> a := 1
1
st> b := 20
20
st> a * b
20

也可以一次创建多个变量,语法为:

| var1 var2 var3 |

例:

st> | x y z |
st> x := 1
1
st> y := 2
2
st> z := 3
3
st> x * y * z
6

5.2 Smalltalk常用类

5.2.1 数字

st> 10 "正数"
10
st> -10 "负数"
-10
st> 10.0 "浮点数"
10.0
st> 10/3 "分数"
10/3
st> 1 + 1
2
st> 2 + 2
4
st> 3 * 3
9
st> 4 / 4
1
st> 4 // 3 "取模"
1
st> 10 = 10 "判断数字是否相等,记住这里不是赋值"
true
st> 2 = 3
false
st> -3 abs "取-3的绝对值"
3
st> 2 raisedTo: 10 "幂运算"
1024
st> 3 between: 1 and: 10 "判断3是否在1~10范围内"
true
5.2.1.1 进制表示

语法:进制r数字

如:

st> 2r111 "二进制"
7
st> 16rA "十六进制"
10
st> 3r2 "三进制"
2
5.2.1.2 科学计数
st> 1e2
100.0
st> 1e10
1.0e10

5.2.2 字符和字符串

单引号之间的内容为字符串,如'hello world'。

字符前面加$,如$a表示字符a。

5.2.2.1 字符串操作示例
st> 'hello' isString "判断对象是否为字符串"
true
st> 'hello ', 'world' "连接两个字符串"
'hello world'
st> 'hello world' reverse "逆转字符串"
'dlrow olleh'
st> 'hello world' size "返回字符串长度"
11
st> 'hello' = 'hello' "判断字符串是否相等"
true
st> 'hello' = 'hi'
false
st> 'hello' at: 1 "取字符串第一个字符,注意索引不是从0开始的"
$h "返回的是字符"
st> 'hello world' copyFrom: 1 to: 5 "取前5个字符组成新的字符串"
'hello'

5.2.3 数组

5.2.3.1 创建数组

有两种方法可以创建数组。

方法1,使用语“#(item1 item2)”法糖:

st> #(1 2 3)
(1 2 3 )

方法2,使用Array类创建对象:

st> aArray := Array new: 10 "创建包含10个元素的数组"
(nil nil nil nil nil nil nil nil nil nil ) "数组初始值为nil"
5.2.3.2 数组引用

给数组对象传递“at:”消息可按下标引用。注意 Smalltalk数组下标是从1开始索引的,不是0, 否则会报错:

error: Invalid index 0: index out of range

同样字符串对象也是如此:

st> 'abc' at: 0
Object: 'abc' error: Invalid index 0: index out of range

示例:

st> aArray := #(1 2 3)
(1 2 3 )
st> aArray at: 2
2
5.2.3.3 修改数组元素
st> array := Array new: 10
(nil nil nil nil nil nil nil nil nil nil )
st> array at: 10 put: 10
10
st> array
(nil nil nil nil nil nil nil nil nil 10 )

5.2.4 集合(Set)

集合用于创建不重复的元素集。

5.2.4.1 创建集合
st> set := Set new
Set ()
5.2.4.2 示例
st> set add: 'lx'
'lx'
st> set
Set ('lx' )
st> set add: 'www.shellcodes.org'
'www.shellcodes.org'
st> set
Set ('lx' 'www.shellcodes.org' )
st> set add: 'lx' "集合不包含重复元素,所以这里是无效操作"
'lx'
st> set
Set ('lx' 'www.shellcodes.org' )
st> set remove: 'lx' "删除集合里指定元素"
'lx'
st> set
Set ('www.shellcodes.org' )

5.2.5 字典(Dictionary)

5.2.5.1 创建字典
st> dict := Dictionary new
Dictionary (
)
5.2.5.2 示例
st> dict at: 'name' put: 'lx' "添加一个元素"
'lx'
st> dict at: 'website' put: 'www.shellcodes.org'
'www.shellcodes.org'
st> dict at: 'github' put: 'github.com/1u4nx'
'github.com/1u4nx'
st> dict
Dictionary (
	'website'->'www.shellcodes.org'
	'name'->'lx'
	'github'->'github.com/1u4nx'
)
st> dict at: 'name' "按key取值"
'lx'
st> dict keys "获得所有的key"
Set ('github' 'name' 'website' ) "因为key是不重复的,所以返回的Set对象"
st> dict values "获得所有的value"
('www.shellcodes.org' 'lx' 'github.com/1u4nx' )

5.2.6 块(Block)

块对象类似类似匿名函数,可将表达式放在一起。

语法如下:

[:arg1 :arg2 | expression-1. expression-2. expression-3]

或者不带参数:

[expression-1. expression-2. expression-3]

每条表达式用“.”分隔开。

5.2.6.1 示例
st> [:msg | message := 'hi, ', msg . message printNl.] value: 'lx' "通过value:来传递参数"
st> sayHello := [:msg | ('Hello, ', msg) printNl.] "将Block对象赋值"
st> sayHello value: 'lx'
'Hello, lx'
st> [ :a :b :c | (a printNl) . (b printNl) . (c printNl)] value: 1 value: 2 value: 3 "多个参数需要指定多个value:选择器"
1
2
3

5.2.7 条件判断

5.2.7.1 true和false

Smalltalk中真假分别用true和false这两个对象表示真假。

5.2.7.2 条件判断

Smalltalk里没有if语法,条件判断都是通过传递消息完成。

a = b,判断a和b的值是否相等:

st> 'hi' = 'hi'
true
st> 'hi' = 'h1'
false

a == b,判断a和b是否指向同一个对象:

st> value := 10
10
st> a := 1
1
st> b := 2
2
st> c := value
10
st> c == value
true
st> a == a
true
st> a == b
false

a ~= b,判断a和b的值是否不等:

st> 'hi' ~= 'hi'
false
st> 'hi' ~= 'hl'
true

a ~~ b,判断a和b是不是指向的同一个对象:

st> a := 1
1
st> b := 2
2
st> a ~~ b
true
st> a ~~ a
false
5.2.7.3 ifTrue:

对象返回true时。

st> a := 100
100
st> (a = 100) ifTrue: ['a equal 100' printNl]
'a equal 100'
'a equal 100'

等价下面的C代码:

if ( a == 10)
{
  puts("a equal 100");
}
5.2.7.4 ifFalse:

与isTrue:相反。

st> (a ~= 100) ifFalse: [a printNl]
100
100
5.2.7.5 ifTrue:ifFalse:

类似:if … else …

st> test := (n > 10)
false
st> test ifTrue: ['yes' printNl] ifFalse: ['no' printNl]
'no'
'no'

5.2.8 循环

5.2.8.1 whileTrue:

类似其他语言的while语句:

从1加到100:

st> sum := 0
0
st> n := 0
0
st> [sum < 100] whileTrue: [sum := sum +1. n := n + sum]
nil
st> n
5050
5.2.8.2 to:do:
st> 1 to: 10 do: [:n | n printNl]
1
2
3
4
5
6
7
8
9
10
5.2.8.3 to:by:do:

类似to:do:,但可指定步长:

st> 1 to: 10 by: 2 do: [:n | n printNl]
1
3
5
7
9

by:指定为负数时,做递减操作:

st> 10 to: 1 by: -1 do: [:n | n printNl]
10
9
8
7
6
5
4
3
2
1
5.2.8.4 遍历数组
st> array do: [:n | n printNl]
2
4
8
16
32
(2 4 8 16 32 )
5.2.8.5 遍历字典
st> dict := Dictionary new
Dictionary (
)
st> dict at: 'name' put: 'lx'
'lx'
st> dict at: 'website' put: 'www.shellcodes.org'
'www.shellcodes.org'
st> dict do: [:value | value printNl]
'www.shellcodes.org'
'lx'
Dictionary (
	'website'->'www.shellcodes.org'
	'name'->'lx'
)

5.2.9 异常处理

Smalltalk异常处理很简单,调用on:方法即可:

array := Array new: 10.

1 to: 11 do: [ :i |
    [array at: i put: i] on: SystemExceptions.IndexOutOfRange
			 do: [
				 'Index out of Range' printNl
			 ]
].

array printNl

ensure:保证无论是否发生异常最终都会执行指定的代码块。上面代码稍调整一下:

array := Array new: 10.

1 to: 11 do: [ :i |
    [array at: i put: i] ensure: [ 'done.' printNl]
].

array printNl

6 创建和扩展类

6.1 创建类

  1. 每个类都默认有个new方法。
  2. 所有类都是Object的子类。

创建类最简单的方法:

父类 subclass: 类名 [
    方法 [
	...
	^返回一个对象
    ]
]

看得出,Smalltalk中定义新的类也是基于消息传递来完成——通过调用subclass:来继承父类。

”类似其他语言中的“return”关键字,用于指定返回值。Smalltalk的每个方法都有返回值,默认返回的self。也正是因为默认返回self,所以才可以用消息链(参见“消息传递”一节)。

方法命名约定:

Smalltalk对方法的访问没有类似Java等语言的public、private属性,一般来说通过命名来约定。Smalltalk有一些常见的命名约定如下:

  • my或self开头,表示私有。
  • is开头的返回true或false。
  • add:、put:返回插入数据后的新对象。
  • remove:返回删除后的新对象。

例,一个简单的类:

Object subclass: Say [
     hello: msg [
	 ('Hello, ', msg) printNl
     ]
 ]

say := Say new.
say hello: 'lx'. "=> 'Hello, lx'"

上面代码创建了一个Say类,并定义了hello这个 实例方法 。定义 类方法 如下:

父类 subclass: 子类 [
    子类 class >> 方法名: 参数 [
	...
	^返回一个对象
    ]
]

例:

Object subclass: Say [
    Say class >> msg: msg [
	msg printNl
    ]
]

Say msg: 'Hi' "=> 'Hi'"

类方法和实例方法不同的地方在于,类方法不需要创建一个对象就可以直接调用,类似其他语言中的静态方法。

6.1.1 self

可给自身发送消息,例:

Object subclass: UserInfo [
    | name |

    setName: userName [
	name := userName.
    ]

    getName [
	^name
    ]

    + otherUser [
	^(self getName), ' ', (otherUser getName)
    ]
]


user1 := UserInfo new.
user1 setName: 'user1'.
user2 := UserInfo new.
user2 setName: 'user2'.

(user1 + user2) printNl "=> 'user1 user2'"

6.1.2 super

方法的查找过程默认是从自身开始的,如果想从父类开始就用super。经常用在new方法中。

例:

Object subclass: Say [
    Say class >> say: msg [
	msg printNl
    ]

    Say class >> hi [
	self say: 'haha'
    ]
]

Say subclass: SayHello [
    SayHello class >> say: msg [
	('I say: ', msg) printNl
    ]

    SayHello class >> hi [
	super hi
    ]
]

Say hi "=> 'haha'"

6.1.3 默认参数

Object subclass: Say [
    Say class >> msg: m [
	self msg: m punctuation: '.'
    ]

    Say class >> msg: m punctuation: p [
	(m, p) printNl
    ]
]


Say msg: 'hello world'. "=> 'hello world.',没有为punctuation:传递参数"
Say msg: 'hello world' punctuation: '!' "=> 'hello world!'"

6.2 扩展类

可以对现有的类进行扩展:

类名 extend [
    方法 [
	...
	^返回一个对象
    ]
]

例,对SmallInteger进行扩展,增加一个printSelf方法:

SmallInteger extend [
    printSelf [
	self printNl
    ]
]

100 printSelf "=> 100"

7 参考资料

  • 《Computer Programming using GNU Smalltalk》
  • 《Smalltalk by Example》