主题:错误,让我们抓住它!(系列)
我们编程不管多么仔细,总还是难免在代码中留下隐患(或者说Bug),有时候看似没有问题的代码,某种情况下却会出错。要让程序健壮(管你怎么折腾,程序照样正常运行),无非是采用未雨绸缪(在错误可能发生之前就避免它)和亡羊补牢(让错误发生,但只要发生就采用补救的方式使程序继续正常执行)两种策略。
先看一个例子:有个表单(Form1),上面一个按钮(Command1),该按钮Click事件代码如下:
USE Test.dbf
Browse NOWAIT
假设工作路径下确实有Test.dbf,我们的意图很简单:单击按钮,打开这个表并且浏览它。
运行表单,单击按钮,没问题。并且随便单击按钮多次,都可以。完全实现了我们的意图。真的很好吗?NO,有隐患。
隐患(1) 第一次单击浏览后,如果工作区被改变,再单击就会出错。(体验:浏览后,简单的在命令窗口select 0,然后单击按钮)。
隐患(2) 如果单击之前,当前工作区已经有打开的另一个重要的表,单击按钮会关闭该表,这可能造成其它代码过程使用该表出错。
隐患(3) 这个谈不上隐患,但是增加了系统负担,那就是后续每次单击,系统都要先关闭表再重新打开表(尽管这个动作很快以至于你感觉不到)。
好吧,我们先采用未雨绸缪策略消除隐患,改写按钮Click事件代码如下:
IF USED("Test") &&先探测Test表是否已经打开
SELECT Test &&是,直接选择Test表所在的工作区
ELSE
USE Test.dbf IN 0 &&否,选择一个没有占用的最小编号工作区并打开Test表
SELECT Test
ENDIF
BROWSE NOWAIT
好家伙,一下子消除了3个隐患。不过可能还有隐患,极端的例子比如:
隐患(4) 无意中把Test.dbf文件删除了,于是错误又来了。
没关系,还是可以未雨绸缪一下:
IF NOT FILE("Test.dbf") &&探测Test表文件是否存在
Return &&愿意的话,这里先来点提示再返回
ENDIF
IF USED("Test")
SELECT Test
ELSE
USE Test.dbf IN 0
ENDIF
BROWSE NOWAIT
完美了,可还有更极端的例子,比如:伪造一个Test.dbf文件,可能它本身是个图片文件。
----有人说:你这完全是考虑过多了,有这必要吗?
----是的,我承认这个极端的情况不多见,不过因为它能引起出错,所以在帖子中还是考虑一下,至于实际开发要怎样,全在编程者考虑。另外即便不是特意伪造,有可能Test.dbf文件损坏,损坏了系统就认为不是表,这样一来,效果是一样的。
隐患(5) Test.dbf文件损坏或者不是表
如果Test.dbf文件损坏或者不是表,还能未雨绸缪吗?比较难了,我们没有一个现成的函数例如IsTable()来判断某文件是不是个表,可以自己编写,但代价巨大,你要熟悉表文件的物理结构,能读取一个文件的字节流判断它是否具有这样的结构。万一表还带有备注文件,那就更麻烦了。这种情况下不如采用亡羊补牢策略。
现在开始集中谈亡羊补牢策略,所以暂时把上面所有措施去除(事实上两种策略应该综合应用,后面谈),让Click代码回到最初的两句:
USE Test.dbf
Browse NOWAIT
这两句都可能出错,但第一句最重要,只要它没错,后一句就不会错。当代码运行产生错误时,老Fox有一套解决的办法,它向我们提供Error Code(错误码)和Error Message(错误信息),如果我们不来处理,老狐狸就自行处理,一般是弹出错误对话框,显示错误信息,让我们选择是终止程序,还是忽略,还是其它。让老狐狸处理不是太好(我们的程序不是成了太监---中途结束没有下面了;就是成了乞丐---破绽百出,可能一句错引起后面多句错),我们应该自行处理。
总体上fox提供了两种方式让编程者处理运行错误,一种是传统的错误处理(Error Handling),另一种是意外处理(Exception Handling)。
错误处理(Error Handling)
主要用ON ERROR 命令设置陷阱捕获错误,先看简单的例子,改写按钮Click事件代码如下:
ON ERROR lOK=.F. &&逻辑变量lOK用来作为错误标志,正常为真,出错为假
lOK=.T. &&假设后面不会出错
USE Test.dbf IN 0
select Test
IF lOK
Browse NOWAIT
ELSE
?"出错"
ENDIF
这里先用ON ERROR设置陷阱,后面只要有语句出错,立刻执行 lOK=.F.,然后返回出错的那条语句后面的一句继续执行。
这段代码中最可能出错的就是那条USE语句,如果它正常执行,则变量lOK为真,于是继续下去browse;一旦出错,程序流程就“掉进陷阱”,把lOK变为假,然后返回进入IF结构判断。
这段亡羊补牢代码可以很好的消除上面提到的隐患(1)、(2)、(3)、(4)、(5),但是请注意仅仅消除的是隐患(即保证程序能正常执行下去),不能保证功能正常。
比如,在Test.dbf文件正常的情况下,第一次单击按钮,功能正常(打开表并且浏览);关闭浏览窗口后,第2次单击的时候,不能浏览了,原因很简单,这时候USE命令会出错,陷阱会捕获它并处理。
如果要保证功能正常,并且能捕获错误,我们还要改写一下。这要求我们能判断到底出了什么错误,是表已经打开过呢?还是表文件损坏?还是表文件不存在?我们需要用到函数ERROR(),可能还要用到MESSAGE(),LINENO()等等。针对这个例子,我们可以写出下面的Click事件代码:
ON ERROR nErrCode=ERROR()
nErrCode=0
USE Test.dbf IN 0
DO CASE
CASE nErrCode=0 &&正常打开表
Select Test
Browse NOWAIT
CASE nErrCode=1 &&表文件不存在
?"表文件不存在"
CASE nErrCode=3 &&表已经打开
Select Test
Browse NOWAIT
OTHERWISE
?"表文件损坏或其它未知错误"
ENDCASE
上面用ERROR()函数得到出错时的错误码,你可能需要看帮助文件了解错误号分别代表什么错误发生了。另外该函数(和上面提到的几个函数)只能在陷阱过程中使用
。
不错不错,这样写代码很健壮,随便折腾它,程序照样完成,不过写的有一点复杂罢了。
-----插一点题外话,也是相关的:
琅拿度朋友回答提问曾写出如下代码
err_fnd=.F.
ON ERROR err_fnd=.T.
OLEAPP=CreateObject("word.application")
IF err_fnd
MESSAGEBOX("找不到“word”程序,请先安装 Microsoft office。",16,"错误...")
QUIT
......
这就很好,你怎么知道某个机器有或者没有安装word,于是采取亡羊补牢策略,不管372十1,先尝试生成word对象,出错就抓住它。
当然未雨绸缪也可以,不过要读取注册表判断是否安装word,代价大,没有这个好。
-----------------------------------
还是回到我们的例子,你注意到似乎ON ERROR命令后面好像只是一条赋值语句,其实不然,也可以是其它命令,事实上通常是调用一个专门编写的出错处理过程。
比较常见的写法是:
ON ERROR DO errhand WITH ERROR( ), MESSAGE( )
可能出错的语句
......
ON ERROR
其它语句
......
PROCEDURE errhand &&专门编写的出错处理过程
PARAMETER errnum,message
处理错误的语句
RETURN
首先注意到上面ON ERROR出现了两次,第二次出现不带有任何语句,这是告诉系统停止我们自己设置的出错处理陷阱,交由系统处理后续可能的错误。这涉及到我
们设置陷阱的有效范围问题,上面代码表明,陷阱有效范围从第一个ON ERROR开始到第二个ON ERROR结束。这之间任何语句出错,都会“落入陷阱”处理,但不会
理会第二个ON ERROR后面的出错(老狐狸接管了)。那么如果没有第二个ON ERROR结束,有效范围到哪里?回答是:如果后面又自行设置了第2个陷阱,则到第二陷阱
设置语句之前;否则到本程序过程结束。
情况1
ON ERROR DO errhand WITH ERROR( ), MESSAGE( )
可能出错的语句
......
其它语句
......
过程最后一条语句 &&陷阱到这里结束
情况2
ON ERROR DO errhand1 WITH ERROR( ), MESSAGE( )
可能出错的语句
......
某条语句 &&陷阱1到这里结束,下面开始陷阱2
ON ERROR DO errhand2 WITH ERROR( ), MESSAGE( )
可能出错的语句
......
另外,如果陷阱范围内调用了下级过程,则被调用过程中出错也会被该陷阱捕获(如果被调用过程中没有自行设置陷阱的话)。
这样看来我们的Click事件代码应该这样写(不过因为我们要实现的功能很简单,初始代码也简单,所以上面列出的就可以了):
nErrCode=0
ON ERROR nErrCode=ERROR() &&陷阱开始
USE Test.dbf IN 0
ON ERROR &&陷阱结束
DO CASE
CASE nErrCode=0 &&正常打开表
Select Test
Browse NOWAIT
CASE nErrCode=1 &&表文件不存在
?"表文件不存在"
CASE nErrCode=3 &&表已经打开
SELECT Test
Browse NOWAIT
OTHERWISE
?"表文件损坏或其它未知错误"
ENDCASE
接下来我们看一个特殊事件(Error事件),这是对象特有的,当对象执行某个事件代码出错时就会产生。因为我们例子中的代码是按钮完成的,因此针对本例还可以
这样做:
Command1的Click事件代码:
nErrCode=0
USE Test.dbf IN 0
DO CASE
CASE nErrCode=0 &&正常打开表
Select Test
Browse NOWAIT
CASE nErrCode=1 &&表文件不存在
?"表文件不存在"
CASE nErrCode=3 &&表已经打开
SELECT Test
Browse NOWAIT
OTHERWISE
?"表文件损坏或其它未知错误"
ENDCASE
Command1的Error事件代码:
LPARAMETERS nError, cMethod, nLine
nErrCode=nError
Error事件的参数nError就是错误码,cMethod是出错的过程名,nLine是出错语句行号,后两个本例没用上。
请不要在本例中同时使用这两种方式(既用ON ERROR又用Error事件),事件过程如果有陷阱,就不会触发Error事件。
系列二谈VFP8开始引进的意外处理