企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
本章是选择性阅读的。本章描述了 Common Lisp 里一些更深奥的特性。Common Lisp 像是一个冰山:大部分的功能对于那些永远不需要他们的多数用户是看不见的。你或许永远不需要自己定义包 (Package)或读取宏 (read-macros),但当你需要时,有些例子可以让你参考是很有用的。 [TOC] ## 14.1 类型标识符 (Type Specifiers)[](http://acl.readthedocs.org/en/latest/zhCN/ch14-cn.html#type-specifiers "Permalink to this headline") 类型在 Common Lisp 里不是对象。举例来说,没有对象对应到 `integer` 这个类型。我们像是从 `type-of` 函数里所获得的,以及作为传给像是 `typep` 函数的参数,不是一个类型,而是一个类型标识符 (type specifier)。 一个类型标识符是一个类型的名称。最简单的类型标识符是像是 `integer` 的符号。这些符号形成了 Common Lisp 里的类型层级。在层级的最顶端是类型 `t` ── 所有的对象皆为类型 `t` 。而类型层级不是一棵树。从 `nil` 至顶端有两条路,举例来说:一条从 `atom`,另一条从 `list` 与 `sequence` 。 一个类型实际上只是一个对象集合。这意味著有多少类型就有多少个对象的集合:一个无穷大的数目。我们可以用原子的类型标识符 (atomic type specifiers)来表示某些集合:比如 `integer` 表示所有整数集合。但我们也可以建构一个复合类型标识符 (compound type specifiers)来参照到任何对象的集合。 举例来说,如果 `a` 与 `b` 是两个类型标识符,则 `(or a b)` 表示分别由 `a` 与 `b` 类型所表示的联集 (union)。也就是说,一个类型`(or a b)` 的对象是类型 `a` 或 类型 `b` 。 如果 `circular?` 是一个对于 `cdr` 为环状的列表返回真的函数,则你可以使用适当的序列集合来表示: [[1]](http://acl.readthedocs.org/en/latest/zhCN/ch14-cn.html#id4) ~~~ (or vector (and list (not (satisfies circular?)))) ~~~ 某些原子的类型标识符也可以出现在复合类型标识符。要表示介于 1 至 100 的整数(包含),我们可以用: ~~~ (integer 1 100) ~~~ 这样的类型标识符用来表示一个有限的类型 (finite type)。 在一个复合类型标识符里,你可以通过在一个参数的位置使用 `*` 来留下某些未指定的信息。所以 ~~~ (simple-array fixnum (* *)) ~~~ 描述了指定给 `fixnum` 使用的二维简单数组 (simple array)集合,而 ~~~ (simple-array fixnum *) ~~~ 描述了指定给 `finxnum` 使用的简单数组集合 (前者的超类型 「supertype」)。尾随的星号可以省略,所以上个例子可以写为: ~~~ (simple-array fixnum) ~~~ 若一个复合类型标识符没有传入参数,你可以使用一个原子。所以 `simple-array` 描述了所有简单数组的集合。 如果有某些复合类型标识符你想重复使用,你可以使用 `deftype` 定义一个缩写。这个宏与 `defmacro` 相似,但会展开成一个类型标识符,而不是一个表达式。通过表达 ~~~ (deftype proseq () '(or vector (and list (not (satisfies circular?))))) ~~~ 我们定义了 `proseq` 作为一个新的原子类型标识符: ~~~ > (typep #(1 2) 'proseq) T ~~~ 如果你定义一个接受参数的类型标识符,参数会被视为 Lisp 形式(即没有被求值),与 `defmacro` 一样。所以 ~~~ (deftype multiple-of (n) `(and integer (satisfies (lambda (x) (zerop (mod x ,n)))))) ~~~ (译注: 注意上面代码是使用反引号 ````` ) 定义了 (multiple-of n) 当成所有 `n` 的倍数的标识符: ~~~ > (type 12 '(multiple-of 4)) T ~~~ 类型标识符会被直译 (interpreted),因此很慢,所以通常你最好定义一个函数来处理这类的测试。 ## 14.2 二进制流 (Binary Streams)[](http://acl.readthedocs.org/en/latest/zhCN/ch14-cn.html#binary-streams "Permalink to this headline") 第 7 章曾提及的流有二进制流 (binary streams)以及字符流 (character streams)。一个二进制流是一个整数的来源及/或终点,而不是字符。你通过指定一个整数的子类型来创建一个二进制流 ── 当你打开流时,通常是用 `unsigned-byte` ── 来作为 `:element-type` 的参数。 关于二进制流的 I/O 函数仅有两个, `read-byte` 以及 `write-byte` 。所以下面是如何定义复制一个文件的函数: ~~~ (defun copy-file (from to) (with-open-file (in from :direction :input :element-type 'unsigned-byte) (with-open-file (out to :direction :output :element-type 'unsigned-byte) (do ((i (read-byte in nil -1) (read-byte in nil -1))) ((minusp i)) (declare (fixnum i)) (write-byte i out))))) ~~~ 仅通过指定 `unsigned-byte` 给 `:element-type` ,你让操作系统选择一个字节 (byte)的长度。举例来说,如果你明确地想要读写 7 比特的整数,你可以使用: ~~~ (unsigned-byte 7) ~~~ 来传给 `:element-type` 。 ## 14.3 读取宏 (Read-Macros)[](http://acl.readthedocs.org/en/latest/zhCN/ch14-cn.html#read-macros "Permalink to this headline") 7.5 节介绍过宏字符 (macro character)的概念,一个对于 `read` 有特别意义的字符。每一个这样的字符,都有一个相关联的函数,这函数告诉 `read` 当遇到这个字符时该怎么处理。你可以变更某个已存在宏字符所相关联的函数,或是自己定义新的宏字符。 函数 `set-macro-character` 提供了一种方式来定义读取宏 (read-macros)。它接受一个字符及一个函数,因此当 `read` 碰到该字符时,它返回调用传入函数后的结果。 Lisp 中最古老的读取宏之一是 `'` ,即 `quote` 。我们可以定义成: ~~~ (set-macro-character #\' #'(lambda (stream char) (list (quote quote) (read stream t nil t)))) ~~~ 当 `read` 在一个普通的语境下遇到 `'` 时,它会返回在当前流和字符上调用这个函数的结果。(这个函数忽略了第二个参数,第二个参数永远是引用字符。)所以当 `read` 看到 `'a` 时,会返回 `(quote a)` 。 译注: `read` 函数接受的参数 `(read &optional stream eof-error eof-value recursive)` 现在我们明白了 `read` 最后一个参数的用途。它表示无论 `read` 调用是否在另一个 `read` 里。传给 `read` 的参数在几乎所有的读取宏里皆相同:传入参数有流 (stream);接著是第二个参数, `t` ,说明了 `read` 若读入的东西是 end-of-file 时,应不应该报错;第三个参数说明了不报错时要返回什么,因此在这里也就不重要了;而第四个参数 `t` 说明了这个 `read` 调用是递归的。 (译注:困惑的话可以看看 [read 的定义](https://gist.github.com/3467235) ) 你可以(通过使用 `make-dispatch-macro-character` )来定义你自己的派发宏字符(dispatching macro character),但由于 `#`已经是一个宏字符,所以你也可以直接使用。六个 `#` 打头的组合特别保留给你使用: `#!` 、 `#?` 、 `##[` 、 `##]` 、 `#{` 、 `#}` 。 你可以通过调用 `set-dispatch-macro-character` 定义新的派发宏字符组合,与 `set-macro-character` 类似,除了它接受两个字符参数外。下面的代码定义了 `#?` 作为返回一个整数列表的读取宏。 ~~~ (set-dispatch-macro-character #\# #\? #'(lambda (stream char1 char2) (list 'quote (let ((lst nil)) (dotimes (i (+ (read stream t nil t) 1)) (push i lst)) (nreverse lst))))) ~~~ 现在 `#?n` 会被读取成一个含有整数 `0` 至 `n` 的列表。举例来说: ~~~ > #?7 (1 2 3 4 5 6 7) ~~~ 除了简单的宏字符,最常定义的宏字符是列表分隔符 (list delimiters)。另一个保留给用户的字符组是 `#{` 。以下我们定义了一种更复杂的左括号: ~~~ (set-macro-character #\} (get-macro-character #\))) (set-dispatch-macro-character #\# #\{ #'(lambda (stream char1 char2) (let ((accum nil) (pair (read-delimited-list #\} stream t))) (do ((i (car pair) (+ i 1))) ((> i (cadr pair)) (list 'quote (nreverse accum))) (push i accum))))) ~~~ 这定义了一个这样形式 `#{x y}` 的表达式,使得这样的表达式被读取为所有介于 `x` 与 `y` 之间的整数列表,包含 `x` 与 `y` : ~~~ > #{2 7} (2 3 4 4 5 6 7) ~~~ 函数 `read-delimited-list` 正是为了这样的读取宏而生的。它的第一个参数是被视为列表结束的字符。为了使 `}` 被识别为分隔符,必须先给它这个角色,所以程序在开始的地方调用了 `set-macro-character` 。 如果你想要在定义一个读取宏的文件里使用该读取宏,则读取宏的定义应要包在一个 `eval-when` 表达式里,来确保它在编译期会被求值。不然它的定义会被编译,但不会被求值,直到编译文件被载入时才会被求值。 ## 14.4 包 (Packages)[](http://acl.readthedocs.org/en/latest/zhCN/ch14-cn.html#packages "Permalink to this headline") 一个包是一个将名字映对到符号的 Lisp 对象。当前的包总是存在全局变量 `*package*` 里。当 Common Lisp 启动时,当前的包会是`*common-lisp-user*` ,通常称为用户包 (user package)。函数 `package-name` 返回包的名字,而 `find-package` 返回一个给定名称的包: ~~~ > (package-name *package*) "COMMON-LISP-USER" > (find-package "COMMON-LISP-USER") #<Package "COMMON-LISP-USER" 4CD15E> ~~~ 通常一个符号在读入时就被 interned 至当前的包里面了。函数 `symbol-package` 接受一个符号并返回该符号被 interned 的包。 ~~~ (symbol-package 'sym) #<Package "COMMON-LISP-USER" 4CD15E> ~~~ 有趣的是,这个表达式返回它该返回的值,因为表达式在可以被求值前必须先被读入,而读取这个表达式导致 `sym` 被 interned。为了之后的用途,让我们给 `sym` 一个值: ~~~ > (setf sym 99) 99 ~~~ 现在我们可以创建及切换至一个新的包: ~~~ > (setf *package* (make-package 'mine :use '(common-lisp))) #<Package "MINE" 63390E> ~~~ 现在应该会听到诡异的背景音乐,因为我们来到一个不一样的世界了: 在这里 `sym` 不再是本来的 `sym` 了。 ~~~ MINE> sym Error: SYM has no value ~~~ 为什么会这样?因为上面我们设为 99 的 `sym` 与 `mine` 里的 `sym` 是两个不同的符号。 [[2]](http://acl.readthedocs.org/en/latest/zhCN/ch14-cn.html#id5) 要在用户包之外参照到原来的 `sym` ,我们必须把包的名字加上两个冒号作为前缀: ~~~ MINE> common-lisp-user::sym 99 ~~~ 所以有着相同打印名称的不同符号能够在不同的包内共存。可以有一个 `sym` 在 `common-lisp-user` 包,而另一个 `sym` 在 `mine` 包,而他们会是不一样的符号。这就是包存在的意义。如果你在分开的包内写你的程序,你大可放心选择函数与变量的名字,而不用担心某人使用了同样的名字。即便是他们使用了同样的名字,也不会是相同的符号。 包也提供了信息隐藏的手段。程序应通过函数与变量的名字来参照它们。如果你不让一个名字在你的包之外可见的话,那么另一个包中的代码就无法使用或者修改这个名字所参照的对象。 通常使用两个冒号作为包的前缀也是很差的风格。这么做你就违反了包本应提供的模块性。如果你不得不使用一个双冒号来参照到一个符号,这是因为某人根本不想让你用。 通常我们应该只参照被输出 ( *exported* )的符号。如果我们回到用户包里,并输出一个被 interned 的符号, ~~~ MINE> (in-package common-lisp-user) #<Package "COMMON-LISP-USER" 4CD15E> > (export 'bar) T > (setf bar 5) 5 ~~~ 我们使这个符号对于其它的包是可视的。现在当我们回到 `mine` ,我们可以仅使用单冒号来参照到 `bar` ,因为他是一个公开可用的名字: ~~~ > (in-package mine) #<Package "MINE" 63390E> MINE> common-lisp-user:bar 5 ~~~ 通过把 `bar` 输入 ( `import` )至 `mine` 包,我们就能进一步让 `mine` 和 `user` 包可以共享 `bar` 这个符号: ~~~ MINE> (import 'common-lisp-user:bar) T MINE> bar 5 ~~~ 在输入 `bar` 之后,我们根本不需要用任何包的限定符 (package qualifier),就能参照它了。这两个包现在共享了同样的符号;不可能会有一个独立的 `mine:bar` 了。 要是已经有一个了怎么办?在这种情况下, `import` 调用会产生一个错误,如下面我们试著输入 `sym` 时便知: ~~~ MINE> (import 'common-lisp-user::sym) Error: SYM is already present in MINE. ~~~ 在此之前,当我们试着在 `mine` 包里对 `sym` 进行了一次不成功的求值,我们使 `sym` 被 interned 至 `mine` 包里。而因为它没有值,所以产生了一个错误,但输入符号名的后果就是使这个符号被 intern 进这个包。所以现在当我们试著输入 `sym` 至 `mine` 包里,已经有一个相同名称的符号了。 另一个方法来获得别的包内符号的存取权是使用( `use` )它: ~~~ MINE> (use-package 'common-lisp-user) T ~~~ 现在所有由用户包 (译注: common-lisp-user 包)所输出的符号,可以不需要使用任何限定符在 `mine` 包里使用。(如果 `sym` 已经被用户包输出了,这个调用也会产生一个错误。) 含有自带操作符及变量名字的包叫做 `common-lisp` 。由于我们将这个包的名字在创建 `mine` 包时作为 `make-package` 的 `:use` 参数,所有的 Common Lisp 自带的名字在 `mine` 里都是可视的: ~~~ MINE> #'cons #<Compiled-Function CONS 462A3E> ~~~ 在编译后的代码中, 通常不会像这样在顶层进行包的操作。更常见的是包的调用会包含在源文件里。通常,只要把 `in-package` 和`defpackage` 放在源文件的开头就可以了,正如 137 页所示。 这种由包所提供的模块性实际上有点奇怪。我们不是对象的模块 (modules),而是名字的模块。 每一个使用了 `common-lisp` 的包,都可以存取 `cons` ,因为 `common-lisp` 包里有一个叫这个名字的函数。但这会导致一个名字为`cons` 的变量也会在每个使用了 `common-lisp` 包里是可视的。如果包使你困惑,这就是主要的原因;因为包不是基于对象而是基于名字。 ## 14.5 Loop 宏 (The Loop Facility)[](http://acl.readthedocs.org/en/latest/zhCN/ch14-cn.html#loop-the-loop-facility "Permalink to this headline") `loop` 宏最初是设计来帮助无经验的 Lisp 用户来写出迭代的代码。与其撰写 Lisp 代码,你用一种更接近英语的形式来表达你的程序,然后这个形式被翻译成 Lisp。不幸的是, `loop` 比原先设计者预期的更接近英语:你可以在简单的情况下使用它,而不需了解它是如何工作的,但想在抽象层面上理解它几乎是不可能的。 如果你是曾经计划某天要理解 `loop` 怎么工作的许多 Lisp 程序员之一,有一些好消息与坏消息。好消息是你并不孤单:几乎没有人理解它。坏消息是你永远不会理解它,因为 ANSI 标准实际上并没有给出它行为的正式规范。 这个宏唯一的实际定义是它的实现方式,而唯一可以理解它(如果有人可以理解的话)的方法是通过实例。ANSI 标准讨论 `loop` 的章节大部分由例子组成,而我们将会使用同样的方式来介绍相关的基础概念。 第一个关于 `loop` 宏我们要注意到的是语法 ( *syntax* )。一个 `loop` 表达式不是包含子表达式而是子句 (*clauses*)。這些子句不是由括号分隔出来;而是每种都有一个不同的语法。在这个方面上, `loop` 与传统的 Algol-like 语言相似。但其它 `loop` 独特的特性,使得它与 Algol 不同,也就是在 `loop` 宏里调换子句的顺序与会发生的事情没有太大的关联。 一个 `loop` 表达式的求值分为三个阶段,而一个给定的子句可以替多于一个的阶段贡献代码。这些阶段如下: 1. *序幕* (*Prologue*)。 被求值一次来做为迭代过程的序幕。包括了将变量设至它们的初始值。 2. *主体* (*Body*) 每一次迭代时都会被求值。 3. *闭幕* (*Epilogue*) 当迭代结束时被求值。决定了 `loop` 表达式的返回值(可能返回多个值)。 我们会看几个 `loop` 子句的例子,并考虑何种代码会贡献至何个阶段。 举例来说,最简单的 `loop` 表达式,我们可能会看到像是下列的代码: ~~~ > (loop for x from 0 to 9 do (princ x)) 0123456789 NIL ~~~ 这个 `loop` 表达式印出从 `0` 至 `9` 的整数,并返回 `nil` 。第一个子句, `for x from 0 to 9` 贡献代码至前两个阶段,导致 `x` 在序幕中被设为 `0` ,在主体开头与 `9` 来做比较,在主体结尾被递增。第二个子句, `do (princ x)` 贡献代码给主体。 一个更通用的 `for` 子句说明了起始与更新的形式 (initial and update form)。停止迭代可以被像是 `while` 或 `until` 子句来控制。 ~~~ > (loop for x = 8 then (/ x 2) until (< x 1) do (princ x)) 8421 NIL ~~~ 你可以使用 `and` 来创建复合的 `for` 子句,同时初始及更新两个变量: ~~~ > (loop for x from 1 to 4 and y from 1 to 4 do (princ (list x y))) (1 1)(2 2)(3 3)(4 4) NIL ~~~ 要不然有多重 `for` 子句时,变量会被循序更新。 另一件在迭代代码通常会做的事是累积某种值。举例来说: ~~~ > (loop for x in '(1 2 3 4) collect (1+ x)) (2 3 4 5) ~~~ 在 `for` 子句使用 `in` 而不是 `from` ,导致变量被设为一个列表的后续元素,而不是连续的整数。 在这个情况里, `collect` 子句贡献代码至三个阶段。在序幕,一個匿名累加器 (anonymous accumulator)設為 `nil` ;在主体裡,`(1+ x)` 被累加至這個累加器,而在闭幕时返回累加器的值。 这是返回一个特定值的第一个例子。有用来明确指定返回值的子句,但没有这些子句时,一个 `collect` 子句决定了返回值。所以我们在这里所做的其实是重复了 `mapcar` 。 `loop` 最常见的用途大概是蒐集调用一个函数数次的结果: ~~~ > (loop for x from 1 to 5 collect (random 10)) (3 8 6 5 0) ~~~ 这里我们获得了一个含五个随机数的列表。这跟我们定义过的 `map-int` 情况类似 (105 页「译注: 6.4 小节。」)。如果我们有了 `loop`,为什么还需要 `map-int` ?另一个人也可以说,如果我们有了 `map-int` ,为什么还需要 `loop` ? 一个 `collect` 子句也可以累积值到一个有名字的变量上。下面的函数接受一个数字的列表并返回偶数与奇数列表: ~~~ (defun even/odd (ns) (loop for n in ns if (evenp n) collect n into evens else collect n into odds finally (return (values evens odds)))) ~~~ 一个 `finally` 子句贡献代码至闭幕。在这个情况它指定了返回值。 一个 `sum` 子句和一个 `collect` 子句类似,但 `sum` 子句累积一个数字,而不是一个列表。要获得 `1` 至 `n` 的和,我们可以写: ~~~ (defun sum (n) (loop for x from 1 to n sum x)) ~~~ `loop` 更进一步的细节在附录 D 讨论,从 325 页开始。举个例子,图 14.1 包含了先前章节的两个迭代函数,而图 14.2 演示了将同样的函数翻译成 `loop` 。 ~~~ (defun most (fn lst) (if (null lst) (values nil nil) (let* ((wins (car lst)) (max (funcall fn wins))) (dolist (obj (cdr lst)) (let ((score (funcall fn obj))) (when (> score max) (setf wins obj max score)))) (values wins max)))) (defun num-year (n) (if (< n 0) (do* ((y (- yzero 1) (- y 1)) (d (- (year-days y)) (- d (year-days y)))) ((<= d n) (values y (- n d)))) (do* ((y yzero (+ y 1)) (prev 0 d) (d (year-days y) (+ d (year-days y)))) ((> d n) (values y (- n prev)))))) ~~~ **图 14.1 不使用 loop 的迭代函数** ~~~ (defun most (fn lst) (if (null lst) (values nil nil) (loop with wins = (car lst) with max = (funcall fn wins) for obj in (cdr lst) for score = (funcall fn obj) when (> score max) (do (setf wins obj max score) finally (return (values wins max)))))) (defun num-year (n) (if (< n 0) (loop for y downfrom (- yzero 1) until (<= d n) sum (- (year-days y)) into d finally (return (values (+ y 1) (- n d)))) (loop with prev = 0 for y from yzero until (> d n) do (setf prev d) sum (year-days y) into d finally (return (values (- y 1) (- n prev)))))) ~~~ **图 14.2 使用 loop 的迭代函数** 一个 `loop` 的子句可以参照到由另一个子句所设置的变量。举例来说,在 `even/odd` 的定义里面, `finally` 子句参照到由两个`collect` 子句所创建的变量。这些变量之间的关系,是 `loop` 定义最含糊不清的地方。考虑下列两个表达式: ~~~ (loop for y = 0 then z for x from 1 to 5 sum 1 into z finally (return y z)) (loop for x from 1 to 5 for y = 0 then z sum 1 into z finally (return y z)) ~~~ 它们看起来够简单 ── 每一个有四个子句。但它们返回同样的值吗?它们返回的值多少?你若试着在标准中想找答案将徒劳无功。每一个 `loop` 子句本身是够简单的。但它们组合起来的方式是极为复杂的 ── 而最终,甚至标准里也没有明确定义。 由于这类原因,使用 `loop` 是不推荐的。推荐 `loop` 的理由,你最多可以说,在像是图 14.2 这般经典的例子中, `loop` 让代码看起来更容易理解。 ## 14.6 状况 (Conditions)[](http://acl.readthedocs.org/en/latest/zhCN/ch14-cn.html#conditions "Permalink to this headline") 在 Common Lisp 里,状况 (condition)包括了错误以及其它可能在执行期发生的情况。当一个状况被捕捉时 (signalled),相应的处理程序 (handler)会被调用。处理错误状况的缺省处理程序通常会调用一个中断循环 (break-loop)。但 Common Lisp 提供了多样的操作符来捕捉及处理错误。要覆写缺省的处理程序,甚至是自己写一个新的处理程序也是有可能的。 多数的程序员不会直接处理状况。然而有许多更抽象的操作符使用了状况,而要了解这些操作符,知道背后的原理是很有用的。 Common lisp 有数个操作符用来捕捉错误。最基本的是 `error` 。一个调用它的方法是给入你会给 `format` 的相同参数: ~~~ > (error "Your report uses ~A as a verb." 'status) Error: Your report uses STATUS as a verb Options: :abort, :backtrace >> ~~~ 如上所示,除非这样的状况被处理好了,不然执行就会被打断。 用来捕捉错误的更抽象操作符包括了 `ecase` 、 `check-type` 以及 `assert` 。前者与 `case` 相似,要是没有键值匹配时会捕捉一个错误: ~~~ > (ecase 1 (2 3) (4 5)) Error: No applicable clause Options: :abort, :backtrace >> ~~~ 普通的 `case` 在没有键值匹配时会返回 `nil` ,但由于利用这个返回值是很差的编码风格,你或许会在当你没有 `otherwise` 子句时使用 `ecase` 。 `check-type` 宏接受一个位置,一个类型名以及一个选择性字符串,并在该位置的值不是预期的类型时,捕捉一个可修正的错误 (correctable error)。一个可修正错误的处理程序会给我们一个机会来提供一个新的值: ~~~ > (let ((x '(a b c))) (check-type (car x) integer "an integer") x) Error: The value of (CAR X), A, should be an integer. Options: :abort, :backtrace, :continue >> :continue New value of (CAR X)? 99 (99 B C) > ~~~ 在这个例子里, `(car x)` 被设为我们提供的新值,并重新执行,返回了要是 `(car x)` 本来就包含我们所提供的值所会返回的结果。 这个宏是用更通用的 `assert` 所定义的, `assert` 接受一个测试表达式以及一个有着一个或多个位置的列表,伴随着你可能传给`error` 的参数: ~~~ > (let ((sandwich '(ham on rye))) (assert (eql (car sandwich) 'chicken) ((car sandwich)) "I wanted a ~A sandwich." 'chicken) sandwich) Error: I wanted a CHICKEN sandwich. Options: :abort, :backtrace, :continue >> :continue New value of (CAR SANDWICH)? 'chicken (CHICKEN ON RYE) ~~~ 要建立新的处理程序也是可能的,但大多数程序员只会间接的利用这个可能性,通过使用像是 `ignore-errors` 的宏。如果它的参数没产生错误时像在 `progn` 里求值一样,但要是在求值过程中,不管什么参数报错,执行是不会被打断的。取而代之的是, `ignore-errors` 表达式会直接返回两个值: `nil` 以及捕捉到的状况。 举例来说,如果在某个时候,你想要用户能够输入一个表达式,但你不想要在输入是语法上不合时中断执行,你可以这样写: ~~~ (defun user-input (prompt) (format t prompt) (let ((str (read-line))) (or (ignore-errors (read-from-string str)) nil))) ~~~ 若输入包含语法错误时,这个函数仅返回 `nil` : ~~~ > (user-input "Please type an expression") Please type an expression> #%@#+!! NIL ~~~ 脚注 [[1]](http://acl.readthedocs.org/en/latest/zhCN/ch14-cn.html#id2) | 虽然标准没有提到这件事,你可以假定 `and` 以及 `or` 类型标示符仅考虑它们所要考虑的参数,与 `or` 及 `and` 宏类似。 [[2]](http://acl.readthedocs.org/en/latest/zhCN/ch14-cn.html#id3) | 某些 Common Lisp 实现,当我们不在用户包下时,会在顶层提示符前打印包的名字。