association list关联列表(简称 alist)用于记录键到值的映射。它是由称为 associations关联项 的 cons 单元构成的列表:每个 cons 单元的 CAR 是 key键,CDR 是 associated value关键值。7
下面是一个关联列表(alist) 示例:键 pine 对应值 cones;键 oak 对应值 acorns;键 maple 对应值 seeds。
((pine . cones) (oak . acorns) (maple . seeds))
关联列表中的键和值都可以是任意 Lisp 对象。
例如下面这个 alist关联列表 里,符号 a 对应数值 1,字符串 "b" 对应 列表 (2 3)(作为该关联列表元素的 CDR):
((a . 1) ("b" 2 3))
有时候,把关联列表设计成将关联值存放在元素的 CDR 的 CAR 位置会更合适。下面是这类关联列表的一个示例:
((rose red) (lily white) (buttercup yellow))
这里我们将 red 视为与 rose 关联的值。这种关联列表的一个优点是,你可以在 CDR 的 CDR 中存储其他相关信息 —— 甚至是其他元素构成的列表。缺点是,你无法使用 rassq(见下文)来查找包含给定值的元素。如果这两点都不重要,那么只要在同一个关联列表中保持风格一致,选择哪种方式只是个人偏好问题。
上面同一个关联列表也可以被理解为:关联值存储在元素的 CDR 中;此时与 rose 关联的值就是列表 (red)。
关联列表常用来记录那些原本可能存放在栈中的信息,因为新的关联项可以很方便地添加到列表头部。 在关联列表中根据指定键搜索关联项时,如果存在多个匹配项,会返回第一个找到的项。
在 Emacs Lisp 中,即使关联列表中的某个元素不是 cons 单元,也 不会 报错。关联列表的搜索函数会直接忽略这类元素。而在其他许多 Lisp 方言中,这种情况会抛出错误。
注意:属性列表在多个方面与关联列表相似。属性列表的行为类似于每个键只能出现一次的关联列表。有关属性列表与关联列表的对比,see 属性列表。
该函数返回 alist 中键为 key 的第一个关联项。
若 testfn 是函数,则使用它来比较 key 与关联列表中的元素,否则使用 equal 进行比较(see 相等性谓词)。
若 testfn 是函数,它会接收两个参数:alist 中某个元素的 CAR 与 key。若经 testfn 检测后,alist 中没有任何关联项的 CAR 与 key 相等,函数返回 nil。示例:
(setq trees '((pine . cones) (oak . acorns) (maple . seeds)))
⇒ ((pine . cones) (oak . acorns) (maple . seeds))
(assoc 'oak trees)
⇒ (oak . acorns)
(cdr (assoc 'oak trees))
⇒ acorns
(assoc 'birch trees)
⇒ nil
下面是另一个示例,其中键和值都不是符号:
(setq needles-per-cluster
'((2 "Austrian Pine" "Red Pine")
(3 "Pitch Pine")
(5 "White Pine")))
(cdr (assoc 3 needles-per-cluster))
⇒ ("Pitch Pine")
(cdr (assoc 2 needles-per-cluster))
⇒ ("Austrian Pine" "Red Pine")
函数 assoc-string 与 assoc 非常相似,区别在于它会忽略字符串之间的某些差异。See 字符与字符串的比较。
该函数返回 alist 中值为 value 的第一个关联项。如果 alist 中没有任何关联项的 CDR 与 value 满足 equal 相等,则返回 nil。
rassoc 与 assoc 类似,区别在于它比较的是每个 alist 关联项的 CDR,而非 CAR。你可以将其看作反向的 assoc,即根据给定的值查找对应的键。
该函数与 assoc 类似,会返回 alist 中键为 key 的第一个关联项,但它使用 eq 进行比较。如果 alist 中没有任何关联项的 CAR 与 key 满足 eq 相等,assq 就返回 nil。这个函数比 assoc 更常用,因为 eq 比 equal 更快,并且大多数关联列表都使用符号作为键。See 相等性谓词。
(setq trees '((pine . cones) (oak . acorns) (maple . seeds)))
⇒ ((pine . cones) (oak . acorns) (maple . seeds))
(assq 'pine trees)
⇒ (pine . cones)
另一方面,在键并非符号的关联列表中,assq 通常并不适用:
(setq leaves
'(("simple leaves" . oak)
("compound leaves" . horsechestnut)))
(assq "simple leaves" leaves)
⇒ Unspecified; might be nil or ("simple leaves" . oak).
(assoc "simple leaves" leaves)
⇒ ("simple leaves" . oak)
该函数与 assq 类似。它通过将 key 与 alist 中的元素进行比较,找到第一个关联(key . value);如果找到,则返回该关联的 value。如果未找到任何关联,函数返回 default。将 key 与 alist 元素进行比较时,使用由 testfn 指定的函数,默认为 eq。
这是一个广义变量(see 广义变量),可通过 setf 用来修改值。当用它来设置值时,若可选参数 remove 为非 nil,则表示当新值与 default 满足 eql 相等时,从 alist 中移除 key 对应的关联。
该函数返回 alist 中值为 value 的第一个关联项。如果 alist 中没有任何关联项的 CDR 与 value 满足 eq 相等,则返回 nil。
rassq 与 assq 类似,区别在于它比较的是每个 alist 关联项的 CDR,而非 CAR。你可以将其看作反向的 assq,即根据给定的值查找对应的键。
示例:
(setq trees '((pine . cones) (oak . acorns) (maple . seeds)))
(rassq 'acorns trees)
⇒ (oak . acorns)
(rassq 'spores trees)
⇒ nil
rassq 函数无法查找存储在元素的 CAR 部分的 CDR 位置的值:
(setq colors '((rose red) (lily white) (buttercup yellow)))
(rassq 'white colors)
⇒ nil
在这种情况下,关联项 (lily white) 的 CDR 并不是符号 white,而是列表 (white)。如果将该关联项写成 点对(dotted pair) 形式,这一点会更加清晰:
(lily white) ≡ (lily . (white))
该函数在 alist 中搜索与 key 匹配的项。对于 alist 的每个元素:
若该元素是原子(atom),则直接将其与 key 比较;
若该元素是 cons 单元,则将其 CAR 与 key 比较。
比较方式为调用 test 函数并传入两个参数:第一个参数是元素本身(原子时)或元素的 CAR(cons 单元时),第二个参数是 key。参数按此顺序传递,目的是当你在包含正则表达式的关联列表中使用 string-match 时,能得到有效的结果(see Regular Expression Searching)。若省略 test 或其值为 nil,则使用 equal 进行比较。
如果关联列表(alist)中的某个元素按此条件与键 key 匹配,那么 assoc-default 会基于该元素返回一个值。
若该元素是一个点对单元(cons),则返回值为该元素的 CDR;
如果不是 cons 单元,返回值为 default, default 默认为 nil。
若关联列表中没有元素匹配 key,assoc-default 返回 nil。
;; 原子元素 vs cons 单元元素的直观对比
;; 原子元素的常见类型:
;; 符号(admin)、字符串("default")、数字(100)、nil 等,都是不可拆分的对象;
(setq mix-alist '(
apple ; 原子(符号)
(banana . 5) ; cons 单元(点对)
"cherry" ; 原子(字符串)
(date . 10) ; cons 单元(点对)
20 ; 原子(数字)
))
;; 匹配原子元素"cherry"
(assoc-default "cherry" mix-alist)
⇒ nil ; 原子匹配成功,返回 default(nil)
;; 匹配 cons 单元的CAR"banana"
(assoc-default 'banana mix-alist)
⇒ 5 ; cons 单元匹配成功,返回 CDR
该函数返回 alist 的二级深拷贝:它会为每个关联项创建一份新副本,这样你就可以修改新关联列表中的关联项,而不会改变原有的关联列表。
(setq needles-per-cluster
'((2 . ("Austrian Pine" "Red Pine"))
(3 . ("Pitch Pine"))
(5 . ("White Pine"))))
⇒
((2 "Austrian Pine" "Red Pine")
(3 "Pitch Pine")
(5 "White Pine"))
(setq copy (copy-alist needles-per-cluster))
⇒
((2 "Austrian Pine" "Red Pine")
(3 "Pitch Pine")
(5 "White Pine"))
(eq needles-per-cluster copy)
⇒ nil
(equal needles-per-cluster copy)
⇒ t
(eq (car needles-per-cluster) (car copy))
⇒ nil
(cdr (car (cdr needles-per-cluster)))
⇒ ("Pitch Pine")
(eq (cdr (car (cdr needles-per-cluster)))
(cdr (car (cdr copy))))
⇒ t
此示例展示了 copy-alist 如何使得修改一份副本的关联关系而不影响另一份成为可能:
(setcdr (assq 3 copy) '("Martian Vacuum Pine"))
(cdr (assq 3 needles-per-cluster))
⇒ ("Pitch Pine")
该函数会从 alist 中删除所有 CAR 与 key 满足 eq 相等 的元素,其效果大致等同于逐个使用 delq 删除这类元素。函数返回缩短后的关联列表,且通常会修改 alist 原有的列表结构。为确保结果正确,应使用 assq-delete-all 的返回值,而非直接查看 alist 保存的原始值。
(setq alist (list '(foo 1) '(bar 2) '(foo 3) '(lose 4)))
⇒ ((foo 1) (bar 2) (foo 3) (lose 4))
(assq-delete-all 'foo alist)
⇒ ((bar 2) (lose 4))
alist
⇒ ((foo 1) (bar 2) (lose 4))
该函数与 assq-delete-all 类似,区别在于它接受一个可选参数 test,这是一个用于比较 alist 中键的谓词函数。若省略该参数或其值为 nil,test 默认为 equal。与 assq-delete-all 一样,该函数通常会修改 alist 原本的列表结构。
该函数从 alist 中删除所有 CDR 与 value 满足 eq 相等 的元素。它返回缩短后的关联列表,并且通常会修改 alist 原有的列表结构。
rassq-delete-all 与 assq-delete-all 类似,区别在于它比较的是 alist 中每个关联项的 CDR,而非 CAR。
为关联列表 alist 中用作键的每个符号创建绑定,名称以点号 ‘.’ 为前缀。在需要访问同一关联列表中的多个项时,这会很有用;通过一个简单的示例就能很好地理解:
(setq colors '((rose . red) (lily . white) (buttercup . yellow)))
(let-alist colors
(if (eq .rose 'red)
.lily))
⇒ white
编译器会在编译期检查 body 部分,且仅会为那些在 body 中出现、符号名首个字符为 ‘.’(点号) 的符号创建绑定。查找这些键(keys)的操作通过 assq 完成,该 assq 调用返回值的 cdr 部分会被赋值为对应绑定的取值。
本功能支持嵌套关联列表:
(setq colors '((rose . red) (lily (belladonna . yellow) (brindisi . pink))))
(let-alist colors
(if (eq .rose 'red)
.lily.belladonna))
⇒ yellow
允许将 let-alist 嵌套使用,但内层 let-alist 中的代码无法访问外层 let-alist 所绑定的变量。
这里 “key(键)” 的用法与 “key sequence(键序列)” 一词无关;它指用于在表中查找条目的值。在本场景下,表就是关联列表(alist),而关联列表中的各个关联项就是条目。