学习 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有三种消息:
- 一元消息(unary message):不带任何参数的消息,如:'hello' size,它的执行结果只涉及到一个对象。
- 二元消息(binary message):两个对象参与,如:1 + 2。
- 关键字消息(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'"
- 首先 reverse 消息发送给字符串 hello,
- 然后将 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. 创建类
- 每个类都默认有个 new 方法。
- 所有类都是 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》