现在的位置: 首页 > 综合 > 正文

错误和异常:不灭的油灯

2014年09月13日 ⁄ 综合 ⁄ 共 7888字 ⁄ 字号 评论关闭

错误和异常:不灭的油灯

周融
(C) 2006 保留所有权利。

本文内容

在应用程序开发日趋庞大和频繁的 IT 业界,什么东西是永恒的?也许很多人要说是市场,是技术,或者是人力资源。但本文以一种独特的观点向您阐述什么是最终永恒的。您可能无法相信,人们所不希望看到的“错误”变成了 IT 业界永不磨灭的东西,而且,正是有了这个,我们才会把我们的产品做得更好。

本文从软件开发的角度,介绍错误和异常的有关概念,并结合一些资料和作者的相关经验,说明如何正确的处理发生的错误和异常,并利用错误和异常指导开发人员不断努力的完善自己的软件或产品。

在本文中将介绍:
    ·错误和异常
    ·什么是错误和异常
    ·异常处理程序
    ·异常的一般分类
    ·使用结构化异常处理
    ·异常常见问题
    ·总结

一、错误和异常

让我们先来熟悉一下我们每天发生的事情吧!每天早上,当我们还沉醉在美丽的梦中,尽情地享受愉快梦境的时候,该死的闹钟响了;我们不得不拎起衣襟快马加鞭——因为上班时间到了,要起床了。等到头脑清醒之后,下意识的看了一下时间,God!今天周六!原来是看错了时间。好了,继续睡觉...

中午起来,精神很好,因为“一觉睡到自然醒(这可是很多人梦寐以求的理想)”,不过,正要去洗漱的时候,发现停水了...

晚上回到宿舍准备睡觉,突然发现手机的皮套不翼而飞...

这就是我的荒唐的一天。这一天发生了很多意想不到的事情:停水、丢东西、甚至看错表。其实这不能全部抱怨我,因为看错表是我不对,脑子不好使了;不过停水了我也确实没有办法,而且事先没有做好准备。于是导致这一天“相当”的“郁闷”,要是每一天都能避免这些问题,那该有多好!

试者用我们敏锐的富有开发天赋的脑子想一下,如果我们写一个程序,或者做一个产品,会不会产生这些问题?会不会有敲错代码的时候?会不会有意想不到的情况?——当然会有!从一个专业角度来看,可以给以上两个案例分别赋予它们好听的名字:错误和异常。

二、什么是错误和异常

错误,是没有能够避免的由于主观因素而发生的与预期行为不相符合的案例。例如,我希望写“登录”,但由于微软拼音输入法的习惯排列,我写成了“登陆”,这可以视为一个错误。因为这个错误是由于我自己疏忽,没有从候选词条中选择合适的词组造成的;异常则是因为客观因素导致的一些无法预料的案例而导致实际行为与预期行为不相符合的情况。例如:当我书写这篇文章时突然当机,这不是我自己的问题,而是不可避免的计算机故障,因此,我们可以将这个案例视为一个异常。

错误是可以消除和避免的。因为错误的产生原因是主观的,而主观的行为是可以被决策的。因此,我们应该在错误发生之前就意识到并阻止它。正如意识到自己的错误并展开“自我批评”一样。

异常是不可避免的,因为它不由我们决定和决策。但是我们可以预先做好发生异常的准备,并提前预防异常的发生。这正例如一个聪明的女士如果要避免被无情的抛弃,她必定会对自己深爱的男士进行跟踪和实时监测,而一旦发生离婚异常,女士也会利用事先的准备继续生活下去。

可见,错误需要仔细检查,严格避免它的产生;异常只能预防,一旦发生后,还可以利用预先的准备进行处理。

三、异常处理程序

异常发生互的处理过程在软件设计中被定义为异常处理程序,或称为异常处理句柄(Exception Handler)。这一段代码往往是一个函数,用来处理当指定的异常发生后程序的行为。一旦异常发生,操作系统或者应用程序就可以定位异常,并转到它对应的异常处理程序执行;当异常处理完毕后,再返回处理异常前的入口点继续执行原来的代码。这个模型和 Windows 消息泵机制是完全类似的。

引入异常处理程序的概念后,我们发现现在需要解决如下的几个问题:
1、操作系统或者应用程序如何定位异常;
2、应用程序如何“捕捉”它们需要的异常;
3、异常信息如何传递到异常处理程序;
4、如何解决多个异常的并发问题。

这几个问题在引入了结构化异常处理机制以后已经全部解决。

四、异常的一般分类

异常可以是有继承关系的,也可以是同一时间并发的,它具备并发性和多样性。因此,人们利用面向对象的机制描述异常类。在这个模型下,人们一般将异常分为如下几个类型。

·异常 (Exception)
这是所有异常的基类,任何一个异常需要从此异常类继承并构造实例。

·系统异常 (SystemException)
系统异常往往用来描述操作系统发生的异常。例如文件不存在,内存不足等。

·应用程序异常 (ApplicationException)
在 SystemException 的托管(Managed)下,为不同俄进程引发应用程序异常。

·非法操作异常 (InvalidOperationException)
此异常隶属于单个应用程序,当在某一个应用程序中发生非法操作(例如:空指针、Access Validation 等),应用程序可以在它自己的堆中托管这些异常。

·其他异常
诸如被零除、格式转换、正则表达式、对象引用等异常,这些异常通常由编写这些应用程序的主语言自动捕获和处理。例如以下几个异常是在其各自的语言中最常用的。

  主语言                       异常
----------------------------------------------------------------------------
  Delphi                        Access Validation
  Visual Basic 2005        InvalidOperationException, UnhandledException
  Visual C# 2005           InvalidOperationException, UnhandledException
  Visual C++ 2005         AssertFailureException
  Java                           InvalidOperationException

一般来说,异常都是按照如下形式继承和传递的。

  Exception
    -- SystemException
      -- ApplicationException
        -- InvalidOperationException
        -- UnhandledException
        -- IOException
          -- FileNotFoundException

可见, 异常的发生也是由先后顺序的。捕获异常也是一样。异常总是被更下一层的类捕获,例如,如果同时在代码中捕获了 IOException 和 FileNotFoundException,那么可能将执行代码路径 2。

  try
  
{
    System.IO.File.CreateText(
"c:/a.txt");
  }

  
catch(IOException ex1)
  
{ ...}  // Path 1
  catch(FileNotFoundException ex2)
  
{ ... } // Path 2
  finally
  
{
    System.GC.Release;
  }

这就要求在书写代码时正确的捕获异常。下面,我们来讨论结构化异常处理。

五、结构化异常处理

*程序必须能够统一处理在执行期间发生的错误。现在几乎所有的开发平台都提供了一个模型,以统一的方式通知程序发生的错误,从而为设计容错软件提供了极大的帮助。所有的操作都通过引发异常来指示出现错误。

传统上,语言的错误处理模型依赖于语言检测错误和查找错误处理程序的独特方式,或者依赖于操作系统提供的错误处理机制。有一些运行库(如 .NET Framework)实现的异常处理具有以下特点:

·处理异常时不用考虑生成异常的语言或处理异常的语言。

·异常处理时不要求任何特定的语言语法,而是允许每种语言定义自己的语法。

·允许跨进程甚至跨计算机边界引发异常。

与其他错误通知方法(如返回代码)相比,异常具有若干优点。不再有出现错误而不被人注意的情况。无效值不会继续在系统中传播。不必检查返回代码。可以轻松添加异常处理代码,以增加程序的可靠性。最后,有些运行库的异常处理比基于 Windows 的 C++ 错误处理更快。

对于 .NET Framework:
由于执行线程例行地遍历托管代码块和非托管代码块,因此运行库可以在托管代码或非托管代码中引发或捕捉异常。非托管代码可以同时包含 C++ 样式的 SEH 异常和基于 COM 的 HRESULT。*

结构化异常处理的关键:引发异常,捕获异常和处理异常。

1. 引发异常

异常总是通过程序本身引发的,并没有一个自动的机制来处理它们,对于操作系统来说也是如此。因此,引发异常的工作由人们来完成。这个过程可以叫做“引发(Raise)”异常或者“抛出(Throw)”异常,程序在适当的地方应该引发异常。来看看这个例子:

  Delphi
  procedure ValidateInputCode(code: string);
  begin
    if (code = '' ) or (TryStrToInt(code) = 0) then
      raise Exception.Create('Input code is not a valid number.');
  end;

  Visual Basic

  Public Sub ValidateInputCode(ByVal code As String)
    
If code.EqualTo(String.Empty) Or TryCast(code, Integer= Nothing Then
      
Throw New Exception("Input code is not a valid number")
    
End If
  
End Sub

当我们判断一个输入码的合法性时,如果代码为空,或代码不是数字,那么就引发“代码非法”的异常。

您可能会问,如果我将引发异常的一行改为提示错误对话框,不是更好?

  Delphi
  procedure ValidateInputCode(code: string);
  begin
    if (code = '' ) or (TryStrToInt(code) = 0) then
      MessageBox(0, 'Input code is not a valid number.', nil, MB_OK or MB_ICONEXCLAMATION);
  end;

  Visual Basic

  Public Sub ValidateInputCode(ByVal code As String)
    
If code.EqualTo(String.Empty) Or TryCast(code, Integer= Nothing Then
      MessageBox.Show(
"Input code is not a valid number.", MessageBoxButtons.OK, MessageBoxIcons.Exclamation)
    
End If
  
End Sub

这样做在发生异常时给出了信息,是一种比较好的用户体验,但是会存在以下问题:
·异常只能在这个局部代码中处理,不会有异常处理程序的干涉;
·开发平台没有为这个异常建立异常信息表,因此无法利用统一异常处理模型处理它们;
·这个异常不会在进程中传播;
·如果每一个异常都这样处理,则应用程序不能利用结构化异常处理模型高效、快速、规范的处理所有引发的异常。

这些问题可能带来直接的后果。
·无法启用操作系统级别的异常跟踪,例如错误报告和事件日志;
·非法的取值在程序中传播;
·异常处理的效率很低,代码重用率低;
·没有比较统一的 UI 和处理模型;
·同一应用程序的不同模块或者其他应用程序无法跟踪异常,异常将潜在威胁应用程序。

如果改用引发异常机制,则可以让此应用程序统一处理异常,或让操作系统进行跟踪和错误报告,这对于一个优秀的应用程序是必须的。

2. 捕获异常和处理异常

异常发生后需要应用程序或者操作系统捕获(Catch)。异常的捕获通常使用结构化异常处理语句 Try...Catch...Finally 来进行,将可能导致异常的代码放在 Try...Catch 之间,将捕获和处理的代码放在 Catch...Finally 之间,将最后的清理代码放在 Finally 块中。例如:

  Delphi
  procedure ButtonOKClick;
  var
    i: Integer;
  begin
    try
      i := listLength div count;
    except
      on Exception do
      begin
        i := 0;
        MessageBox(0, 'Error: Divided by zero.', nil, MB_OK or MB_ICONERROR);
      end;
    end;
  end;

  Visual Basic

  Public Sub Button_Click(ByVal sender As ObjectByVal e As EventArgs) Handles ButtonOK.Click
    
Try    
      
Dim i = listLength / count
    
Catch ex As DividedByZeroException
      i 
= 0
      MessageBox.Show(ex.Description.ToString)
    
End Try
  
End Sub

看看上面的代码,如果 listLength / count 发生异常(事实上很容易发生异常,如果列表为空,则 count = 0,就会发生被零除异常),程序就会转到 catch 块执行,将 i 赋予合适的值之后结束。

一个 Try 结构可以捕获多个异常,根据异常类型的不同分别处理。这里不举例了。

Finally 块在异常处理之后运行,不管有没有发生异常,Finally 都会执行,IT 开发人员可以利用这个特性实现一些特殊逻辑,例如:当进行数据库操作时,无论异常是否发生,最后应该关闭连接。例如:

  Try
    
Dim conn As System.Data.SqlClient.SqlConnection
    
' TODO: operation code here...
  Catch ex As SqlException
    
MsgBox(ex.Description, MsgBoxButton.OKOnly)
  
Finally
    conn.Close
  
End Try

不过,现在有一种全新的语言特色诞生,那就是使用 Using 来动态分配对象变量的内存,当对象变量不再使用时自动进行清理。这一语句并不是所有的语言都支持。

异常处理还可以嵌套,实现更为复杂的异常处理。虽然 Delphi 7 没有实现 try...except...finally 的构造,但是,可以利用两个 try 嵌套实现这个功能。

  try
    try
      ...
    except
      ...
    end;
  finally
    ...
  end;

但不合理的应用桥套会造成一些莫名其妙的错误。这一点将在下一节阐述。

六、异常处理常见问题

Q: 什么时候使用 MessageBox,什么时候使用 try 结构处理异常?
A: 其实 MessageBox 与 try 没有必然的联系,使用信息框处理异常时,异常在局部被处理,但不会引发和传播,以至于结构化异常处理失效;使用 try 结构目的在于对所有的异常进行统一的处理,利用结构化异常处理的优势(参见第四节)处理异常,这样编写的程序更加有利于移植和容错,与操作系统结合更加紧密。不过,有一些程序员会把的 Exception 代替 MessageBox 使用,造成某一些不该发生的异常;而另一些则把 MessageBox 大量的代替 except 结构,导致异常无法正确传递,出现了很多莫名其妙的错误。所以我建议您根据情况,仔细研究一下什么地方使用消息框,

Q: 我在使用了结构化异常处理后,一些代码路径被终止运行。
A: 这是因为您使用 try 结构的类有潜逃的 try 结构,而且在更内层的 try 结构中异常处理程序没有正确的捕获异常和处理它们,我们看看这种情况:

  try
    myColumnName := Grid.SelRowFieldByName('abc').AsString;
    MessageBox(0, PChar(myColumnName), nil, 0);
  except
    MessageBox(0, 'value not found', nil, 0);
  end;

  而在类 Grid 的 SelRowFieldByName 中,异常处理这样被定义

  try
    // Get the field value by name.
  except
    // ...
    Exit;
  end;

那么,当这一行代码被执行时,如果域 abc 不存在,那么将导致异常发生,按道理会执行 except 块,显示 value not found 的提示框,但是,当我们编译这段代码并执行时,消息框并没有产生,而是直接结束了整个 try 结构,或者更糟糕一点,整个过程都被终止。

出现这种情况,只能使用两种方法,一是处理基类,使用正确的异常处理,不要使用 Abort 和 Exit 函数;如果没有办法处理基类,则可以通过 MessageBox 代替 try 结构,虽然丧失了结构化的异常处理功能,但可以解决这个问题,我不推荐这样做,除非您认为十分必要这么做。

Q: 我有必要继承 Exception 类吗?
A: 很有必要。因为不同的异常有不同的处理方式,继承 Exception 类可以定制各种异常的处理方法。我建议您将应用程序中的所有异常归类,并尽量使用继承的异常类捕获异常。如果一些异常没有明确的分类,则苛以使用 Exception 类直接处理。

Q: Access Validation 异常经常会发生,是什么原因?
A: Access Validation(访问冲突)是 Delphi 开发中常见的异常,它是一个基本异常,因此很多操作都有可能导致此异常的发生,最有可能的一些操作有:
1. 未将对象变量引用到对象的实例。(没有显式的执行对象构造函数);
2. 程序中未处理的异常;
3. Win32 API 或 COM 调用错误,但未捕获;
4. Delphi VCL 本身的异常;
当然,发生 AV 错误最多的,还是 1 和 2 两个案例。为了避免这些异常,您可以:
·在每一个对象变量被引用时,执行 Create 和 Free;
·每一个可能引发 COM 异常的地方使用 Try,并通过消息框将异常的 Message 属性显示出来;
·对于每一个 Win32 API 调用,检查 GetLastError 的值;
·使用 ApplicationEvents 组件处理所有未知的和未处理的应用程序异常。

七、小结

异常处理是一个永不磨灭的话题,因为它始终会存在,但是,就是因为异常的存在,IT 开发人员可以让它指引软件的开发过程和测试。错误和异常就像是传说中不灭的油灯,一方面消灭不了,一方面有给予指引和光明。

程序总是有无数个 Bug 和 Debug 构成的,这一点毋庸置疑,捕捉每一个异常,让代码变得更加健壮,让人民生活的更加幸福。

 

 

 

抱歉!评论已关闭.