例如列表 (rose violet buttercup) 包含三个元素:
‘rose’、‘violet’、‘buttercup’。
在计算机中,‘rose’ 的地址被存放在一段名为 表单元(cons cell) 的内存中
(因为它正是函数 cons 创建的结构)。
该表单元同时保存着第二个表单元的地址;
第二个表单元的 CAR 是原子 ‘violet’,
其地址又与第三个表单元的地址一同保存;
第三个表单元则保存原子 ‘buttercup’ 的地址。
这段描述听起来复杂,实际结构很简单,用图更易理解:
___ ___ ___ ___ ___ ___
|___|___|--> |___|___|--> |___|___|--> nil
| | |
| | |
--> rose --> violet --> buttercup
图中每个方框代表一个存储 Lisp 对象的内存单元,通常存放内存地址。
方框成对出现。每个箭头指向地址对应的内容,可以是原子或另一对地址。
第一个方框存放 ‘rose’ 的地址,箭头指向 ‘rose’;
第二个方框指向下一对方框,其前半部分是 ‘violet’ 的地址,
后半部分是再下一对的地址。最后一个方框指向 nil,表示列表结束。
当使用 setq 等操作将变量设为一个列表时,
变量中存储的是第一个方框的地址。
因此执行表达式:
(setq bouquet '(rose violet buttercup))
creates a situation like this:
bouquet
|
| ___ ___ ___ ___ ___ ___
--> |___|___|--> |___|___|--> |___|___|--> nil
| | |
| | |
--> rose --> violet --> buttercup
在这个例子中,符号 bouquet 保存着第一对方框的地址。
同一个列表也可以用另一种方框表示法:
bouquet
|
| -------------- --------------- ----------------
| | car | cdr | | car | cdr | | car | cdr |
-->| rose | o------->| violet | o------->| butter- | nil |
| | | | | | | cup | |
-------------- --------------- ----------------
(符号的结构不止包含地址对,本身也由地址构成。
实际上,符号 bouquet 包含一组地址单元:
一个指向打印名 ‘bouquet’,
一个指向该符号绑定的函数(如果有),
一个指向列表 (rose violet buttercup) 的第一个地址对,依此类推。
这里只展示符号的第三个地址单元指向列表的首地址对。)
如果将一个符号设为某个列表的 CDR,列表本身不会被修改; 该符号只是保存了列表中更靠后的地址。 (用行话说,CAR 和 CDR 都是 “非破坏性(non-destructive)” 操作。) 因此执行下面表达式:
(setq flowers (cdr bouquet))
结果如下:
bouquet flowers
| |
| ___ ___ | ___ ___ ___ ___
--> | | | --> | | | | | |
|___|___|----> |___|___|--> |___|___|--> nil
| | |
| | |
--> rose --> violet --> buttercup
flowers 的值为 (violet buttercup),
也就是说符号 flowers 保存着某对地址单元的地址:
其前半部分指向 violet,后半部分指向 buttercup。
一对地址单元称为一个 表单元(cons cell) 或 点对(dotted pair)。 关于表单元和点对的更多信息, See Cons Cell and List Types in The GNU Emacs Lisp Reference Manual 以及 Dotted Pair Notation in The GNU Emacs Lisp Reference Manual。
函数 cons 会在一串地址前添加一个新的地址对。
例如执行:
(setq bouquet (cons 'lily bouquet))
结果如下:
bouquet flowers
| |
| ___ ___ ___ ___ | ___ ___ ___ ___
--> | | | | | | --> | | | | | |
|___|___|----> |___|___|----> |___|___|---->|___|___|--> nil
| | | |
| | | |
--> lily --> rose --> violet --> buttercup
但这并不会改变符号 flowers 的值,
执行下面表达式可以验证:
(eq (cdr (cdr bouquet)) flowers)
它会返回真值 t。
在被重新赋值前,flowers 仍然是 (violet buttercup),
即它保存着指向 violet 的那个表单元的地址。
同时,原有所有表单元都没有被改动,依然存在。
因此在 Lisp 中:
取列表的 CDR,只是获取下一个表单元的地址;
取 CAR,只是获取列表第一个元素的地址;
用 cons 添加新元素,只是在列表头部新建一个表单元。
原理仅此而已!Lisp 的底层结构简洁得令人惊叹。
一串表单元中的最后一个地址指向哪里?
它指向空列表,即 nil。
总而言之,当一个 Lisp 变量被赋值时, 它保存的是该变量所指向列表的首地址。