Common Lisp 也和其他语言一样有错误及异常处理(Condition)的机制,而且比其他的语言做的更好。

什么是异常?

Just like in languages that support exception handling (Java, C++,
Python, etc.), a condition represents, for the most part, an
“exceptional” situation. However, even more so that those languages,
a condition in Common Lisp can represent a general situation where
some branching in program logic needs to take place
, not
necessarily due to some error condition. Due to the highly
interactive nature of Lisp development (the Lisp image in
conjunction with the REPL), this makes perfect sense in a language
like Lisp rather than say, a language like Java or even Python,
which has a very primitive REPL. In most cases, however, we may not
need (or even allow) the interactivity that this system offers
us. Thankfully, the same system works just as well even in
non-interactive mode.

z0ltan

现在来一步步来讲解介绍把。

忽略所有的错误,并返回 nil

有时候你明确的知道所调用的函数会出错,但是这个错误无关紧要,就可以使用 [ignore-errors][ignore-errors] 来忽略:

  1. (ignore-errors
  2. (/ 3 0))
  3. ; in: IGNORE-ERRORS (/ 3 0)
  4. ; (/ 3 0)
  5. ;
  6. ; caught STYLE-WARNING:
  7. ; Lisp error during constant folding:
  8. ; arithmetic error DIVISION-BY-ZERO signalled
  9. ; Operation was (/ 3 0).
  10. ;
  11. ; compilation unit finished
  12. ; caught 1 STYLE-WARNING condition
  13. NIL
  14. #<DIVISION-BY-ZERO {1008FF5F13}>

以上的代码会得到 division-by-zero 的警告,但是代码依然可以完整的运行并返回:nil 和 错误的信号。返回值是无法改变的。

记住,在 Slime 中可以使用鼠标右击来 插入(inspect) 异常状态的条件。

捕获任意异常(handler-case)

ignore-errors 集成在 handler-case。可以将上面的例子修改成以下的代码,然后返回所需要的结果:

  1. (handler-case (/ 3 0)
  2. (error (c)
  3. (format t "We caught a condition.~&")
  4. (values 0 c)))
  5. ; in: HANDLER-CASE (/ 3 0)
  6. ; (/ 3 0)
  7. ;
  8. ; caught STYLE-WARNING:
  9. ; Lisp error during constant folding:
  10. ; Condition DIVISION-BY-ZERO was signalled.
  11. ;
  12. ; compilation unit finished
  13. ; caught 1 STYLE-WARNING condition
  14. We caught a condition.
  15. 0
  16. #<DIVISION-BY-ZERO {1004846AE3}>

这个也返回了两个值,0 和 异常结果。

handler-case 的通用格式是:

  1. (handler-case (code that errors out)
  2. (condition-type (the-condition) ;; <-- optional argument
  3. (code))
  4. (another-condition (the-condition)
  5. ...))

也可以像 cond 中匹配 t 一样匹配一些异常:

  1. (handler-case
  2. (progn
  3. (format t "This won't work…~%")
  4. (/ 3 0))
  5. (t (c)
  6. (format t "Got an exception: ~a~%" c)
  7. (values 0 c)))
  8. ;;
  9. ;; This won't work…
  10. ;; Got an exception: arithmetic error DIVISION-BY-ZERO signalled
  11. ;; Operation was (/ 3 0).
  12. ;; 0
  13. ;; #<DIVISION-BY-ZERO {100608F0F3}>

捕获特定的异常

  1. (handler-case (/ 3 0)
  2. (division-by-zero (c)
  3. (format t "Caught division by zero: ~a~%" c)))
  4. ;;
  5. ;; Caught division by zero: arithmetic error DIVISION-BY-ZERO signalled
  6. ;; Operation was (/ 3 0).
  7. ;; NIL

以上的工作流和其他语言中的 try/catch 很想,但是在 Lisp 中可以做的更多。

handler-case VS handler-bind

handler-case 类似于 try/catch

handler-bind (下面例子中会使用) 是当异常信号抛出时完全进行控制的。他可以使用 调试器和重启器,无论是交互时还是已经编程好了的。

当某些库无法捕获所有的异常并造成困恼时,可以使用 restarts (通过建立 restart-case) 来查看栈中的状态,包括该库调用的其他的库的 restart。然后就可以对栈中的信息进行追踪,在某些lisp中,甚至可以查看到局部变量等信息。一旦使用 handler-case,“忘了”这点的话,所以的问题都会解决。handler-bind 不支持重定位栈。

在介绍 handler-bind 前,先来看看 conditions 和 restarts 吧。

定义和处理异常

define-conditionmake-condition.

  1. (define-condition my-division-by-zero (error)
  2. ())
  3. (make-condition 'my-division-by-zero)
  4. ;; #<MY-DIVISION-BY-ZERO {1005A5FE43}>

在创建异常时提供一些信息会让这个异常更加方便使用:

  1. (define-condition my-division-by-zero (error)
  2. ((dividend :initarg :dividend
  3. :initform nil
  4. :reader dividend)) ;; <-- we'll get the dividend with (dividend condition). See the CLOS tutorial if needed.
  5. (:documentation "Custom error when we encounter a division by zero.")) ;; good practice ;)

这样,就可以抛出异常并将信息显示出来。

  1. (make-condition 'my-division-by-zero :dividend 3)
  2. ;; #<MY-DIVISION-BY-ZERO {1005C18653}>

注,以下是 Common Lisp Object System 中的一个简单的使用方法:

  1. (make-condition 'my-division-by-zero :dividend 3)
  2. ;; ^^ this is the ":initarg"

:reader dividend 生成一个通用函数,该函数是 my-division-by-zero 对象的一个 “getter”。

  1. (make-condition 'my-division-by-zero :dividend 3)
  2. ;; #<MY-DIVISION-BY-ZERO {1005C18653}>
  3. (dividend *)
  4. ;; 3

“:accessor” 既是 getter 也是 setter.

因此,define-condition 的一般格式看起来想类的定义,但要区分一下,异常不是单独的对象。

区别是无法使用 slot-value

抛出异常: error, warn, signal

error 的两种用法:

  • (error "some text"):抛出 simple-error 异常,然后打开解释器的debugger。
  • (error 'my-error :message "We did this and that and it didn't work."):创建并抛出自定义的异常,然后打开解释器的debugger。
  1. (error 'my-division-by-zero :dividend 3)
  2. ;; which is a shortcut for
  3. (error (make-condition 'my-division-by-zero :dividend 3))

warn 不会打开 debugger (create warning conditions by subclassing simple-warning).

signal 进入debugger,同时抛出一个上层的异常。

这个可以用在任何地方。比如说,可以在操作是来追踪一个进程。也可以创建个带有 percent 的异常,并在进程创建时抛出,让后通过上层的代码进行处理显示出来。

异常继承

simple-error 的子类有 simple-error, simple-condition, error, serious-condition, condition, t.

simple-warning 的子类有 simple-warning, simple-condition, warning, condition, t.

自定义错误信息(:report)

到目前位置,抛出自定义的错误时,会在 debugger 中见到如下的信息:

  1. Condition COMMON-LISP-USER::MY-DIVISION-BY-ZERO was signalled.
  2. [Condition of type MY-DIVISION-BY-ZERO]

可以通过 :report 这个函数在异常的定义中定义更详细的信息:

  1. (define-condition my-division-by-zero (error)
  2. ((dividend :initarg :dividend
  3. :initform nil
  4. :accessor dividend))
  5. ;; the :report is the message into the debugger:
  6. (:report (lambda (condition stream)
  7. (format stream "You were going to divide ~a by zero.~&" (dividend condition)))))

现在的话

  1. (error 'my-division-by-zero :dividend 3)
  2. ;; Debugger:
  3. ;;
  4. ;; You were going to divide 3 by zero.
  5. ;; [Condition of type MY-DIVISION-BY-ZERO]

Inspecting the stacktrace

另一个小提示,不是 Slime 的教程。可以在 debugger 中插入一些栈追踪,函数调用的参数,跳转到出错的代码行 通过 v 键,执行代码,e 键等等。

通常,你可以编辑一个有bug的函数,编译(C-c C-c),然后选择 “RETRY” 来重现代码的执行。

以上的都是基于编译器。

更多信息参见 debugging section.

Restarts, interactive choices in the debugger

Restart 是在 debugger 中的一些选择,通常是有 RETRYABORT

在处理 restarts 时,可以认为不会出错时进行操作。

使用断言中的 optional restart

assert 最简单的格式如下:

  1. (assert (realp 3))
  2. ;; NIL = passed

当断言失败时,可以在 debugger 中输出如下信息:

  1. (defun divide (x y)
  2. (assert (not (zerop y)))
  3. (/ x y))
  4. (divide 3 0)
  5. ;; The assertion (NOT #1=(ZEROP Y)) failed with #1# = T.
  6. ;; [Condition of type SIMPLE-ERROR]
  7. ;;
  8. ;; Restarts:
  9. ;; 0: [CONTINUE] Retry assertion.
  10. ;; 1: [RETRY] Retry SLIME REPL evaluation request.
  11. ;;

也接受参数来修改值:

  1. (defun divide (x y)
  2. (assert (not (zerop y))
  3. (y) ;; list of values that we can change.
  4. "Y can not be zero. Please change it") ;; custom error message.
  5. (/ x y))

现在,得到了一个新的 restart,并且可以修改 Y 的值:

  1. (divide 3 0)
  2. ;; Y can not be zero. Please change it
  3. ;; [Condition of type SIMPLE-ERROR]
  4. ;;
  5. ;; Restarts:
  6. ;; 0: [CONTINUE] Retry assertion with new value for Y. <--- new restart
  7. ;; 1: [RETRY] Retry SLIME REPL evaluation request.
  8. ;;

当选定时,会有个新的值在 REPL 中输出:

  1. The old value of Y is 0.
  2. Do you want to supply a new value? (y or n) y
  3. Type a form to be evaluated:
  4. 2
  5. 3/2 ;; and our result.

定义 restarts (restart-case)

restart-case.

  1. (defun divide-with-restarts (x y)
  2. (restart-case (/ x y)
  3. (return-zero () ;; <-- creates a new restart called "RETURN-ZERO"
  4. 0)
  5. (divide-by-one ()
  6. (/ x 1))))
  7. (divide-with-restarts 3 0)

在每个错误中,最上面都会有以下两个选择:

12. Error Handling - 图1

好吧,现在开始写一些可多性更高的 “reports” 吧:

  1. (defun divide-with-restarts (x y)
  2. (restart-case (/ x y)
  3. (return-zero ()
  4. :report "Return 0" ;; <-- added
  5. 0)
  6. (divide-by-one ()
  7. :report "Divide by 1"
  8. (/ x 1))))
  9. (divide-with-restarts 3 0)
  10. ;; Nicer restarts:
  11. ;; 0: [RETURN-ZERO] Return 0
  12. ;; 1: [DIVIDE-BY-ONE] Divide by 1

这样就好多了,但是不能像 assert 中的例子那样修改操作符。

在 restarts 中修改变量的值

以上定义的 restarts 并不会去接受新的值。为了实现这步,可以在 restart 中添加一个 :interactive 的 lambda 函数,用来获取一个新的值。在这,将使用 read

  1. (defun divide-with-restarts (x y)
  2. (restart-case (/ x y)
  3. (return-zero ()
  4. :report "Return 0"
  5. 0)
  6. (divide-by-one ()
  7. :report "Divide by 1"
  8. (/ x 1))
  9. (set-new-divisor (value)
  10. :report "Enter a new divisor"
  11. ;;
  12. ;; Ask the user for a new value:
  13. :interactive (lambda () (prompt-new-value "Please enter a new divisor: "))
  14. ;;
  15. ;; and call the divide function with the new value
  16. ;; possibly catching bad input again!
  17. (divide-with-restarts x value))))
  18. (defun prompt-new-value (prompt)
  19. (format *query-io* prompt) ;; *query-io*: the special stream to make user queries.
  20. (force-output *query-io*) ;; Ensure the user sees what he types.
  21. (list (read *query-io*))) ;; We must return a list.
  22. (divide-with-restarts 3 0)

调用时,会产生新的 restart,然后输入一个值,就会得到相应的结果:

  1. (divide-with-restarts 3 0)
  2. ;; Debugger:
  3. ;;
  4. ;; 2: [SET-NEW-DIVISOR] Enter a new divisor
  5. ;;
  6. ;; Please enter a new divisor: 10
  7. ;;
  8. ;; 3/10

噢,你个倾向于使用图形界面?那就用 zenity (GNU/Linux环境中)吧:

  1. (defun prompt-new-value (prompt)
  2. (list
  3. (let ((input
  4. ;; We capture the program's output to a string.
  5. (with-output-to-string (s)
  6. (let* ((*standard-output* s))
  7. (uiop:run-program `("zenity"
  8. "--forms"
  9. ,(format nil "--add-entry=~a" prompt))
  10. :output s)))))
  11. ;; We get a string and we want a number.
  12. ;; We could also use parse-integer, the parse-number library, etc.
  13. (read-from-string input))))

在运行一次,你就可以得到如下的图片了:

12. Error Handling - 图2

有趣吧,但这还不是全部。手动去选择 restarts 并不能满足需求。

自动调用 restarts (handler-bind, invoke-restart)

现在有一小段代码可以抛出异常,但是如果需要在更高层会自动的去处理这个异常并调用合适的 restart。那么就需要用到 handler-bindinvoke-restart:

  1. (defun divide-and-handle-error (x y)
  2. (handler-bind
  3. ((division-by-zero (lambda (c)
  4. (format t "Got error: ~a~%" c) ;; error-message
  5. (format t "and will divide by 1~&")
  6. (invoke-restart 'divide-by-one))))
  7. (divide-with-restarts x y)))
  8. (divide-and-handle-error 3 0)
  9. ;; Got error: arithmetic error DIVISION-BY-ZERO signalled
  10. ;; Operation was (/ 3 0).
  11. ;; and will divide by 1
  12. ;; 3

restarts 的其他用法 (find-restart)

find-restart.

find-restart 'name-of-restart 返回对应的 restart 或 nil

显示/隐藏 restarts

可以将 restarts 进行隐藏的,在 restart-case 中,除了 :report
:interactive,也接受 :test 关键词:

  1. (restart-case
  2. (return-zero ()
  3. :test (lambda ()
  4. (some-test))
  5. ...

处理 conditions (handler-bind)

handler-bind

  1. (handler-bind ((a-condition #'function-to-handle-it)
  2. (another-one #'another-function))
  3. (code that can...)
  4. (...error out))

可以通过 [unix-opts](https://github.com/mrkkrp/unix-opts) 库来进行讲解:

  1. (handler-bind ((opts:unknown-option #'unknown-option)
  2. (opts:missing-arg #'missing-arg)
  3. (opts:arg-parser-failed #'arg-parser-failed))
  4. (opts:get-opts))

自定义的 unknown-option 如下:

  1. (defun unknown-option (condition)
  2. (format t "~s option is unknown.~%" (opts:option condition))
  3. (opts:describe)
  4. (exit)) ;; <-- we return to the command line, no debugger.

接受异常作为参数,因此,可以获取所需要的信息。

执行代码, condition or not (“finally”) (unwind-protect)

try/catch/finally 中 “finally” 的部分是由 unwind-protect 实现。

with-open-file 的宏类似,结束后会自动关闭文件:

  1. (unwind-protect (/ 3 0)
  2. (format t "This place is safe.~&"))

以上代码弹出交互的 debugger,但之后会输出相对应的信息。

Resources

See also