学习 GNU Smalltalk

Table of Contents

1. 前言

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

Smalltalk 也有许多实现,常见的有:

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

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

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

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

2. 安装 GNU Smalltalk

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

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

$  gst
GNU Smalltalk ready

st>

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

st> 1 + 1
2

按 Ctrl+d 终止交互式进程。

执行 gst 进入交互式,或加上代码文件名参数:

$ gst hello.st

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

3. Hello World

"双引号中的是注释"

"单引号中的是字符串"
'Hello World!' printNl

执行结果如下:

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

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

位置②是执行的返回值。

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

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

Transcript show: 'Hello World!' "=> Hello World!Transcript"

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

4. 消息传递

4.1. 类和对象

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

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

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

'Hi' class                      "=> String"
1 class                         "=> SmallInteger "
1.0 class                       "=> FloatD"

4.2. 消息传递

对象之间的沟通是通过“消息传递”来完成的。“消息”(message)在其他支持面向对象编程的语言中被称作为“方法”,如 1 + 2 意思是:给“1”这个对象发送消息“+”,同时它包含参数对象“2”,多数语言把“+”这些作为运算符,而 Smalltalk 把它定义为消息,甚至逻辑判断、循环都是通过传递消息来完成的。

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

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

Smalltalk有三种消息:

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

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

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 是根据消息来分优先级的,否则会出现习惯性的错误,如:

3 + 2 * 10                      "=> 50"

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

优先级从高到低如下:

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

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

3 + (2 * 10)                    "=> 23"

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

(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 返回的结果,如此循环,例如:

'hello' reverse asUppercase     "=> 'OLLEH'"

4.5. 消息级联(message cascading)

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

语法如下:

objectName Message1; Message2

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

例:

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

所以最后结果是 HELLO。

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

Transcript show: 'Hello world'; cr

5. 常用类

5.1. 变量

创建变量的语法如下:

variableName := object

示例:

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

"也可以一次创建多个变量,语法为:"
"| var1 var2 var3 |"
| x y z |
x := 1                          "=> 1"
y := 2                          "=> 2"
z := 3                          "=> 3"
x * y * z                       "=> 6"

5.2. Smalltalk 常用类

5.2.1. 数字

10                              "正数"
-10                             "负数"
10.0                            "浮点数"
10/3                            "分数"

" 基本运算: "
1 + 1                           "=> 2"
3 * 3                           "=> 9"
4 / 4                           "=> 1"
4 // 3                          "取模"

" 判断数字是否相等,记住这里不是赋值 "
10 = 10                         "=> true"
2 = 3                           "=> false"

" 取 -3 的绝对值 "
-3 abs                          "=> 3"

" 幂运算 "
2 raisedTo: 10                  "=> 1024"

" 判断3是否在 1~10 范围内 "
3 between: 1 and: 10            "=> true"

" 进制表示 "
" 语法:进制 r 数字,例如: "
" 二进制 "
2r111                           "=> 7"
" 十六进制 "
16rA                            "=> 10"
" 三进制 "
3r2                             "=> 2"

" 科学计数 "
1e2                             "=> 100.0"
1e10                            "=> 1.0e10"

5.2.2. 字符和字符串

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

" 字符前面加 $,如: "
$a                              "表示字符 a"

" 判断对象是否为字符串 "
'hello' isString                "=> true"

" 连接两个字符串 "
'hello ', 'world'               "=> 'hello world'"

" 逆转字符串 "
'hello world' reverse           "=> 'dlrow olleh'"

" 返回字符串长度 "
'hello world' size              "=> 11"

" 判断字符串是否相等 "
'hello' = 'hello'               "=> true"
'hello' = 'hi'           "=> false"

" 取字符串第一个字符,注意索引不是从 0 开始的,返回的是字符 "
'hello' at: 1                       "=> $h"

" 取前 5 个字符组成新的字符串 "
'hello world' copyFrom: 1 to: 5 "=> 'hello'"

5.2.3. 数组

" 有两种方法可以创建数组。 "
" 方法1,使用语法糖 #(item1 item2): "
#(1 2 3)                        "=> (1 2 3 )"

" 方法2,使用 Array 类创建对象: "
" 创建包含 10 个元素的数组,数组初始值为 nil "
aArray := Array new: 10 "=> (nil nil nil nil nil nil nil nil nil nil )"

" 数组引用:给数组对象传递“at:”消息可按下标引用。 "
" 注意数组下标是从 1 开始索引的,不是 0,否则会报错: "
" error: Invalid index 0: index out of range "
" 同样字符串对象也是如此: "
'abc' at: "报错:Object: 'abc' error: Invalid index 0: index out of range"
" 示例: "
aArray := #(1 2 3)              "=> (1 2 3 )"
aArray at: 2                    "=> 2"

"修改数组元素"
array := Array new: 10    "=> (nil nil nil nil nil nil nil nil nil nil )"
array at: 10 put: 10      "=> 10"
array                   "=> (nil nil nil nil nil nil nil nil nil 10 )"

5.2.4. 集合(set)

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

"创建集合"
set := Set new                  "=> Set ()"

"插入元素:"
set add: 'lx'                   "=> 'lx'"
set add: 'www.shellcodes.org'   "=> 'www.shellcodes.org'"
"集合不包含重复元素,所以这里是无效操作"
set add: 'lx'
set                             "=> Set ('lx' 'www.shellcodes.org' )"

"删除集合里指定元素"
set remove: 'lx'
set                             "=> Set ('www.shellcodes.org' ) "

5.2.5. 字典(dictionary)

"创建字典"
dict := Dictionary new          "=> Dictionary ()"

"添加元素:"
dict at: 'name' put: 'lx'                    "=> 'lx'"
dict at: 'website' put: 'www.shellcodes.org' "=> 'www.shellcodes.org'"
dict at: 'github' put: 'github.com/1u4nx'    "=> 'github.com/1u4nx'"
dict "=> Dictionary ( "
     "         'website'->'www.shellcodes.org' "
     "         'name'->'lx' "
     "         'github'->'github.com/1u4nx' "
     " ) "

"按 key 取值"
dict at: 'name' "'lx'"

"获得所有的 key "
"因为 key 是不重复的,所以返回的 set 对象"
dict keys "=> Set ('github' 'name' 'website' )"

"获得所有的 value "
dict values       "=> ('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]"
"每条表达式用“.”分隔开。"

"通过 value: 来传递参数"
[:msg | message := 'hi, ', msg . message printNl.] value: 'lx'

"将 Block 对象赋值"
sayHello := [:msg | ('Hello, ', msg) printNl.]

sayHello value: 'lx'            "=> 'Hello, lx'"

"多个参数需要指定多个 value: 选择器"
[ :a :b :c | (a printNl) . (b printNl) . (c printNl)] value: 1 value: 2 value: 3
"输出:"
"1"
"2"
"3"

5.2.7. 条件判断

"真假分别用 true 和 false"

"条件判断"
"Smalltalk 里没有 if 语法,条件判断都是通过传递消息完成。"
"a = b,判断 a 和 b 的值是否相等:"
'hi' = 'hi'                     "=> true"
'hi' = 'h1'                     "=> false"

"a == b,判断 a 和 b 是否指向同一个对象:"
value := 10
a := 1
b := 2
c := value
c == value                      "=> true"
a == a                          "=> true"
a == b                          "=> false"

"a ~= b,判断 a 和 b 的值是否不等:"
'hi' ~= 'hi'                    "=> false"
'hi' ~= 'hl'                    "=> true"

"a ~~ b,判断 a 和 b 是否指向的不同的对象:"
a := 1
b := 2
a ~~ b                          "=> true"
a ~~ a                          "=> false"

"ifTrue:,对象返回 true 时,执行 block。"
a := 100
(a = 100) ifTrue: ['a equal 100' printNl] "=> 'a equal 100'"

"ifFalse:,与 ifTrue: 相反。"
(a ~= 100) ifFalse: [a printNl] "=> 100"

"ifTrue:ifFalse:,等同其他语言中的 if ... else ..."
test := (n > 10)                "=> false"
test ifTrue: ['yes' printNl] ifFalse: ['no' printNl] "=> 'no'"

5.2.8. 循环

"whileTrue:,类似其他语言的 while 语句:"
"例,从 1 加到 100:"
sum := 0
n := 0
[sum < 100] whileTrue: [sum := sum +1. n := n + sum]
n                               "=> 5050"

"to:do:"
1 to: 10 do: [:n | n printNl]
"输出:
1
2
3
4
5
6
7
8
9
10
"

"to:by:do:,类似 to:do:,可指定步长:"
1 to: 10 by: 2 do: [:n | n printNl]
"输出:
1
3
5
7
9"

"by: 指定为负数时,做递减操作:"
10 to: 1 by: -1 do: [:n | n printNl]
"输出:
10
9
8
7
6
5
4
3
2
1
"

"遍历数组"
array do: [:n | n printNl]
"输出:
2
4
8
16
32
=> (2 4 8 16 32 )"

"遍历字典"
dict := Dictionary new
dict at: 'name' put: 'lx'       "=> 'lx'"
dict at: 'website' put: 'www.shellcodes.org' "=> 'www.shellcodes.org'"
dict do: [:value | value printNl]
"输出:
'www.shellcodes.org'
'lx'
=> Dictionary (
         'website'->'www.shellcodes.org'
         'name'->'lx'
)"

5.2.9. 异常处理

"异常处理很简单,调用 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                   "=> (1 2 3 4 5 6 7 8 9 10 )"

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

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

array printNl                   "=> (1 2 3 4 5 6 7 8 9 10 )"

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》