处理和引发异常
程序必须能够统一处理在执行期间发生的错误。公共语言运行库提供了一个平台,以统一的方式通知程序发生的错误,这样为设计容错软件提供了极大的帮助。所有的 .NET Framework 操作都通过引发异常来指示出现错误。传统上,语言的错误处理模型依赖于语言检测错误和查找错误处理程序的独特方法,或者依赖于操作系统提供的错误处理机制。运行库实现的异常处理具有下列特点:
1. 处理异常时不考虑生成异常的语言或处理异常的语言。
2. 异常处理时不要求任何特定的语言语法,而是允许每种语言定义自己的语法。
3. 允许跨进程甚至跨计算机边界引发异常。
与其他错误通知方法(如返回代码)相比,异常具有若干优点:
1. 不再有出现错误而不被人注意的情况。
2. 无效值不会继续在系统中传播。
3. 不必检查返回代码。
4. 可以轻松添加异常处理代码,以增加程序的可靠性。
5. 最后,运行库的异常处理比基于 Windows 的 C++ 错误处理更快。
由于执行线程例行地遍历托管代码块和非托管代码块,因此运行库可以在托管代码或非托管代码中引发或捕捉异常。非托管代码可以同时包含 C++ 样式的 SEH 异常和基于 COM 的 HRESULT。
异常概述
异常是程序执行时遇到的任何错误情况或意外行为。以下这些情况都可以引发异常:您的代码或调用的代码(如共享库)中有错误,操作系统资源不可用,公共语言运行库遇到意外情况(如无法验证代码),等等。对于这些情况,应用程序可以从其中一些恢复,而对于另一些,则不能恢复。尽管可以从大多数应用程序异常中恢复,但不能从大多数运行库异常中恢复。在 .NET Framework 中,异常是从 Exception 类类继承的对象。异常从发生问题的代码区域引发,然后沿堆栈向上传递,直到应用程序处理它或程序终止。
运行库如何管理异常
运行库使用基于异常对象和受保护代码块的异常处理模型。发生异常时,创建一个 Exception 对象来表示该异常。运行库为每个可执行文件创建一个异常信息表。在异常信息表中,可执行文件的每个方法都有一个关联的异常处理信息数组(可以为空)。数组中的每一项描述一个受保护的代码块、任何与该代码关联的异常筛选器和任何异常处理程序(Catch 语句)。此异常表非常有效,在没有发生异常时,在处理器时间或内存使用上没有性能损失。仅在异常发生时使用资源。
异常信息表对于受保护的块有四种类型的异常处理程序:
- Finally 处理程序,它在每次块退出时都执行,不论退出是由正常控制流引起的还是由未处理的异常引起的。
- 错误处理程序,它在异常发生时必须执行,但在正常控制流完成时不执行。
- 类型筛选的处理程序,它处理指定类或该类的任何派生类的任何异常。
- 用户筛选的处理程序,它运行用户指定的代码,来确定异常应由关联的处理程序处理还是应传递给下一个受保护的块。
每种语言根据自己的规范实现这些异常处理程序。例如,Visual Basic .NET 通过 Catch 语句中的变量比较(使用 When关键字)提供对用户筛选的处理程序的访问;C# 不实现用户筛选的处理程序。
异常发生时,运行库开始执行由下列两步组成的过程:
- 运行库在数组中搜索满足下列条件的第一个受保护块:
- 保护包含当前执行的指令的区域,而且
- 包含异常处理程序或包含处理异常的筛选器。
- 如果出现匹配项,运行库创建一个 Exception对象来描述该异常。然后运行库执行位于发生异常的语句与处理该异常的语句之间的所有 Finally 语句或错误处理语句。请注意,异常处理程序的顺序很重要:最里面的异常处理程序最先计算。还请注意,异常处理程序可以访问捕捉异常的例程的局部变量和本地内存,但引发异常时的任何中间值都会丢失。
如果当前方法中没有出现匹配项,则运行库搜索当前方法的每一个调用方,并沿着堆栈一直向上查找。如果任何调用方都没有匹配项,则运行库允许调试器访问该异常。如果调试器不能附加到该异常,则运行库引发 UnhandledException 事件。如果没有 UnhandledException事件的侦听器,则运行库转储堆栈跟踪并结束程序。
筛选运行库异常
可以按类型或按某些用户定义的条件对捕捉和处理的异常进行筛选。类型筛选的处理程序处理特定类型的异常(或从该异常派生的类)。最常见形式的类型筛选的异常处理程序指定仅捕捉特定类型的异常。下面的示例说明一个旨在捕捉特定异常(此例中为 FileNotFoundException)的异常处理程序。
catch(FileNotFoundException e) {
Console.WriteLine("[Data File Missing] {0}", e);
}
用户筛选的异常处理程序根据您为异常定义的要求捕捉和处理异常。这些处理程序使用 C++ 的托管扩展中的 try/except 块或 Visual Basic .NET 中带 When关键字的 Catch语句。
Exception 类
概述
Exception 类表示在应用程序执行期间发生的错误,是异常从其进行继承的基类。大多数异常对象都是Exception的某个派生类的实例,不过,任何从 Object 类派生的对象都可以作为异常引发。请注意,并非所有语言都支持引发和捕捉不从 Exception派生的对象。在几乎任何情况下,都建议仅引发和捕捉 Exception对象。
Exception类的若干属性使了解异常更容易。这些属性包括:
- StackTrace 属性。
此属性包含可用来确定错误发生位置的堆栈跟踪。如果有可用的调试信息,则堆栈跟踪包含源文件名和程序行号。
- InnerException 属性。
此属性可用来在异常处理过程中创建和保留一系列异常。可使用此属性创建一个新异常来包含以前捕捉的异常。原始异常可由 InnerException属性中的第二个异常捕获,这使处理第二个异常的代码可以检查附加信息。
例如,假设有一个读取文件并格式化相应数据的方法。代码试图从文件读取,但引发 FileException。该方法捕捉 FileException 并引发 BadFormatException。在此情况下,FileException 可保存在 BadFormatException 的 InnerException属性中。
为提高调用方确定异常引发原因的能力,有时可能需要方法捕捉帮助器例程引发的异常,然后引发一个进一步指示已发生的错误的异常。可以创建一个更有意义的新异常,其中内部异常引用可以设置为原始异常。然后可以针对调用方引发这种更有意义的异常。请注意,使用此功能,可以创建以最先引发的异常作为结束点的一系列相链接的异常。
- 示例
下面的示例演示引发和捕捉引用内部异常的异常。
using System;
public class MyAppException:ApplicationException
{
public MyAppException (String message) : base (message)
{}
public MyAppException (String message, Exception inner) : base(message,inner) {}
}
public class ExceptExample
{
public void ThrowInner ()
{
throw new MyAppException("ExceptExample inner exception");
}
public void CatchInner()
{
try
{
this.ThrowInner();
}
catch (Exception e)
{
throw new MyAppException("Error caused by trying ThrowInner.",e);
}
}
}
public class Test
{
public static void Main()
{
ExceptExample testInstance = new ExceptExample();
try
{
testInstance.CatchInner();
}
catch(Exception e)
{
Console.WriteLine ("In Main catch block. Caught: {0}", e.Message);
Console.WriteLine ("Inner Exception is {0}",e.InnerException);
}
}
}
MyAppException (String* message) : ApplicationException (message) {}
MyAppException (String* message, Exception* inner) : ApplicationException(message, inner) {}
void ThrowInner () {
throw new MyAppException(S"ExceptExample inner exception");
}
void CatchInner() {
try {
this->ThrowInner();
} catch (Exception* e) {
throw new MyAppException(S"Error caused by trying ThrowInner.", e);
}
}
ExceptExample* testInstance = new ExceptExample();
try {
testInstance->CatchInner();
} catch (Exception* e) {
Console::WriteLine (S"In Main catch block. Caught: {0}", e->Message);
Console::WriteLine (S"Inner Exception is {0}", e->InnerException);
}
此代码的输出如下:
In Main
catch block. Caught: Error caused by trying ThrowInner. Inner Exception is
MyAppException: ExceptExample inner exception at ExceptExample.ThrowInner() at
ExceptExample.CatchInner()
的示例。若要查看 Visual Basic、C# 或 C++ 示例,请单击页左上角的“语言筛选器”按钮 。
- Message 属性。
此属性提供有关异常起因的详细信息。Message用引发异常的线程的 Thread.CurrentUICulture 属性所指定的语言表示。
- HelpLink 属性。
此属性可保存某个帮助文件的 URL(或 URN),该文件提供有关异常起因的大量信息。
大多数从 Exception继承的类都不实现其他成员或提供附加功能;它们只是从 Exception继承。因此,在异常层次结构、异常名称以及异常包含的信息中可以找到有关异常的最重要信息。
备注
此类是所有异常的基类。当发生错误时,系统或当前正在执行的应用程序通过引发包含关于该错误的信息的异常来报告错误。异常发生后,将由该应用程序或默认异常处理程序进行处理。
公共语言运行库提供一种异常处理模型,该模型基于对象形式的异常表示形式,并且将程序代码和异常处理代码分到 try块和 catch块中。可以有一个或多个 catch块,每个块都设计为处理一种特定类型的异常,或者将一个块设计为捕捉比其他块更具体的异常。
如果应用程序将处理在执行应用程序代码块期间发生的异常,则代码必须放置在 try语句中。try语句中的应用程序代码是 try块。处理由 try块引发的异常的应用程序代码放在 catch语句中,称为 catch块。零个或多个 catch块与一个 try块相关联,每个 catch块均包含一个确定该块处理的异常类型的类型筛选器。
在 try块中出现异常时,系统按所关联 catch块在应用程序代码中出现的顺序搜索它们,直到定位到处理该异常的 catch块为止。如果某 Catch 块的类型筛选器指定 T或任何派生出 T的类型,则该 catch块处理 T类型的异常。系统在找到第一个处理该异常的 catch块后即停止搜索。因此,正如本节后面的示例所演示的那样,在应用程序代码中处理某类型的 catch块必须在处理其基类型的 catch块之前指定。处理 System.Exception的 Catch 块最后指定。
如果当前 try块所关联的所有 catch块均不处理该异常,且当前 try块嵌套在当前调用的其他 try块中,则搜索与下一个封闭 try块相关联的 catch块。如果没有找到用于该异常的 catch块,则系统搜索当前调用中前面的嵌套级别。如果在当前调用中没有找到用于该异常的 catch块,则将该异常沿调用堆栈向上传递,搜索上一个堆栈帧来查找处理该异常的 catch块。继续搜索调用堆栈,直到该异常得到处理或调用堆栈中没有更多的帧为止。如果到达调用堆栈顶部却没有找到处理该异常的 catch块,则由默认的异常处理程序处理该异常,然后应用程序终止。
异常类型支持下面的功能:
- 描述错误的可读文本。当异常发生时,运行库产生文本消息通知用户错误的性质并提供解决该问题的操作建议。此文本消息保存在异常对象的 Message 属性中。在创建异常对象过程中,可以将文本字符串传递给构造函数以描述该特定异常的详细信息。如果没有向构造函数提供错误信息参数,则将使用默认错误信息。
- 发生异常时调用堆栈的状态。StackTrace 属性包含可以用来确定代码中错误发生位置的堆栈跟踪。堆栈跟踪列出所有调用的方法和源文件中这些调用所在的行号。
基类 Exception下存在两类异常:
- 从 SystemException 派生的预定义公共语言运行库异常类。
- 从 ApplicationException 派生的用户定义的应用程序异常类。
Exception包含下列属性,可以帮助标识异常的代码位置、类型、帮助文件和原因:StackTrace、InnerException、Message、HelpLink、HResult、Source 和 TargetSite。
当在两个或多个异常之间存在因果关系时,InnerException属性会维护此信息。作为对此内部异常的反应将引发外部异常。处理外部异常的代码可利用以前的内部异常的信息更妥当地处理错误。
应本地化在创建异常对象过程中传递给构造函数的错误信息字符串,这种字符串可以使用 ResourceManager 从资源文件提供。有关本地化资源的更多信息,请参见“System.Resources 命名空间概述”和“打包和部署 .NET Framework 应用程序”。
若要向用户提供有关异常发生原因的大量信息,可以使用 HelpLink属性保存帮助文件的 URL(或 URN)。
Exception 构造函数
初始化 Exception类的新实例。受 .NET Framework 精简版的支持。
public Exception();
使用指定的错误信息初始化 Exception类的新实例。
受 .NET Framework 精简版的支持。
public Exception(string);
使用序列化数据初始化 Exception类的新实例。
protected Exception(SerializationInfo, StreamingContext);
使用指定错误信息和对导致此异常的内部异常的引用来初始化 Exception 类的新实例。
受 .NET Framework 精简版的支持。
public Exception(string, Exception);
异常层次结构
有两种类型的异常:由执行程序生成的异常和由公共语言运行库生成的异常。另外,还有由应用程序或运行库引发的异常的层次结构。Exception 是异常的基类。若干异常类直接从 Exception继承,其中包括 ApplicationException 和 SystemException。这两个类构成几乎所有运行库异常的基础。大多数直接从 Exception派生的异常不为 Exception类添加任何功能。例如,InvalidCastException 类层次结构如下所示:
Object
Exception
SystemException
InvalidCastException
错误发生时,运行库引发 SystemException的适当派生类。这些错误是失败的运行库检查(如数组超出界限错误)导致的,它们可在任何方法的执行过程中发生。ApplicationException由用户程序引发,而不是由运行库引发。如果设计创建新异常的应用程序,应从 ApplicationException类派生那些异常。不建议捕捉 SystemException,在应用程序中引发 SystemException也不是好的编程做法。
最严重的异常,即那些由运行库引发或在不可恢复的情况中引发的异常,包括 ExecutionEngineException、StackOverflowException 和 OutOfMemoryException。
交互操作异常从 SystemException派生并由 ExternalException 进一步扩展。例如,COMException 是 COM Interop 操作过程中引发的异常,它从 ExternalException派生。Win32Exception 和 SEHException 也从 ExternalException派生。
运行库异常层次结构
运行库有一组从 SystemException派生的基异常,它在执行各指令时引发这些异常。下表按层次结构列出了运行库提供的标准异常以及派生类的创建条件。
异常类型 |
基类型 |
说明 |
示例 |
Exception |
Object |
所有异常的基类。 |
无(使用此异常的派生类)。 |
SystemException |
Exception |
所有运行时生成的错误的基类。 |
无(使用此异常的派生类)。 |
IndexOutOfRangeException |
SystemException |
仅当错误地对数组进行索引时,才由运行库引发。 |
在数组的有效范围外对数组进行索引:
|
NullReferenceException |
SystemException |
仅当引用空对象时,才由运行库引发。 |
|
InvalidOperationException |
SystemException |
当处于无效状态时,由方法引发。 |
从基础集合移除 |
ArgumentException |
SystemException |
所有参数异常的基类。 |
无(使用此异常的派生类)。 |
ArgumentNullException |
ArgumentException |
由不允许参数为空的方法引发。 |
|
ArgumentOutOfRangeException |
ArgumentException |
由验证参数是否位于给定范围内的方法引发。 |
|
ExternalException |
SystemException |
在运行库的外部环境中发生或针对这类环境的异常的基类。 |
无(使用此异常的派生类)。 |
COMException |
ExternalException |
封装 COM HRESULT 信息的异常。 |
在 COM Interop 中使用。 |
SEHException |
ExternalException |
封装 Win32 结构化异常处理信息的异常。 |
在非托管代码 interop 中使用。 |
异常处理基础知识
公共语言运行库支持基于异常对象和受保护代码块概念的异常处理模型。运行库在异常发生时创建一个表示该异常的对象。也可以通过从适当的基异常派生类来创建自己的异常类。所有使用运行库的语言都以相似的方式处理异常。每种语言都使用 Try/Catch/Finally 形式的结构化异常处理。
使用 Try/Catch 块捕捉异常
将可能引发异常的代码节放在 Try 块中,而将处理异常的代码放在 Catch 块中。Catch 块是一系列以关键字 catch开头的语句,语句后跟异常类型和要执行的操作。几乎任何代码行都可以引发异常,尤其是公共语言运行库本身引发的异常,如 OutOfMemoryException 和 StackOverflowException。大多数应用程序不必处理这些异常,但在编写由其他人使用的库时应知道这种可能性。下面的代码示例使用 Try/Catch 块捕捉可能的异常。Main 方法包含带有 StreamReader语句的 Try 块,该语句打开名为 data.txt 的数据文件并从该文件写入字符串。Try 块后面是 Catch 块,该块捕捉 Try 块产生的任何异常。
Public Shared Sub Main()
Try
Dim sr As StreamReader = File.OpenText("data.txt")
Console.WriteLine("The first line of this file is {0}", sr.ReadLine())
Catch e As Exception
Console.WriteLine("An error occurred: '{0}'", e)
End Try
End Sub
using System;
using System.IO;
using System.Security.Permissions;
public class ProcessFile {
public static void Main() {
try {
StreamReader sr = File.OpenText("data.txt");
Console.WriteLine("The first line of this file is {0}", sr.ReadLine());
}
catch(Exception e) {
Console.WriteLine("An error occurred: '{0}'", e);
}
}
}
此示例阐释捕捉任何异常的基本 Catch 语句。一般而言,好的编程做法是捕捉特定类型的异常而不是使用基本 Catch 语句。
在 Catch 块中使用特定异常
发生异常时,异常沿堆栈向上传递,每个 Catch 块都有机会处理它。Catch 语句的顺序很重要。将针对特定异常的 Catch 块放在常规异常 Catch 块的前面,否则编译器可能会发出错误。确定正确 Catch 块的方法是将异常的类型与 Catch 块中指定的异常名称进行匹配。如果没有特定的 Catch 块,则由可能存在的常规 Catch 块捕捉异常。
下面的代码示例使用 Try/Catch 块捕捉 InvalidCastException。该示例创建一个名为 Employee
的类,它带有一个属性:职员级别 (Emlevel
)。PromoteEmployee
方法取得对象并增加职员级别。将 DateTime 实例传递给 PromoteEmployee
方法时,发生 InvalidCastException。
'Create employee level property.
Public Property Emlevel() As Integer
Get
Return emlevel
End Get
Set
emlevel = value
End Set
End Property
Private emlevel As Integer
Public Shared Sub PromoteEmployee(emp As [Object])
'Cast object to Employee.
Dim e As Employee = CType(emp, Employee)
' Increment employee level.
e.Emlevel = e.Emlevel + 1
End Sub 'PromoteEmployee
Public Shared Sub Main()
Try
Dim o = New Employee()
Dim newyears As New DateTime(2001, 1, 1)
'Promote the new employee.
PromoteEmployee(o)
'Promote DateTime; results in InvalidCastException as newyears is not an employee instance.
PromoteEmployee(newyears)
Catch e As InvalidCastException
Console.WriteLine(("Error passing data to PromoteEmployee method. " + e))
End Try
End Sub 'Main
using System;
public class Employee
{
//Create employee level property.
public int Emlevel
{
get
{
return(emlevel);
}
set
{
emlevel = value;
}
}
int emlevel;
}
public class Ex13
{
public static void PromoteEmployee(Object emp)
{
//Cast object to Employee.
Employee e = (Employee) emp;
// Increment employee level.
e.Emlevel = e.Emlevel + 1;
}
public static void Main()
{
try
{
Object o = new Employee();
DateTime newyears = new DateTime(2001, 1, 1);
//Promote the new employee.
PromoteEmployee(o);
//Promote DateTime; results in InvalidCastException as newyears is not an employee instance.
PromoteEmployee(newyears);
}
catch (InvalidCastException e)
{
Console.WriteLine("Error passing data to PromoteEmployee method. " + e);
}
}
}
公共语言运行库捕捉 Catch 块没有捕捉的异常。根据运行库的配置,或者出现一个调试对话框,或者程序停止执行并出现一个包含异常信息的对话框。
引发异常
可以使用 Throw 语句显式引发异常。还可以使用 Throw 语句再次引发捕捉的异常。好的编码做法是向再次引发的异常添加信息以在调试时提供更多信息。
下面的代码示例使用 Try/Catch 块捕捉可能的 FileNotFoundException。Try 块后面是 Catch 块,Catch 块捕捉 FileNotFoundException,如果找不到数据文件,则向控制台写入消息。下一条语句是 Throw 语句,该语句引发新的 FileNotFoundException 并向异常添加文本信息。
using System;
using System.IO;
public class ProcessFile
{
public static void Main()
{
FileStream fs = null;
try
//Opens a text tile.
{
fs = new FileStream("data.txt", FileMode.Open);
StreamReader sr = new StreamReader(fs);
string line;
//A value is read from the file and output to the console.
line = sr.ReadLine();
Console.WriteLine(line);
}
catch(FileNotFoundException e)
{
Console.WriteLine("[Data File Missing] {0}", e);
throw new FileNotFoundException("[data.txt not in c:""dev directory]",e);//再次引发异常,包括innerException信息
}
finally
{
fs.Close();
}
}
}
使用用户定义的异常
如果希望用户能以编程方式区分一些错误条件,您可以创建自己的用户定义的异常。.NET Framework 提供根本上从基类 Exception 派生的异常类层次结构。这些类中的每一个都定义一个特定的异常,因此在很多情况下只需捕捉该异常。您也可以通过从 ApplicationException 类派生来创建自己的异常类。
创建自己的异常时,好的编码做法是以“Exception”这个词作为用户定义的异常类名的结尾。如下面的示例所示实现三个推荐的公共构造函数也是好的做法。
注意 当使用远程处理时,对于任何用户定义的异常的元数据,都必须确保在服务器(被调用方)和客户端(代理对象或调用方)上均可用。例如,在单独的应用程序域中调用方法的代码必须能够找到包含远程调用引发的异常的程序集。有关更多信息,请参见处理异常的最佳做法。
在下面的示例中,新异常类 EmployeeListNotFoundException 从 System.ApplicationException 派生。该类中定义了三个构造函数,每一个带不同的参数。
[C#]
using System;
public class EmployeeListNotFoundException: ApplicationException
{
public EmployeeListNotFoundException()
{
}
public EmployeeListNotFoundException(string message)
: base(message)
{
}
public EmployeeListNotFoundException(string message, Exception inner)
: base(message, inner)
{
}
}
使用用户筛选的异常
目前,Visual Basic 和 C++ 的托管扩展都支持用户筛选的异常。用户筛选的异常处理程序根据您为异常定义的要求捕捉和处理异常。这些处理程序使用 C++ 的托管扩展中的 Try/Except 块或 Visual Basic 中带 When关键字的 Catch语句。
当一个特定的异常对象对应于多个错误时,此方法很有用。在那种情况下,对象通常有一个属性包含与错误关联的特定错误代码。可以在表达式中使用此错误代码属性,仅选择要在 Catch子句中处理的特定错误。
使用 Finally 块
异常发生时,执行将终止,并且控制交给最近的异常处理程序。这通常意味着不执行希望总是调用的代码行。有些资源清理(如关闭文件)必须总是执行,即使有异常发生。为实现这一点,可以使用 Finally 块。Finally 块总是执行,不论是否有异常发生。
下面的代码示例使用 Try/Catch 块捕捉 ArgumentOutOfRangeException。Main 方法创建两个数组并试图将一个数组复制到另一个数组。该操作生成 ArgumentOutOfRangeException,同时错误被写入控制台。Finally 块执行,不论复制操作的结果如何。
using System;
class ArgumentOutOfRangeExample
{
static public void Main()
{
int[] array1={0,0};
int[] array2={0,0};
try
{
Array.Copy(array1,array2,-1);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine("Error: {0}",e);
}
finally
{
Console.WriteLine("This statement is always executed.");
}
}
}