5.8 关联列表

association list关联列表(简称 alist)用于记录键到值的映射。它是由称为 associations关联项 的 cons 单元构成的列表:每个 cons 单元的 CARkey键CDRassociated 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))

有时候,把关联列表设计成将关联值存放在元素的 CDRCAR 位置会更合适。下面是这类关联列表的一个示例:

((rose red) (lily white) (buttercup yellow))

这里我们将 red 视为与 rose 关联的值。这种关联列表的一个优点是,你可以在 CDRCDR 中存储其他相关信息 —— 甚至是其他元素构成的列表。缺点是,你无法使用 rassq(见下文)来查找包含给定值的元素。如果这两点都不重要,那么只要在同一个关联列表中保持风格一致,选择哪种方式只是个人偏好问题。

上面同一个关联列表也可以被理解为:关联值存储在元素的 CDR 中;此时与 rose 关联的值就是列表 (red)

关联列表常用来记录那些原本可能存放在栈中的信息,因为新的关联项可以很方便地添加到列表头部。 在关联列表中根据指定键搜索关联项时,如果存在多个匹配项,会返回第一个找到的项。

在 Emacs Lisp 中,即使关联列表中的某个元素不是 cons 单元,也 不会 报错。关联列表的搜索函数会直接忽略这类元素。而在其他许多 Lisp 方言中,这种情况会抛出错误。

注意:属性列表在多个方面与关联列表相似。属性列表的行为类似于每个键只能出现一次的关联列表。有关属性列表与关联列表的对比,see 属性列表

Function: assoc key alist &optional testfn

该函数返回 alist 中键为 key 的第一个关联项。 若 testfn 是函数,则使用它来比较 key 与关联列表中的元素,否则使用 equal 进行比较(see 相等性谓词)。 若 testfn 是函数,它会接收两个参数:alist 中某个元素的 CARkey。若经 testfn 检测后,alist 中没有任何关联项的 CARkey 相等,函数返回 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-stringassoc 非常相似,区别在于它会忽略字符串之间的某些差异。See 字符与字符串的比较

Function: rassoc value alist

该函数返回 alist 中值为 value 的第一个关联项。如果 alist 中没有任何关联项的 CDRvalue 满足 equal 相等,则返回 nil

rassocassoc 类似,区别在于它比较的是每个 alist 关联项的 CDR,而非 CAR。你可以将其看作反向的 assoc,即根据给定的值查找对应的键。

Function: assq key alist

该函数与 assoc 类似,会返回 alist 中键为 key 的第一个关联项,但它使用 eq 进行比较。如果 alist 中没有任何关联项的 CARkey 满足 eq 相等,assq 就返回 nil。这个函数比 assoc 更常用,因为 eqequal 更快,并且大多数关联列表都使用符号作为键。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)
Function: alist-get key alist &optional default remove testfn

该函数与 assq 类似。它通过将 keyalist 中的元素进行比较,找到第一个关联(key . value);如果找到,则返回该关联的 value。如果未找到任何关联,函数返回 default。将 keyalist 元素进行比较时,使用由 testfn 指定的函数,默认为 eq

这是一个广义变量(see 广义变量),可通过 setf 用来修改值。当用它来设置值时,若可选参数 remove 为非 nil,则表示当新值与 default 满足 eql 相等时,从 alist 中移除 key 对应的关联。

Function: rassq value alist

该函数返回 alist 中值为 value 的第一个关联项。如果 alist 中没有任何关联项的 CDRvalue 满足 eq 相等,则返回 nil

rassqassq 类似,区别在于它比较的是每个 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))
Function: assoc-default key alist &optional test default

该函数在 alist 中搜索与 key 匹配的项。对于 alist 的每个元素: 若该元素是原子(atom),则直接将其与 key 比较; 若该元素是 cons 单元,则将其 CARkey 比较。 比较方式为调用 test 函数并传入两个参数:第一个参数是元素本身(原子时)或元素的 CAR(cons 单元时),第二个参数是 key。参数按此顺序传递,目的是当你在包含正则表达式的关联列表中使用 string-match 时,能得到有效的结果(see Regular Expression Searching)。若省略 test 或其值为 nil,则使用 equal 进行比较。

如果关联列表(alist)中的某个元素按此条件与键 key 匹配,那么 assoc-default 会基于该元素返回一个值。 若该元素是一个点对单元(cons),则返回值为该元素的 CDR; 如果不是 cons 单元,返回值为 default, default 默认为 nil

若关联列表中没有元素匹配 keyassoc-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
Function: copy-alist alist

该函数返回 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")
Function: assq-delete-all key alist

该函数会从 alist 中删除所有 CARkey 满足 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))
Function: assoc-delete-all key alist &optional test

该函数与 assq-delete-all 类似,区别在于它接受一个可选参数 test,这是一个用于比较 alist 中键的谓词函数。若省略该参数或其值为 niltest 默认为 equal。与 assq-delete-all 一样,该函数通常会修改 alist 原本的列表结构。

Function: rassq-delete-all value alist

该函数从 alist 中删除所有 CDRvalue 满足 eq 相等 的元素。它返回缩短后的关联列表,并且通常会修改 alist 原有的列表结构。 rassq-delete-allassq-delete-all 类似,区别在于它比较的是 alist 中每个关联项的 CDR,而非 CAR

Macro: let-alist alist body

为关联列表 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 所绑定的变量。


Footnotes

(7)

这里 “key(键)” 的用法与 “key sequence(键序列)” 一词无关;它指用于在表中查找条目的值。在本场景下,表就是关联列表(alist),而关联列表中的各个关联项就是条目。


emacs

Emacs

org-mode

Orgmode

Donations

打赏

Copyright

© 2025 Jasper Hsu

Creative Commons

Creative Commons

Attribute

Attribute

Noncommercial

Noncommercial

Share Alike

Share Alike