.NET中的Exception处理(C#)

摘要:

本文以C#为编程语言,讨论了 .NET 中的异常处理方式,主要包括 try/catch 块、finally语句、Exception 对象、throw语句等主题。


本文内容

  • 理解异常的基本概念
  • 使用 try/catch 块处理异常
  • 理解finally的意义
  • 使用 Exception 对象确定异常
  • 将异常返回过程调用程序

基本概念

就像其他面向对象语言一样,C#采用异常(exception)来应对程序错误和非正常情况。

异常是包含程序非正常事件信息的对象。与缺陷(bug)不同,一个bug是程序员的疏漏,它们应该在产品发布前被更正;尽管一个bug可能引发异常的抛出,你不应该完全依靠异常来处理你的bug,它至多是你测试的手段,你应该自己更正哪些bug。类似的,错误(error)是由用户操作而引起,比如在一个应该输入字母的地方用户输入了一个数字;虽然它也可能引发异常,但你应该通过校验代码(validation code)来抓住这些错误。无论何时,在可能的情况下错误都应该是能预料和能被预防的。即使你除去了所有的bug和列举了所有可能的用户错误,你仍会遇到无法预料和阻止的异常,如内存耗尽、网络崩溃。你无法预防异常,但你能处理它们,以避免它们使你的程序崩溃。

当你的程序遇到一个非正常情况,比如说内存不足,它就会引发(throw/raise)一个异常。此时,当前的过程调用将挂起,.NET 运行时(CLR)将从下至上搜索过程调用堆栈,以查找相应的异常处理程序。也就是说,如果抛出异常的代码正处于某个 Try 块中,运行时将首先使用本地的 Catch 块(如果有)来处理异常(它将执行在该位置找到的 Catch 块代码),否则这个程序段将被终止并将异常的处理权交给其调用函数;如果没有函数处理此异常(即在整个调用堆栈中没能找到适当的 Catch 块),最终运行时将会得到并处理它,并立刻将你的程序终止。


引发错误

示例为一个简单的文件打开操作并检索其长度的程序(以后几个示例的内容基本相同),示例从窗体文本框textBoxfilepath中得到文件名:

string filepath=this.textBoxfilepath.Text;
long isize;
FileStream fs=File.Open(filepath,FileMode.Open);
isize=fs.Length;
fs.Close();

有许多原因会使代码引发异常,如文件不存在,访问权限不够等等。在现在这种没有异常处理的情况下,运行时发生的任何错误会回溯到.NET运行时(CLR);而运行时会呈现给用户一个让人费解并可能造成危险的对话框(图1)。为了避免出现此对话框,如果发生运行时错误,您至少需要向顶层过程添加异常处理,并在必要时在下层过程中也添加异常处理。

图1:包含的Continue按钮使 .NET 默认错误处理程序变得有些危险
图1:包含的Continue按钮使 .NET 默认错误处理程序变得有些危险。

此外,其中的详细信息并不是您希望用户看到的内容。


添加简单try/catch块处理异常

在C#中,为了恰当地处理运行时异常,在要需要保护的任何代码附近添加一个try/catch块。在 try 块的代码中发生的任何运行时异常都将立即使用 catch 块中的代码继续执行:

try
{
    string filepath=this.textBoxfilepath.Text;
    long isize;
    FileStream fs=File.Open(filepath,FileMode.Open);
    isize=fs.Length;
    fs.Close();
}
catch
{
    MessageBox.Show("error occured!");
}

这段代码运行时,当有异常出现(如文件不存在)程序不会显示图2的对话框,取而代之的是一条简单的"error occurred!"警告,因为try块捕获住了异常并立即转到catch块中的代码继续执行。

注意:如果catch块中没有退出的代码(如return,throw),catch块后的代码将继续得到执行。并且try 块后面至少需要包含一个 catch 块(有关包含多个 catch 块的详细信息,请参阅下文)或一个finally块(参阅下文)。


处理特定异常(多个catch块)

.NET 框架提供了大量的特定异常类,所有这些异常类都是从基类 Exception 类继承而来的。在 .NET 框架文档中,您会看到一些表,它们列出了在调用任何方法时都可能出现的所有异常。图 2便列出了.NET中File.Open()方法调用时可能发生的所有异常情况:

图 2: File.Open 可能发生的所有异常
图 2: File.Open 可能发生的所有异常

你可以在一个try块后添加足够多的 Catch 块,以便对不同的异常情况作出不同的处理。以下的代码就列举了对几个不同的异常进行不同处理的情况。

try
{
    string filepath=this.textBoxfilepath.Text;
    long isize;
    FileStream fs=File.Open(filepath,FileMode.Open);
    isize=fs.Length;
    fs.Close();
}
catch(UnauthorizedAccessException uex)
{
    MessageBox.Show(uex.Message);
}
catch(FileNotFoundException fex)
{
    MessageBox.Show(fex.Message);
}
catch(NotSupportedException nex)
{
    MessageBox.Show(nex.Message);
}
catch(ArgumentException aex)
{
    MessageBox.Show(aex.Message);
}
catch
{
    MessageBox.Show("error occured!");
}

注意:catch块的次序必须十分小心,比如一个DivideByZeroException 异常继承自ArithmeticException异常,如果你先捕获后者,则当除数为0时抛出的异常就会进入ArithmeticException块而永远不会进入DivideByZeroException块。事实上,当这种情况出现时,编译器会发现DivideByZeroException块不能被执行到,并会报告一个编译错误。


Finally关键字

除了 trycatch 块中的代码外,有时你还需要添加无论何种情况下都会被执行到的代码,比如你可能需要释放一些资源、关闭一个文件等等,这是就需要使用Finally 关键字在 Catch 块后添加 Finally 块。即使代码抛出异常,并在Catch 块中添加了显式的return语句,finally块中的代码仍会被执行。Finally 块中的代码将在异常处理代码之后、控制返回到调用过程之前执行。

注意:finally 块只需要一个 try 块,catch 块的存在与否对其并没有影响。使用break,continue,return语句退出 finally 块都是非法的。 我们对上面的示例进行如下修改,以使任何情况下都可以调用结束代码,关闭可能打开了的文件:

FileStream fs=null;
try
{
    string filepath=this.textBoxfilepath.Text;
    long isize;
    fs=File.Open(filepath,FileMode.Open);
    isize=fs.Length;
}
catch(FileNotFoundException fex)
{
    MessageBox.Show(fex.Message);
}
catch
{
    MessageBox.Show("error occured!");
}
finally
{
    //无论发生什么情况,下面的代码都将被执行到
    if(null!=fs)
    {
        fs.Close();
    }
}

Throw 关键字

在C#中,要通知一个非正常情况,你可以使用 throw 关键字抛出一个异常。下面一行代码创建一个新的System.Exception实例,并将它抛出:

throw new System.Exception();

抛出的异常和所有自然引发的异常一样,立即将代码段挂起,并由CLR寻找一个异常处理者。如果要截取不同的异常并将它们作为单个异常类型全部返回到调用程序,使用 Throw 语句可以非常轻松地完成此操作。比如有一段代码捕获所有异常,而且无论导致异常的原因是什么,都只抛出一个 FileNotFoundException 对象给调用程序,因为实际的过程调用者可能并不关心实际发生的事情,也不关心为什么无法找到文件,他只关心该文件是否可用,并且需要从其他不同的异常中辨别该特定异常。


Exception对象

到目前为止我们一直使用Exception作为错误的信号,但未真正接触Exception对象本身,接着我们就来讨论这个问题。

图3:Exception 类的公共成员
图3:Exception 类的公共成员

图3是Exception类的公共成员变量。其中 Message 只读属性提供了关于这个异常的信息,比如为什么它被抛出,抛出异常的代码能在Exception构造函数中设定 Message 属性值。HelpLink 属性提供了一个此异常帮助文件的一个链接。而 StackTrace 只读属性是在运行时被设置的。在下面的例子中,Exception.HelpLink 属性被设置以提供用户关于DivideByZeroException的帮助,而异常的 StackTrace属性提供一个对错误语句的运行栈追踪,显示了堆栈信息和导致异常抛出的一系列方法调用。

public class Test
{
    public static void Main()
    {
        Test t = new Test();
        t.TestFunc();
    }
    // try to divide two numbers
    // handle possible exceptions
    public void TestFunc()
    {
        try
        {
            Console.WriteLine("Open file here");
            double a = 12;
            double b = 0;
            Console.WriteLine("{0} / {1} = {2}", a, b, DoDivide(a, b));
            Console.WriteLine("This line may or may not print");
        }
        // most derived exception type first
        catch (System.DivideByZeroException e)
        {
            Console.WriteLine("\nDivideByZeroException! Msg: {0}", e.Message);
            Console.WriteLine("\nHelpLink: {0}", e.HelpLink);
            Console.WriteLine("\nHere's a stack trace: {0}\n", e.StackTrace);
        }
        catch
        {
            Console.WriteLine("Unknown exception caught");
        }
        finally
        {
            Console.WriteLine("Close file here.");
        }
    }
    // do the division if legal
    public double DoDivide(double a, double b)
    {
        if (b == 0)
        {
            DivideByZeroException e = new DivideByZeroException();
            e.HelpLink = "http://www.libertyassociates.com";
            throw e;
        }
        if (a == 0)
        {
            throw new ArithmeticException();
            return a / b;
        }
    }
}

Output:

Open file here
DivideByZeroException! Msg: Attempted to divide by zero.
HelpLink: http://www.libertyassociates.com
Here's a stack trace:
at Programming_CSharp.Test.DoDivide(Double a, Double b)
in c:\...exception06.cs:line 56
at Programming_CSharp.Test.TestFunc( )
in...exception06.cs:line 22
Close file here.

在输出中,stack trace反向列出了被调用的方法,显示错误在DoDivide( )中发生,而此函数被TestFunc()调用。当多种方法纠缠在一起,stack trace能帮助你理清方法调用的次序。
在异常被抛出之前,你可以设置 HelpLink 属性:

e.HelpLink = "http://www.libertyassociates.com";

这让你能为用户提供有用的信息。


小结

使用一个 Try/Catch 块可以向代码段添加异常处理。
.NET 运行时可以依次处理 Catch 块,它将使用找到的第一个匹配块。
您可以嵌套 Try 块,以便轻松而有效地推入和弹出异常处理状态。
在 Try 块后添加一个 Finally 块,这样无论发生什么,都可以无条件运行代码。


作者:赵彦

Contributors: FHL