[原创] 出错处理的点滴经验
出错处理函数的浅析暨
Acet-error-init 的“解密”
======================================================================
【文 章】caoyin·LongfinTools
【引用资源】部分 Lisp 的源代码来自 AutoCAD R14 BONUS
Copyright (C) 1997 by Autodesk, Inc.
======================================================================
■■■■ 0 前言
自一年前邂逅明经通道以来,开始学习 LISP ,收获真的不少。虽然自己的
水平还十分浅薄,但仍欣慰之至。
我收集并总结了 LISP 出错处理函数的点滴经验,与大家分享。希望能得到
大家的指点和批评。
■■■■ 1 浅析定义出错处理函数的常犯的错误
■■ 1.1 概念及其编程应用的意义
我们知道 *error*函数是用来执行程序出错时进行相关错误处理
的特殊函数操作。就调用方式而言,大家都很熟悉。所以我们要讨论的
不是技术问题,而是它对我们编程应用的意义。
一个完整的程序应该具备一个周全的出错处理。因此,出错处理函
数我们就会频繁的使用。怎样让我们自定义的出错处理能够应对各种各
样的出错情况呢?
通常我们的出错处理函数会分为三部分来定义:
a. 定义初始化设置,如保存 *error* 的当前值,将自定义的出错函数
赋名为 *error*,放置 UNDO 的开始标记,保存系统变量信息并设置新
值等等。★这部分通常被放在程序的开始。
b. 定义自己的 *error*,在出错时自动激活该函数。执行必要的 UNDO
恢复,系统变量恢复,初始化设置中的全局变量恢复以及其他的图形操
作等。
c. 定义正常退出的恢复设置。★这部分通常被放在程序的结束。
基本的格式如下:
;;------------------------------------------------------------
;;例1 这是大多数人的写法
(defun xxx-begin (varlst / cmd)
(setq cmd (getvar "cmdecho"))
(setvar "cmdecho" 0)
(command "_.undo" "_begin");放置UNDO开始标记
(setvar "cmdecho" cmd)
(setq $savvarlst (mapcar '(lambda (x)
(list x (getvar x))
)
varlst
) ;系统变量保存及设置
$olderr *error* ;保存 *error* 的当前值
*error* xxx-error ;自定义的出错函数赋名为 *error*
)
;...
)
(defun xxx-error (msg)
(princ msg) ;打印出错信息
(xxx-end) ;调用自定义函数 xxx-end
;... ;其他出错处理
)
(defun xxx-end (/ cmd)
(if $savvarlst
(mapcar '(lambda (x)
(apply 'setvar x)
)
$savvarlst
) ;系统变量恢复到初始设置
)
(setq *error* $olderr ;*error* 恢复到初始设置
$savvarlst nil ;全局变量设置为 nil
$olderr nil
)
(setq cmd (setvar "cmdecho"))
(setvar "cmdecho" 0)
(command "_undo" "_end") ;放置 UNDO 的结束标志
(setvar "cmdecho" cmd)
(princ)
)
;;------------------------------------------------------------
看上去似乎可以了,但在一些特定的情况下还会出错,它仍然不具备通
用性。
我们在下面具体分析。
■■ 1.2 通用性及出错可能性分析
我们在出错处理函数要完成的基本任务,通常包括以下几个要素,
大家可以检验一下自己写的出错函数能否胜任以下要求。
a. 系统变量设置
◇当程序被用户取消或出现错误而中止能否恢复。
◇中途设置的(非在初始时设置的)的系统变量,当程序被用户取消
或出现错误而中止能否恢复。
用上面(例1)的函数举例如下:
;例2---------------------------------------------------
(defun c:test (/ os)
(xxx-begin '("cmdecho" "skpoly" "cecolor"))
(setvar "cmdecho" 0)
(setvar "skpoly" 1)
(setvar "cecolor" "1")
;...
(setq os (getvar "osmode"))
(setvar "osmode" 3333)
;...
;;出错点,在此按 ESC
;...
(setvar "osmode" os)
(xxx-end)
)
;-------------------------------------------------------
在运行以上程序时用户按下 "ESC" ,相信 "cmdecho" "skpoly"
"cecolor" 能够恢复原值,但 "osmode" 就不行了。
--------------------------------------------------------
问:为什么不在初试时的函数中设置?
答:假如在复杂程序中可能会套嵌调用初始化函数,然 *error*
设置只能有一个。
(defun c:test (/ ttt)
(xxx-begin '("cmdecho" "skpoly" "cecolor"))
...
(defun ttt ()
(xxx-begin '("osmode"))
...
)
...
)
问:为什么不在 xxx-error 中补充定义?
答:补充定义的 xxx-error 只能适用当前的程序。我们要求的是
能够在任意的程序中调用,这就是我讲的“通用性”。
---------------------------------------------------------
其实我们只要把例2中的
(setq os (getvar "osmode"))
(setvar "osmode" 3333)
...
***出错点
...
(setvar "osmode" os)
改成:
(setq $savvarlst
(cons (list "osmode" (getvar "osmode"))
$savvarlst
)
)
(setvar "osmode" 3333)
;...
;;出错点,在此按 ESC
;...
就行了,这我们在后面讨论。
b. UNDO 设置
谈论 UNDO 设置之前我们可能有必要来了解一下系统变量 UNDOCTL
的相关设置。下面是 AUTOCAD 的帮助:
---------------------------------------------------------------
(只读)
类型 整数
保存位置尚未保存
初始值 21
指示 UNDO 命令的“自动”、“控制”和“编组”选项的状态。 系统将
使用下列位码值之和将该设置存储为一个位码:
0 UNDO 关闭
1 UNDO 打开
2 只能放弃一条命令
4 打开“自动”
8 一个编组处于当前活动状态
16 将缩放和平移操作编组为单个操作
---------------------------------------------------------------
看完上面的帮助,我们来用(例1)中的函数来作测试:
;例3-----------------------------------------------------------
;我们先作如下操作:
(command "_.undo" "_control" "_none")
;然后我们来测试
(xxx-begin '())
;结果返回命令行操作错误信息:"无效的选项关键字。"
;--------------------------------------------------------------
为什么呢?其实错误在于 xxx-begin 函数中的
(command "_.undo" "_begin")
因为我们执行了
(command "_.undo" "_control" "_none")
操作之后 UNDO 命令的关键字发生了变化,如下:
“输入 UNDO 控制选项 [全部(A)/无(N)/一个(O)/合并(C)] <全部>:”
所以我们需要根据系统变量 UNDOCTL 的值,而对症下药。然而由于该变量
为只读属性,SETVAR 对它无效。所以我们必须通过命令行操作来完成相关
设置。
c. 程序出错后的操作
同样以上面的例1的函数为例:
;例4-------------------------------------------------------------
(defun c:test (/ p1 p2 e p3)
(xxx-begin '("osmode"))
(initget 1)
(setq p1 (getpoint "\n指定点1: "))
(initget 1)
(setq p2 (getpoint p1 "\n指定点2: "))
(command "_.line" p1 p2 "")
(setq e (entlast))
(redraw e 3)
(initget 1)
(setq p3 (getpoint p2 "\n指定点3 (或在此按 ESC): ")) ;;出错点
(command "_.line" p2 p3 "")
(redraw e 4)
(xxx-end)
)
;---------------------------------------------------------------
正常运行例4的程序第一个对象被高亮显示后被恢复,而中途出错的则无法
恢复。
d. 先选后执行的功能
我们知道当 (= 1 (logand 1 (getvar "pickfirst")))返回 T 的情
况下我们可以先选择对象,而后执行命令操作。如命令 MOVE、COPY 等。
但是例1的函数在此则无法实现。为什么呢?
原来我们先选后,然后执行 xxx-begin 中的
(command "_.undo" "_begin")
后先选的对象被取消了。
ActiveX 的方法不失为一种好方法:
;---------------------------------------------------------------
;初始时
(setq $acaddoc (vla-get-activedocument (vlax-get-acad-object)))
(vla-startundomark $acaddoc)
;结束时
(if $acaddoc
(progn (vla-endundomark $acaddoc) (setq $acaddoc nil))
)
;---------------------------------------------------------------
上面的方法优劣参半:
优点--是既能够实现先选后执行的功能,又避免了系统变量 cmdecho
的设置。
缺点--它在理论上等价于
(command "_undo" "_begin") 和
(command "_undo" "_end")
也存在我们在例 3 中的问题。
于是我们只能在调用 xxx-begin 开始加上以下代码:
(if (= 1 (logand 1 (getvar "pickfirst")))
(setq ss (ssgetfirst))
)
而在调用 xxx-begin 结束加上以下代码:
(if (apply 'or ss)
(apply 'sssetfirst ss)
)
■■■■ 2 Acet-error-init Acet-error Acet-error-restore 的“解密”
■■ 2.1 本程序根据 AutoCAD R14 BONUS 的源码修改,源程序中有不少 bug
这里做了修正。
■■ 源代码;;; ┏━━━┳━━━━━━━━━━━━━━━━━━━┓
;;; ┃LT: ┃ 出错处理函数 ┃
;;; ┗━━━┻━━━━━━━━━━━━━━━━━━━┛
;;曹饮整理修改
;;____________________________________________________________________________________________________
;; 〓 (lt:error-init lst) 〓
;;[功能] 出错处理初始化
;;[参数] lst---表(元素1 元素2 元素3)
;; 元素1:包含系统变量及其值的列表
;; 元素2:nil--->不操作
;; 0----->放置 UNDO 的 BEGIN 和 END 标志
;; 1或T-->放置 UNDO 的 BEGIN 和 END 标志,但出错时 UNDO 回操作前状态
;; 元素3:出错时执行的特殊操作
(defun lt:error-init (lst / ss varl untag reset bar)
(if (= 1 (logand 1 (getvar "pickfirst")))
(progn
(setq ss (ssgetfirst))
(if (not (apply 'or ss)) (setq ss nil))
)
)
(mapcar 'set '(varl untag reset bar) lst)
(setq $lt-alive$ (if (not $lt-alive$) 1 (1+ $lt-alive$)))
(if (and (> $lt-alive$ 1)
(= "*LTErr*" (last $lt-error$))
)
(progn
(setq *error* lt:error $lt-alive$ 1)
(lt:error-restore)
(setq $lt-alive$ 1)
)
)
(if (<= $lt-alive$ 0)
(progn
(setq $lt-alive$ 0)
(lt:error-restore)
(setq $lt-alive$ 1)
)
)
(if (= $lt-alive$ 1)
(progn
(setq $lt-olderror$ *error*)
(if (or (not (listp $lt-error$))
(/= "*LTErr*" (last $lt-error$))
)
(setq $lt-error$ (list "*LTErr*"))
)
(if (or (= (type (car $lt-error$)) 'list)
(= "*LTErr*" (car $lt-error$))
)
(setq $lt-error$ (cons untag $lt-error$))
(setq $lt-error$ (cons untag (cdr $lt-error$)))
)
(if untag
(lt:undo-init T)
(setq $lt-undoctl$ nil)
)
)
)
(lt:sysvar-set (car lst))
(if (= $lt-alive$ 1)
(progn
(setq *error* lt:error)
(if reset (setq $lt-error$ (append (reverse (cdr (reverse $lt-error$)))
(list reset (last $lt-error$))
)
)
)
)
)
(if ss (apply 'sssetfirst ss))
)
;;____________________________________________________________________________________________________
;; 〓 (lt:error msg) 〓
;;[功能] 出错处理
;;[参数] msg---出错信息。在通常情况下,*ERROR* 函数会根据不同出错情况,而自动返回出错信息,如:“函数
;; 被取消”。
;; 所以本程序需要用户可以通过定义全局变量 $lt-errormsg$ 而改变默认出错信息:
;; (1) 为字符串---打印 $lt-errormsg$ 的内容,如:(setq $lt-errormsg$ "程序出错")))
;; (2) 为表达式---通过执行表达式而获取 msg 的值。如:
;; (if (= msg "函数被取消") (setq msg "用户中止"))
;; 但表达式应以表的方式来传递给程序,即在表达式前面加上一个单引号 “'”。
;; (setq $lt-errormsg$ '(if (= msg "函数被取消") (setq msg "用户中止")))
;; (3) 为 T ------显示默认出错信息
;; (4) 为 nil-----不显示出错信息
(defun lt:error (msg)
(if (and (listp $lt-error$) (= "*LTErr*" (last $lt-error$)))
(progn
(mapcar 'eval (cdr (reverse (cdr $lt-error$))))
(setq $lt-error$ (list (car $lt-error$) (last $lt-error$)))
)
)
(cond
((= (type $lt-errormsg$) 'list) (eval $lt-errormsg$))
((= (type $lt-errormsg$) 'str) (setq msg $lt-errormsg$))
((not $lt-errormsg$) (setq msg nil))
)
(if msg (princ msg))
(while (not (= (getvar "cmdnames") "")) (command))
(if (and (or (= (car $lt-error$) 1) (= (car $lt-error$) T))
$lt-undoctl$
)
(progn
(lt:sysvar-set '("cmdecho" 0))
(while (not (wcmatch (getvar "cmdnames") "*UNDO*"))
(command "_.undo")
)
(command "_end")
(command "_.undo" "1")
(while (not (= (getvar "cmdnames") "")) (command))
)
)
(setq $lt-alive$ 1)
(lt:sysvar-restore)
(setq $lt-alive$ 0)
(lt:undo-restore)
(if $lt-olderror$
(setq *error* $lt-olderror$ $lt-olderror$ nil)
)
(princ)
)
;;____________________________________________________________________________________________________
;; 〓 (lt:error-restore) 〓
;;[功能] 出错处理恢复
(defun lt:error-restore ()
(setq $lt-alive$ (1- $lt-alive$)
$lt-error$ (cons (car $lt-error$)
(reverse (cons (last $lt-error$)
(cddr (reverse (cdr $lt-error$)))
)
)
)
)
(if (>= $lt-alive$ 0)
(lt:sysvar-restore)
(setq $lt-varlist$ nil)
)
(if (<= $lt-alive$ 0)
(progn
(lt:undo-restore)
(if $lt-olderror$ (setq *error* $lt-olderror$ $lt-olderror$ nil))
)
)
(princ)
)
;;____________________________________________________________________________________________________
;; 〓 (lt:sysvar-set lst) 〓
;;[功能] 系统变量设置
;;[参数] lst---表。0 和偶数位置的元素:系统变量
;; 奇数位置的元素: 要设置的系统变量值,对应该元素之前的系统变量。
;; 格式: '(系统变量1 系统变量值1 系统变量2 系统变量值2 ...)
;;[返回] 表。表的长度等于 $lt-varlist$ 值为 nil 之后运行 lt:sysvar-set 函数的次数。
;; 每个表元素具备以下格式:((系统变量1 系统变量值1) (系统变量2 系统变量值2) ...)
(defun lt:sysvar-set (lst / lst3 a n b lst2)
(setq lst3 (car $lt-varlist$) n -2)
(repeat (/ (length lst) 2)
(setq a (strcase (nth (setq n (+ n 2)) lst))
b (nth (1+ n) lst)
lst2 (append lst2 (list (list a (getvar a))))
)
(if (and $lt-varlist$ (not (assoc a lst3)))
(setq lst3 (append lst3 (list (list a (getvar a)))))
)
(setvar a b)
)
(if $lt-varlist$
(setq $lt-varlist$ (append (list lst3) (cdr $lt-varlist$) (list lst2)))
(setq $lt-varlist$ (list lst2))
)
)
;;____________________________________________________________________________________________________
;; 〓 (lt:sysvar-restore) 〓
;;[功能] 系统变量恢复
;;[说明] 必须确保变量 $lt-varlist$ 存在,该变量在运行 lt:sysvar-set 函数时设置;
;; 当变量 $lt-alive$ 小于等于 0 时,依次恢复到 lt:sysvar-set 函数最近设置点
;; 当变量 $lt-alive$ 大于 0或为 nil时,本程序恢复到 lt:sysvar-set 函数第一个设置点,且
;; 设置变量 $lt-alive$ 为 nil。
;;[返回] 变量 $lt-alive$ 的当前值
(defun lt:sysvar-restore (/ lst)
(if (<= $lt-alive$ 0)
(setq lst (car $lt-varlist$) $lt-varlist$ (list lst))
(setq lst (last $lt-varlist$))
)
(mapcar '(lambda (x) (apply 'setvar x)) lst)
(setq $lt-varlist$ (reverse (cdr (reverse $lt-varlist$))))
)
;;____________________________________________________________________________________________________
;; 〓 (lt:undo-init varset) 〓
;;[功能] UNDO 初始化设置,及放置开始(begin)标记
;;[参数] varset---为 T 时:将返回值赋于全局变量 $lt-undoctl$
;; 为 nil 时:忽略值赋
;;[返回] 初始化前系统变量 "undoctl" 的值
(defun lt:undo-init (varset / x y z)
(defun x () (getvar "undoctl"))
(defun y (i) (= (logand i (x)) i))
(lt:sysvar-set '("cmdecho" 0))
(setq z (x))
(if (or (= (x) 0) (= (x) 16)) (command "_.undo" "_all"))
(if (or (not (y 1)) (y 2)) (command "_.undo" "_control" "_all"))
(if (y 4) (command "_.undo" "_auto" "_off"))
(while (y 8) (command "_.undo" "_end"))
(while (not (y 8)) (command "_.undo" "_begin"))
(lt:sysvar-restore)
(if (= varset T) (setq $lt-undoctl$ z) z)
)
;;____________________________________________________________________________________________________
;; 〓 (lt:undo-restore) 〓
;;[功能] UNDO 恢复设置,及放置结束(end)标记放置
;;[说明] 必须确保变量 $lt-undoctl$ 存在,即表示开始(begin)标记已经放置且保存 UNDO 初始化前的
;; "undoctl" 变量值
(defun lt:undo-restore (/ x)
(if $lt-undoctl$
(progn
(defun x (i val) (= (logand i val) i))
(lt:sysvar-set '("cmdecho" 0))
(while (= 8 (logand 8 (getvar "undoctl"))) (command "_.undo" "_end"))
(if (/= $lt-undoctl$ (getvar "undoctl"))
(progn
(cond
((= 0 $lt-undoctl$) (command "_.undo" "_control" "_none"))
((x 2 $lt-undoctl$) (command "_.undo" "_control" "_one"))
)
(if (x 4 $lt-undoctl$) (command "_.undo" "_auto" "_on"))
)
)
(if (not (x 2 (getvar "undoctl"))) (lt:sysvar-restore))
(setq $lt-undoctl$ nil)
)
)
)
■■ 2.2 测试以程序(defun c:test (/ p1 p2 e p3 p4)
(lt:error-init (list '("cmdecho" 0 "cecolor" "1") ;;初始时设置的变量
0 ;;
'(if e (redraw e 4)) ;;如果出错,取消亮显
)
)
(princ "\n测试开始!!")
(initget 1)
(setq p1 (getpoint "\n点1: "))
(initget 1)
(setq p2 (getpoint p1 "\n点2: "))
(lt:sysvar-set '("osmode" 0))
(command "_.line" p1 p2 "")
(setq e (entlast))
(redraw e 3) ;;亮显
(initget 1)
(setq p3 (getpoint "\n点3(在这里按取消): "))
(initget 1)
(setq p4 (getpoint p3 "\n点4: "))
(command "_.line" p3 p4 "")
(lt:error-restore)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
有关其中的参数说明及全局变量说明明天再发
不错,可以 谢谢LZ分享经验 <p>接一楼:</p><p>以上共提供了 7 个函数,以下是参数和全局变量的具体用法:</p><p>1. lt:error-init</p><p> 本函数共 1 个参数,参数为表,但表中包含三个元素</p><p><font color="#000000"> 元素1: 包含系统变量及其值的列表,程序在初始时将系统变量依次设置为对应</font></p><p> 值。</p><p><font color="#000000"> 格式为: '(变量1 变量值1 变量2 变量值2 变量3 变量值3 ... 变量n 变量值n)</font></p><p><font color="#000000"> 比如: '("cmdecho" 0 "osmode" 0 "cecolor" "1")</font></p><p><font color="#800080"><font color="#ff0000"> 中途设置的变量为了确保能够在出错后恢复,可以使用 lt:sysvar-set 函数,</font></font></p><p><font color="#800080"><font color="#ff0000"> 后面我们再详细说明。</font></font></p><p><br/><font color="#000000"> 元素2: UNDO 的设置</font></p><p><font color="#000000"> nil--->不做任何操作</font><font color="#000000"><br/> 0----->放置 UNDO 的 BEGIN 和 END 标志</font></p><p><font color="#000000"> 这样确保程序结束时,用户可通过 U 命令,一次取消该操作<br/> 1或T-->放置 UNDO 的 BEGIN 和 END 标志,但出错时 UNDO 回操作前状态</font></p><p><font color="#000000"> 这样确保程序结束时,用户可通过 U 命令,一次取消该操作</font></p><p><font color="#000000"> 而且,出错后,程序自动执行(command "undo" 1)的操作</font></p><p><font color="#000000"> 元素3: 出错时执行的特殊操作。为任意表达式,但以表的方式传递。</font></p><p><font color="#000000"> 所以<font color="#000000"><font color="#ff1111">表达式之前要加一个 ' 符号</font>。</font></font></p><p><font color="#000000"> 我们通常会需要在程序出错后,执行其他的任何表达式的操作。</font></p><p><font color="#000000"> 比如:redraw 函数亮显某图元。出错后 UNDO 的设置无法正常恢复显示。</font></p><p><font color="#000000"> 所以我们可以设置此元素为: '(if en (redraw en 4))</font></p><p><font color="#000000"> 对于多个操作可用 progn 函数连接。</font></p><p><font color="#000000"> 如:'(progn (if en (redraw en 4))</font></p><p><font color="#000000"> (if en2 (entdel en2))</font></p><p><font color="#000000"> )</font></p><p><font color="#000000"> 全局变量:</font></p><p><font color="#000000"> <font color="#ff0000">$lt-alive$</font> 记录执行 lt:error-init lt:error lt:error-restore 的执行情况</font></p><p><font color="#000000"> 如 $lt-alive$=1 说明已经执行了一次 lt:error-init 函数,并且尚未执行 lt:error-restore </font></p><p><font color="#000000"> 恢复操作。</font></p><p><font color="#000000"> 这个时候,如果用户再次 执行 lt:error-init ,lt:error-init会自动判别情况,先恢复已经记录</font></p><p><font color="#000000"> 在 $lt-error$ 变量里的相关操作后,将 $lt-alive$ 变量归零。再执行 lt:error-init 设置。</font></p><p><font color="#000000"> <font color="#f70909">$lt-error$ </font></font></p><p><font color="#000000"><font color="#f70909"> </font><font color="#000000">lt:error-init 通过 $lt-error$ 变量将出错的相关恢复要求传递给 lt:error 和lt:error-restore</font></font></p><p><font color="#000000"> 并将执行过的内容依次删除。</font></p><p><font color="#000000">;;;------------------------------待续</font></p><p><font color="#000000"></font></p><p><font color="#000000"></font></p><p><font color="#000000"></font></p> 很有帮助,谢谢楼主。 透彻 <p>值得学习</p><p>谢楼主分享</p> ........ <p>挖到前排来学习 </p> 谢谢版主的无私!