回 帖 发 新 帖 刷新版面

主题:错误,让我们抓住它!(系列)

错误,让我们抓住它!(系列一)  根据2楼提醒更正并重新排版

我们编程不管多么仔细,总还是难免在代码中留下隐患(或者说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开始引进的意外处理

回复列表 (共11个回复)

沙发

发现搂主的一个问题,Browsw可能不在Test.dbf的工作区

IF USED("Test")         
   SELECT Test          
ELSE 
   USE Test.dbf IN 0    
ENDIF   
BROWSE NOWAIT 

上面的程序有问题,应该写为

IF USED("Test")
   SELECT Test          
ELSE 
   USE Test.dbf IN 0    
ENDIF  
************************
*  加一句,才是正确的
sele("Test")
* ***********************
BROWSE NOWAIT

板凳

我都是这样写的
IF .not.USED("Test")
  USE Test IN 0    
ENDIF  
sele("Test")
*  如果必要,再指定索引 set orde to XXXXX
..........

当你的软件是一个复杂的工程时,必须养成这种书写习惯,这样你可以避免去考虑是否打开表的烦杂问题,提高软件的稳定性。

3 楼

呵呵,乌鸦兄指正的对。

4 楼

错误,让我们抓住它!(系列二)

意外处理(Exception Handling)

Fox用TRY...CATCH...FINALLY结构进行意外处理,这是从8版本开始引入的,其实C++早就有了。不管372十1先尝试用这个结构的最简形式来写我们的Click事件代码:

TRY 
   USE Test.dbf IN 0
   SELECT Test
   BROWSE NOWAIT
CATCH
ENDTRY 

上面TRY和CATCH之间写入了语句(称为TRY块),这些是可能会出错的语句,当然了我们知道USE语句最可能出错,TRY结构先顺序执行这些语句。上面代码如果USE语句有错误发生,系统立刻停止后续命令的执行,转到CATCH,不过CATCH后面没有语句,则忽略错误,转到ENDTRY后继续(这里ENDTRY后没有语句了,Click事件过程也就结束了)。
仅仅这样写,坦白地说,的确避免了系列一中列出的5个隐患,不过同样没有完全实现我们想要的功能,就像系列一中我们第一次用ON ERROR实现的语句一样。
当Test.dbf正常的情况下,我们先改写一下:

TRY 
   USE Test.dbf IN 0
   SELECT Test
   BROWSE NOWAIT
CATCH
   SELECT Test
   BROWSE NOWAIT
ENDTRY 

这回使用了CATCH语句,CATCH后有2条语句(称为CATCH块),当TRY块有错误发生,系统立刻停止TRY块后续语句的执行,并且系统发现有CATCH块,就立刻执行CATCH块的语句。我们写这段代码是在Test.dbf正常的情况下,那么正常情况下,执行完TRY块,就转到ENDTRY后继续;出错的情况只能是USE之前表已经打开了,那么系统执行CATCH块,正好。执行完CATCH块转到ENDTRY后继续。

如果表不存在或者表文件损坏引起出错会怎么样?同样会转入CATCH块,这时CATCH块就会出错。该块中语句没有考虑出错处理,最终由老狐狸弹出错误对话框。我们可以有多种措施挽救这种情况下的出错。
措施1:
TRY 
   USE Test.dbf IN 0
   SELECT Test
   BROWSE NOWAIT
CATCH
   TRY 
     SELECT Test
     BROWSE NOWAIT
   CATCH 
   ENDTRY 
ENDTRY
这里嵌套使用了Try结构,对外层CATCH块的语句又用Try结构处理,如果表不存在或者表文件损坏引起出错,流程从外层Try块转入外层CATCH块,从而进入内层Try结构,如果内层Try块出错,则转入内层CATCH块。注意到这个内层CATCH块没有任何语句,则系统忽略内层Try块的错误。特别注意,不能省略这个内层CATCH,否则仍然由老狐狸接管错误,弹出错误对话框。

措施2:
TRY 
   USE Test.dbf IN 0
   SELECT Test
   BROWSE NOWAIT
CATCH TO oException    &&oException是个对象变量
   DO CASE
   CASE oException.ErrorNo=1    &&表文件不存在
     ?"表文件不存在"
   CASE oException.ErrorNo=3    &&表已经打开
     SELECT Test
     Browse NOWAIT
   OTHERWISE
      ?"表文件损坏或其它未知错误"
   ENDCASE
ENDTRY
这次利用了Exception(意外)对象,当TRY块出错,老狐狸发现CATCH语句后有TO <变量>就会生成一个Exception对象,用变量保存对该对象的引用。这个对象有个ErrorNo属性保存错误码,于是在CATCH块中我们检查这个属性来决定进一步的处理。该对象其它属性请参考Help。

措施3:
TRY 
   USE Test.dbf IN 0
   SELECT Test
   BROWSE NOWAIT
CATCH TO oException WHEN oException.ErrorNo=1 &&表文件不存在
   ?"表文件不存在"
CATCH TO oException WHEN oException.ErrorNo=3 &&表已经打开
   SELECT Test
   Browse NOWAIT
CATCH TO oException
   ?"表文件损坏或其它未知错误"
ENDTRY
这次使用了带有WHEN子句的多CATCH结构,当TRY块出错,老狐狸一一检查每个CATCH后的WHEN条件,只要为真,立刻转入该CATCH块,不再理会其它CATCH块。这很类似措施2的DO CASE结构。

由于本例的限制,我们没有用到FINALLY语句(FINALLY块),如果有该块,无论Try块有无错误,该块都会执行。这很容易让我们产生错觉,认为该块没有必要,如果把语句写在ENDTRY后面,不也一样吗?呵呵,错。举个例子:
Clear
Try
   Try 
      ?"这是Try块"
      x=x+1        &&故意用语句产生错误(变量不存在)
   FINALLY
      ?"这是FINALLY块"
   ENDTRY
   ?"内层TRY结束"
CATCH
   ?"出错了"
ENDTRY

你认为会打印出"内层TRY结束"这个句子吗?不会,因为内层Try结构不含CATCH,错误交由外层Try结构的CATCH块处理,不过因为内层有FINALLY块,在转向外层CATCH块之前会执行FINALLY块。    

本贴不能一下子把TRY...CATCH...FINALLY结构讲完,它还有更多的方面,请参考网上资料。

至此未雨绸缪和亡羊补牢基本上说完了,我们通过一个简单的例子,独立地谈了这两个方面,事实上应该综合应用两种策略。所举的例子只是为了描述问题,不一定符合你的要求,若大家能从中有所启发,便是最好的结果。

最后说个无法亡羊补牢的例子,除0计算,比如:
n=0
m=1/n

无论ON ERROR还是TRY,都不行,上面第二个句子不产生任何错误,用TYPE()函数测试m,得到数值型,应该是个很大很大的数,呵呵。只能未雨绸缪,先if判断n了。
隐约记得一位朋友发帖说表不能replace了,之前都好好的,回帖的朋友发现表中有数值型字段内容是“*********”,俺怀疑曾经有除0计算的值送进表中。这样的错误很隐蔽。

5 楼


     受教了!老师最后关于0的运算,我也曾经在CSDN发贴过:
[color=FF0000]k=0 
?1/k=******        注:6个* 
?1111/k=********    注:9个* 
这些*号代表的是什么?因为k是变量,一旦k=0,数值就会溢出,程序出错,当一位数除零给我6个星号,当四位数除零给我9个星号,这电脑也在作无厘头运算吧![/color]
这个错误确实很隐蔽。我知道IlikeFox在应用SQL语句方面是顶尖高手,而分析问题又是如此精彩,真想知道IlikeFox是个什么样的人,什么职业的。记得2007年刚步入本论坛,还在为如何写路径犯难的时候得到过IlikeFox的指点,记忆犹新。
    谢谢楼主的精彩博文!

6 楼

精彩好贴,虽然时间很久了,还是要顶一下。

7 楼

很好吗,项一下。这种东东教材上是不会有的,是个人编程的总结。

8 楼


精彩极了!! 听君一席话,胜读十年书!致敬

9 楼

哈哈,还提到了菜鸟我啊

我是学习论坛里的老师,照样画葫芦的

这个论坛的确很好

能有这么多热心的老师

真的非常感谢!

向楼主、MOZ、种子、乌鸦、cl518等老师致敬!

10 楼

好贴!!!曾经写程序,老提示文件正在使用。总爱在程序开头close all 一句。
如果早点看到赵老师的文章,就不会走那么多弯路了。

另外,提个问题,如果replace 字段1 with 字段2+字段3...如何让字段1大于某个数时,就提示自定义的错误。比如字段1大于1000就提示超过满分。

我来回复

您尚未登录,请登录后再回复。点此登录或注册