语言:一组值,也可以成为一个值
本节阅读量:第六章把 record 定义为不可变、可变长度的聚合值。它的字段数量写在创建表达式中:
|
|
... 表示零个或多个字段,因此下面三种写法都合法:
|
|
这里的 record 不是有字段名的 struct,也没有方法、继承或更新操作。字段只按从 0 开始的位置编号。
创建 record
求值 (record e0 e1 ... en) 时,语言依次完成:
- 在当前环境中求值
e0; - 再求值
e1,依此类推; - 所有字段都成功求值后,分配一个新 record;
- 把得到的 Value 按原顺序存入字段。
例如:
|
|
先得到 40,再得到 2,最后得到 3,结果显示为:
|
|
左到右顺序现在看似不重要,因为本章还没有可变状态;第八章加入 set! 后,这个顺序会直接影响结果。现在把语义固定下来,后续就不必改写 record。
若某个字段求值失败,后面的字段不会继续求值,也不会创建半成品对象。
空 record 是真正的对象
(record) 没有字段,但它不是数字 0,也不是一个全局共享的 nil。每次求值都创建一个新的零字段 record:
|
|
它们依次得到 1、0 和 0。最后一个结果为零,是因为两个 (record) 分别创建了两个对象。空 record 只是字段数为零的普通 record,因此对象模型不需要额外的“空值例外”。
若程序想把空 record 当作某种数据结构的结束标记,可以先把同一个对象绑定给变量,再传递这份引用。语言本身不把它预设成 Lisp 的唯一空表。
字段保存完整 Value
record 字段不限于整数:
|
|
第一个字段保存另一个 record 的引用,第二个字段保存函数值。外层对象不会把内层对象展开复制,它们形成一张对象图:
|
|
例如可以连续读取:
|
|
外层 index 1 得到内层 record,再从内层 index 0 得到 40。
record?:安全地询问值的种类
record? 先把唯一的子表达式求值一次,再检查结果的种类:
|
|
结果依次是 1、1、0、0。
它不会因收到非 record 而报错,而是返回源语言整数 1 或 0。这也是统一 tagged value 的一个直接用途:生成代码先检查机器字低位是否为 heap-object tag;只有通过后才读取 header 并检查 record kind,不会把整数当地址解引用。
size:读取实际字段数
size 先求值对象表达式,确认结果是 record,再读取 header 中的 payload count:
|
|
结果依次是 0、1、3。
它只接受 record:
|
|
解释器会报告类型错误;编译后的程序会走统一失败出口,以状态 70 结束。size 返回的是普通源语言整数,因此可以继续参与算术和条件:
|
|
结果是 42。
get:按实际长度安全读取
get 的求值规则是:
- 求值第一个子表达式,得到一个 Value;
- 检查它是否为 record;
- 从 record header 取得实际字段数;
- 检查
index < field_count; - 返回对应字段中保存的完整 Value。
例如:
|
|
结果依次是 10、30。
get 的 index 必须直接写成非负整数字面量:
|
|
第一行是合法语法;其余三行分别使用负数、变量和表达式作为 index,都会被 parser 拒绝。
index 不是表达式,不会在运行时求值。把它保留为 AST 和 IR 中的整数元数据,能让本章集中讲动态对象长度,而不必同时引入动态索引计算。
语法合法不等于读取必然成功:
|
|
这里 index 2 是合法的非负字面量,但对象只有两个字段。解释器报告越界错误;生成代码在真正访问内存前检查 header,并以状态 70 结束。它不会从对象后方读出随机机器字。
对非 record 执行 get 同样是动态类型错误:
|
|
record 是不可变的
本章只有创建和观察操作:
|
|
没有 set-field!。一旦创建,字段数量和字段值都不会改变。把 record 绑定给另一个变量,只会复制引用:
|
|
p 与 q 指向同一对象,所以结果是 1。
eq? 比较对象身份
整数仍按数值比较;record 按身份比较。字段序列相同的两个新对象并不相等:
|
|
结果是 0。
包括空 record:
|
|
结果同样是 0。
这种语义不会递归遍历字段,不会因嵌套结构变深而改变成本,也为后续共享、更新和垃圾回收保留稳定的对象身份。
函数仍不属于本章 eq? 支持的比较范围。把函数交给 eq? 会报错;record 字段能够保存函数,并不等于函数获得了相等语义。
record 在条件中为真
延续前文规则,只有整数 0 为假;非零整数、函数和所有 record 都为真。空 record 也是真值:
|
|
结果是 42。
要判断 record 是否为空,应读取长度:
|
|
不要把空 record 与刚创建的 (record) 比较,因为身份比较会得到 0。
解释器的显示形式
mini run 使用与源码相近的格式显示 record:
|
|
显示会递归格式化字段,便于观察对象内容;这不改变 eq? 的身份语义。两个对象可能显示完全相同,仍然互不相等。
为什么不再加入 vector、struct 或 class
可变长度 record 已经足以演示本书当前需要的能力:
- 在堆上创建大小由程序决定的对象;
- 把任意 Value 放进 payload;
- 建立嵌套和共享的对象图;
- 通过 header 保存 kind 与长度;
- 安全地做类型判断和边界检查。
源语言不需要再增加 vector、struct 或 class。后文代码中的 std::vector 只是 C++ 实现容器,用来拥有数量不定的 AST 子树、Value 或 IR operand;它不是源语言新增的集合类型。
下一节会把这些语言规则翻译成机器表示。重点不再是“二字段对象占 24 字节”,而是一条适用于零个、两个或任意多个 payload 的统一布局公式。