第三章 语法基础
正如大多数现代Smalltalk方言,Squeak使用一种非常接近Smalltalk-80的语法。这个语法设计的非常符合英语阅读习惯
(Smalltalk includes:Class) ifTrue:[Transcript show: Class superclass]
Squeak语法核心非常简洁,仅仅包含方法声明与消息发送语法。表达式基于少量的原语元素构建。仅仅保护6个关键词,没有任何控制结构语法和声明新类语法。取而代之的是,逻辑控制ifelse等语言结构使用发送消息ifTrue到布尔对象,创建新的子类发送superclass消息给父类。
3.1 语法要素
表达式保护以下的内建块结构:
- 六个保留关键词 pseudo-variables:self super nil true false thisContext
- 字面量常量表达式 数字 字符 字符串 符号表和数组
- 变量声明
- 变量赋值
- 块结构
- 消息发送
我可以看看表3.1中的语法要素使用例子
syntax本地变量 Local 小写
startPoint是一个变量名称或标识符。通常,标识符使用驼峰大小写camelCase
。实例变量的首字符是小写的,表明这个变量是私有作用域
共享变量 Shared 大写
Transcript是一个全局变量,类TranscriptStream的实例。通常以大写字母开头。
消息接受者 Receiver
self是一个关键词代表当前方法所在的对象。我们将这个称为消息接受者,因为通常用来接收消息来执行方法。self称为预定义变量不可以手动赋值
数字类型 Integers
squeak除了十进制42,还提供了其他类型的数字,2r101等
浮点数
squeak提供了十进制的浮点数 2.4e7
字符
字符使用$进行声明 $a表示a
。可以发生消息给字符Character类 来处理 如 space tab等
字符串
单引号用来定义字符串字面量,可以在单引号中使用双号'G"day'
符号
与字符串类似,符号包含一系列的字符,然而不同于字符串,符号字面量是全局唯一的。#hello
是一个符号对象 'Hello'
是一个字符串
编译时数组
使用#()定义编译时数组,包含空格分隔的字面量。其中的参数必须是编译时常量 #(27 #(true false) abc)
是一个三个元素的编译时字面量数组
运行时数组
使用{}定义运行时数组,其中的元素使用句号分隔 {1. 2. 1+2}
顶一个包含三个元素的运行时数组,
注释
使用双引号包围字符串作为注释"hello"
。在smalltalk中双引号不是字符串只是注释说明
本地定义变量
使用||
包含方法中的一个或多个本地变量定义
赋值
使用:=
赋值对象变量。有时可以使用<-代替。因此x := 1
等价于x <- 1
与 x _ 1
。
块语法
使用[]表明块语法。作为块语法或谓词语法。通常代表一个一个方法对象。块语法可以接受形参和本地变量
原语
使用<primitive:...>引入一个虚拟机原语,其中的原语代码将会在原语执行失败时运行。
一元消息
仅仅一个方法名称的消息
二元消息
包含一个参数的方法的消息
关键字消息
包含多个参数的方法的小
方法返回
可以使用^ 返回方法的运算结果
语句结束
可以使用.语法作为语法分隔符。
级联消息
可以发送多个消息到一个对象,使用(;)实现级联消息发送
3.2 预定义变量
在Smalltalk中,包含6个保留关键词或者说是预定义变量
nil,true,false,self,super,thisContext
。
这些预定义变量的值是运行环境决定的,无法进行手动赋值,
其中true,false,nil
是常量值。
self,super,thisContext
是动态变量值
true
和false
是Boolean类的子类True和False的唯一实例对象。 在第八章详细讲解
nil
代表未定义对象。是UndefinedObject类的唯一实例对象。实例变量通常初始化为nil
self
通常表示当前方法所在的对象,也就是方法消息的接受者对象。
super
当前方法所在对象的父级,发送消息给super,方法查找将会从当前对象类的父类开始进行查找。这个在第5章详解
thisContext
代表运行栈的顶帧。也就是说,代表当前运行方法上下文,块上下文。在大多数开发过程中不需要关心当前上下文,但是在实现调试器等开发工具中非常有用,也会用来实现异常处理和继续运行。
3.3 消息发送
在Squeak中包含三类消息
- 一元消息 (Unary message)
通常不需要参数,如1 factorial
发送消息factorial
给数字对象 1
2 .二元消息(Binary message)
通常需要一个参数,如1 + 2
发送消息+
携带一个参数2给数字对象 1
- 关键字消息(Keyword message)
通常可以携带任意数量的参数, 如2 raisedTo: 6 modulo: 10
发送消息包含多个消息选择器raisedTo: modulo:
分别包含参数6 10
到数字对象 2
一元消息选择器通常使用字符数字字符,通常使用小写字母开头
二元消息选择器包含以下的一个或多个字符
+-/\*~<>=@%|&?,
关键词消息选择包含一系列的关键词,每个关键词都使用小写字母开头并以分号结束
一元消息选择器通常有最高的优先级,然后是二元消息选择器,最后是关键词消息选择器
2 raisedTo: 1 + 3 factorial
我们首先发送 factorial到数字对象3,然后发送6到数字对象1,最后我们发送raisedTo: 7到数字对象2.
我们可以使用表达式—>表示这个表达式的计算结果
如果不考虑优先级,我们通常直接计算如下
1 + 2 * 3 —> 9
因为没有优先级,所以先计算得到3然后相乘得到9
然而我们包含的优先级法则如下
1 + (2 * 3) —> 7
消息发送可以使用句号和分号进行组合。
一个句号分隔符可以将一系列表达式的依次一个接一个的运行。
Transcript cr.
Transcript show: 'hello world'.
Transcript cr
上面的语句将会发送cr,show:'hello world'到Transcript。最后再次发送cr。
当多个消息被发送到相同的接受者,这些消息将会按照级联消息运行。接受者只需要被指明一次,一系列的消息按照级联的方式发送给消息接受者
Transcript cr;
show: 'hello world';
cr
上面两个表达式的执行结果将会相同
3.4 消息语法
在Squeak中,一个表达式可以在workspace,debugger,browser等任意的地方运行。
方法的定义可以在系统查看器或者调试器中
编程者通常在给的类的上下文中开发一个方法,一个类的定义通过发送消息给已存在的类,要求创建一个子类,因此不需要使用特定的语法来定义新的类
下面是字符串类String中定义lineCount的方法。
lineCount通常一个方法的定义包含以下结构
- 方法参数,包含方法名称和任意多个参数(lineCount)
- 方法注释,方法体中双引号中的字符串形描述
- 本地变量,使用||声明的本地变量
4.方法体表达式,使用句号分隔的方法体表达式。
可以使用^终止方法运行,返回运行结果。
一个方法没有包含显式返回值的将默认返回self,
这个默认实现链式运算
参数和本地变量通常以小写字母开头,大写字母开头的变量假设为全局变量。例如Character类,仅仅是简单的全局变量代表字符类对象。
3.5 块语法
块语法提供一种机制来定义表达式的运算。一个块语法通常是匿名函数。可以给块语法发送value求值一个块语法。块语法可以使用^显示表明返回值,默认的通常返回一系列表达式最后一个表达式的值,
[1 + 2] value —> 3
块语法作为匿名函数,还可以接受形参,通常使用分号开头作为形参声明。使用垂直线分割形参与块语法
[:x | 1 + x] value: 2 —> 3
[:x :y | x + y] value: 1 value: 2 —> 3
如果块语法包含多余四个参数的,必须使用valueWithArguments:
,并且以数组形式传递参数。
包含多个参数的块语法通常拾遗设计错误的标志
块语法中可以声明本地变量,使用双竖线声明本地变量,通常本地变量在参数之后声明
[:x :y | |z| z := x+y. z] value: 1 value: 2 —> 3
块语法事实上词法作用域,因为我们可以将当前块的包围环境作为变量的作用域。下面的块语法限制了x变量作用范围
|x|
x := 1
[:y | x + y] value:2 —> 3
块语法通常是类BlockContext类的实例对象。也就是说它们仅仅是对象,可以被赋值给变量或者作为参数传递给任何其他对象
3.6 条件与循环控制
smalltal并没有提供任何特殊的语法用来实现控制结构。相反,这些仅仅典型的通过发送以语法块作为参数发送消息给布尔值,数字或者集合,
条件语句通常发送ifTure:,ifFalse: ifTrue:ifFalse:
给布尔值变量。
(17*13>220)
ifTrue:['bigger']
ifFalse:['smaller'] —>'bigger'
循环语句通常典型的发送消息给块,数字或者集合。因为循环语句的跳出条件或许会被反复运行,因此条件语句组织为一个可执行的块语法而不是一个布尔变量值,下面是一个典型的循环
n := 1.
[n < 1000] whileTrue: [ n := n*2].
n —> 1024
whileFlase: 用来退出循环
n := 1.
[n > 1000] whileFalse: [n := n*2].
n —> 1024
timeRepest: 实现固定次数循环
n := 1.
10 timesRepeat: [n := n*2].
n —> 1024
还可以使用to:do
发送给一个数字,这个数字作为循环计数的初始化值。第二个参数作为上限,语法块接受当前计数值,运行求值结果
n := 0 .
1 to: 10 do: [:counter | n:= n + counter ] .
n —> 55
高级迭代器。集合中包含大量的不同的类,这些类通常支持相同的协议。可以用于集合迭代的消息类有do:,collect:,select:,reject:,detect: inject:into:
。这些消息定义了较为高级的迭代器。使用这些迭代器可以简化代码
Interval
可以作为一系列数字集合,通常包含开始与结束位置信息。1 to: 10
代表从1到10的数字集合。因为这个是数字集合,我们可以发送do:
给数字集合,携带一个块语法参数可以用来遍历执行
n := 0.
(1 to: 10) do: [:element|n := n +element ].
n —> 55
可以使用 collect:
构建一个集合,转换元素集合
(1 to: 10) collect:[:each|each * each]
—> #(1 4 9 16 25 36 49 64 81 100)
可以使用select:
和reject:
构建新的集合,每个包含(不)符合一定条件的选择内容。detect:
返回固定集合的第一个元素
'hello there' select: [:char | char isVowel] —> 'eeee'
'hello there' reject: [:char | char isVowel] —> 'hll thr'
'hello there' detect: [:char | char isVowel] —> $e
最终,有些集合在inject:into:
方法中支持fold运算符。
通常我们使用一个种子值然后使用一个表达式累积起来得到结果,通常使用求和和求积
(1 to: 10) inject: 0 into:[:sum :each |sum + each]
—> 55
等价于使用
0+1+2+3+4+5+6+7+8+9+10
更多的有关集合与流的操作可以在第九章与第十章中找到
3.7 原语与程序组织
在smalltalk中,每个变量都是对象,每个变量都可以发送消息。然而,特定的时刻我们可能接触到岩底。特定的对象仅仅通过调用虚拟机的原语才可以得到执行
比如,下面的操作通常实现为原语结构:内存操作(new new:),二进制运算(bitAnd:,bitOr:,bitShift),指针操作和数字运算(+ - < > * / = ==)还有数组访问(at:,at:put:)
原子操作可以调用原子语法实现<primirive:aNumber>。调用原语操作的过程可能包含Smalltalk代码,通常是在原语执行失败的时候运行。
可以查看SmallInteger>>+代码。如果原语执行失败,表达式 + aNumber将会得到执行并返回
+ aNumber
<privmitive: 1>
^ super + aNumber
Squeak3.9后,尖括号语法也会用在方法作为编译指示的注释使用。