C#异常
编写异常代码
Eric Gunnerson
Microsoft Corporation
2001 年 7 月 19 日
最近,我曾对有关从“错误代码”样式的错误处理到 .NET 世界中的异常的使用的转变作过一些有趣的讨论,这是我决定用几个专栏来探讨这个主题的主要原因(第二个原因是我的专栏明天就要交稿,而我还没有想出什么其他主题来)。
本月我们将讨论使用异常的方式和原因,下月我们将讨论编写异常类的细节。
使用异常的原因
从错误代码到异常的转换对会大大影响代码的外观。学会用不同的方式编写代码要求您养成一套新的习惯,在您进行努力之前,了解到进行这样的努力是值得的,会有所帮助。
错误代码的使用已经有很长一段时间了。如果您过去用 C++ 编写代码,那么通常它看起来是这样的:
HRESULT retval = query.FetchRow();
if (FAILED(retval))
{
// error handling here
}
使用错误代码带来的真正的问题是产生的代码不是很好。虽然使用错误代码编写代码而可以正确地处理所有地错误在理论上是可能的,但它可不是一种曾经在洪荒时代出现过的东西。传统编程模型的一些弱点源于不正确的错误处理;编写能够正确进行错误处理的代码非常困难。
症结是错误代码方法要求程序员去做一些人类并不是特别擅长的工作,即始终保持一致和完整。而环境没有为正确地进行此操作提供任何帮助。异常使生活变得轻松多了。
Opt Out 而不是 Opt In
错误代码与异常之间重要区别是:“做正确的工作”需要些什么。错误代码使用了 opt in;如果代码不显式地对错误代码进行处理,错误就会吞下去。
相反,异常使用了 opt out 模型。默认情况下,运行库将对异常进行正确的处理,于是异常总是被传送。
这种区别对所编写的代码的种类有着巨大的影响。由于运行库处理一般情况,因此程序员可以放心大胆地把精力集中在行为不同的情况上。这提供了更稳定的代码,而所需的努力少得多。
仅仅是这个好处就足以成为转变为异常的理由。但是还有另一个重要的好处。
更多的信息
在编写新代码时,经常出现编写出的代码不能正确运行的情况。您已经从函数获取回了返回值,如“0x80001345”,现在您必须搞清楚它的意思是什么。
您的第一个任务是将其翻译为某种符号代码。尽管可能有更好的方法来做到这一点,我的做法通常是在 .h 文件中搜索该常数。接着,您需要在 MSDN 中查询来找出代码的含义,然后,要弄明白要更改什么。如果幸运的话就是这样的。有时,您将得到在任何地方都找不到的错误代码。
即使您确实找到了错误代码,它有时也不是很有帮助的。如果它是 ERROR_BAD_ARGUMENTS,我知道我所传递的其中一个参数可能不正确,但我并不知道哪一个是不正确的。我调用的函数知道,但它却没办法通知我是哪一个。
利用异常,我既得到了通知我犯了何种错误的名称,又得到了带有更多详细信息的消息。例如,如果我传递错误的参数,异常将通知我哪个参数不正确。这节约了大量的时间。
更秒的是,异常消息可以向我们提示解决问题所需的操作。今年早些时候我将一些 Beta 1 代码导入了到了 Beta 2。我运行应用程序时,引发了一条异常,说:
"Paint operation cannot be performed on this thread. Use Control.Invoke() instead"
更新的 Beta 2 代码会检查不正确的用法,而如果找到了,它将引发异常,告诉程序员解决问题的确切方法。这样来解决问题真的很方便。
异常处理和性能
当异常处理被添加到 C++ 时,它得到了使代码变慢的坏名声;启用了异常处理的代码会比没有异常处理的代码慢得多。这种名声一部分是应得的,尽管应该指出没有异常处理的代码显然是不完整的,原因是它没有对大量错误的检查。不过,既然它大部分时候都工作,就够好的了。
随着时间的流逝,C++ 中异常处理的系统开销减少了,但没有完全消除。为了正确地实现异常处理,C++ 编译器必须确定在任何可能引发异常的地方将创建(即,需要毁坏)哪些对象,然后生成代码以检查异常,并在检测到异常时正确地清理对象。这种代码有副作用,它使优化变得更加困难,所以代价还是有的。使用 C++,您必须始终付出代价。
.NET 使这对于编译器编写器变得容易得多。运行库很了解代码是如何执行的,而 .NET 编译器不必考虑检测是否已经发生异常。更重要的是,对象被垃圾回收器回收,因此不必分析不同的对象状态。垃圾回收器在发生异常时进行正确的处理以便进行清理。这意味着在非异常的情况中事实上不存在任何附加的系统开销。
使用异常处理
现在我已经说服您异常处理是个好主意,该是理解如何编写处理它的代码时候了。
Throw
在发生引发异常的条件时,可以用 throw 语句来对发出信号。例如,如果例程要求非空字符串作为参数,则它可能包含下列代码:
public void Process(string location)
{
if (location == null)
throw new ArgumentNullException("null value", "location");
}
在该示例中,ArgumentNullException 的新实例是用特定的消息和参数名称来创建的,throw 语句用于引发它。
Try-Catch
用于编写错误处理的最基本的构造是 try-catch。考虑下列代码:
try
{
Process(currentLocation);
Console.WriteLine("Done processing");
}
catch (ArgumentNullException e)
{
// handle the exception here
}
在该示例中,如果 ArgumentNullException 被 try 块中的代码引发(在此情况下,是 Process() 函数),控制将被立即转到 catch 块,而不会执行 Console.WriteLine() 调用。
更通用的捕捉
在上一个示例中,catch 子句捕捉到了 ArgumentNullException,它完全与 Process() 所引发的异常匹配。
不过,这不是一项要求。Catch 子句将捕捉指定的异常或者任何从该类派生的异常。例如:
try
{
Process(currentLocation);
Console.WriteLine("Done processing");
}
catch (ArgumentException e)
{
// handle the exception here
}
由于 ArgumentNullException 是从 ArgumentException 派生的,因此该 catch 子句将捕捉任何一种异常。此外,它还将捕捉其他的派生异常:ArgumentOutOfRangeException、InvalidEnumArgumentException 和 DuplicateWaitObjectException。
由于所有的异常归根到底都是从 Exception 类派生的,因此捕捉 Exception 将捕捉任何异常。对了,不是捕捉任何异常;这是因为 C++ 不会将用户限制为只引发从 Exception 派生的类,C# 提供了捕捉所有异常的语法:
catch
{
// handle the exception here
}
尽管这个语法是为了完整性而提供的,但是实际上很少使用它。大部分的 C++ 程序都将选择引发从 Exception 派生的异常,并且即使它们不这样做,此 catch 语法也不会让您超出引发了什么。
Catch 排序
对于给定的 try 子句,可以有一个以上的 catch 子句,每个 catch 捕捉不同的异常类型。在上一个示例中,对 ArgumentException 进行特殊处理,然后对所有其他异常进行别的处理可能很合适。选择最具体的(即,派生程度最大的)catch 子句。
那么示例将是这样的:
try
{
Process(currentLocation);
Console.WriteLine("Done processing");
}
catch (ArgumentException e)
{
// handle the exception here
}
catch (Exception e)
{
// handle the more general exception here
}
当使用多个 catch 子句时,导出类型必须始终列在任何基类型之前(或者,以从比较合适到不太合适的顺序列出)。这是为了提高可读性。通过阅读较早的 catch 子句,您可以确定运行时行为将是什么。
Catch 操作
既然我们已经捕捉到了异常,我们可能希望对它来做一些有意义的事情。我们可能想做的第一件事是用一些附加的上下文信息包装异常。
这是以下面的方式来实现的:
try
{
Process(currentLocation);
Console.WriteLine("Done processing");
}
catch (ArgumentException e)
{
throw new ArgumentException("Error while processing", e);
}
它为 ArgumentException 使用获取一个消息和另一个异常的构造函数。该构造函数将把传递的异常包装在新的异常中,而那个新的将被引发。
此过程为开发人员带来了巨大的好处。不是只获得单个一条有关所发生的事情的信息,包装异常还提供了一种类似堆栈跟踪的东西:
Exception occurred: System.Exception: Exception in Test1 ---> System.Exception: Exception in Test2 ---> System.DivideByZeroException: Attempted to divide by zero.
这样的输出使调试变得容易得多。如果用 /debug 编译,在各个等级上还将获得文件名和行号。
包装有助于为调试提供附加的信息。另一种方案是针对需要根据异常采取一些操作的情况。将输出发送到文件的代码看起来可能是这样的:
try
{
FileStream f = new FileStream(filename, FileMode.Create);
StreamWriter s = new StreamWriter(f);
s.WriteLine("{0} {1}", "test", 55);
s.Close();
f.Close();
}
catch (IOException e)
{
Console.WriteLine("Error opening file {0}", filename);
Console.WriteLine(e);
}
如果文件不能打开,则引发异常,激发 catch,出现错误会,而程序可以继续执行。在许多情况下,这没问题。
不过,在这种情况下就存在问题。如果异常在文件打开之后发生,则文件将无法关闭。这很糟糕。
所需要的是确保即使发生了异常也可以关闭文件的方法。
Try-Finally
finally 构造用于指定即使发生异常也始终会运行的代码。finally 通常用于清理发生异常时所产生的内容。修订上一个示例:
try
{
FileStream f = new FileStream(filename, FileMode.Create);
StreamWriter s = new StreamWriter(f);
s.WriteLine("{0} {1}", "test", 55);
s.Close();
f.Close();
}
catch (IOException e)
{
Console.WriteLine("Error opening file {0}", filename);
Console.WriteLine(e);
}
finally
{
if (f != null)
f.Close();
}
使用修订后的代码,即使发生了异常 finally 子句也将被运行。
Using 和 Lock 语句
前面的模式是相当普通的,C# 提供特殊的支持鼓励程序员做正确的事情。using 语句可用于创建 try-finally 代码,如下所示:
using (FileStream f = new FileStream(filename, FileMode.Create))
{
StreamWriter s = new StreamWriter(f);
s.WriteLine("{0} {1}", "test", 55);
s.Close();
f.Close();
}
catch (IOException e)
{
Console.WriteLine("Error opening file {0}", filename);
Console.WriteLine(e);
}
此代码等效于上一个示例。
lock 语句在 System.Threading.Monitor 类上提供一个类似的保护,以确保即使发生了异常也可以释放锁。
设计指导原则
本节将介绍几条处理异常的设计指导原则。
捕捉最具体的异常
如果代码需要从某些异常恢复,请确保仅捕捉这些异常。如果您捕捉了更一般性的异常,那么您就更有可能错误地吞下您不希望吞下的异常。
只在确定时才吞下
这的确是上一条原则的必然结果。在吞下异常时,您事实上在表明您理解可能引发此异常的所有的情况,并且您正在编写的恢复代码将处理所有这些情况。
如果适用就使用 lock 或 using
如果能使用 lock 或 using 语句,那么就使用它们。它们使代码更具可读性,并且使您执行正确处理的可能性更大。
如果适用就包装异常
如果可以将附加信息添加到异常,那么就想尽办法去做到这一点。如果把参数继续传递到另一个函数,那么添加有关该参数的附加信息对我就可能十分有用。
try
{
o.Process(v1, v2, v3);
}
catch (ArgumentException e)
{
throw new ArgumentException("error updating contact information", e);
}
catch 子句包装捕捉到的相同类型的表达式,并引发它。这将向用户提供两个级别的信息。
不过,对此要小心。在此示例中,catch 子句可以捕捉 ArgumentException 或者任何从 ArgumentException 派生的类型(如 ArgumentNullException)。通过只引发 ArgumentException,我已经确定我的调用方根本不关心其中的区别。
对此最糟的情况是捕捉 Exception 并把它包装在 Exception 里面。如果调用方需要对异常的真实类型进行解码,则必须编写下列代码:
try
{
o.BadFunction();
}
catch (Exception e)
{
if (e.Inner.GetType() == typeof(ArgumentException))
{
}
else
throw;
}
此代码查找被包装的异常的类型,要么处理它,要么重新引发异常。如果用户必须以这种方式编写代码,那么代码将被严重损坏。
Eric Gunnerson
Microsoft Corporation
2001 年 8 月 28 日
几个月前,我在编写一些代码时遇到了一个异常类,它并没有按我所期望的方式工作。我本想捕捉一个异常,然后将其包装在同一类型的异常中。但是,没有一个构造函数接受异常,所以我没能做到这一点。换句话说,类设计器未包括 (string message, Exception inner) 构造函数。
这使我考虑应该如何编写异常类,并且我意识到我并没有正确地理解规则。在进行了一些研究(编写少量代码)后,我明白了编写异常类的正确方法。我还确定了在编写异常类时容易犯错误。
因此,我已经决定将本月的时间用来讨论异常类的设计并且介绍一个验证异常类设计的实用工具。
新异常是必需的吗?
.NET 框架具有许多可以用在代码中的通用用途异常。例如,如果确定参数具有一个空值,则可以引发 ArgumentNullException。如果现有的异常不是一个好的匹配,那么应该编写自己的异常类。
基本异常
确定了要创建一个新异常后,需要给它命名。尽量想出能够体现该异常的引发原因的名称。
所有的异常类都应该以单词 Exception 结尾,并且应该从类 ApplicationException 派生。尽管这不是严格的要求,并且不这样做也不会中断任何事,但是这样做会使人们更容易维护您的代码。所以,异常类的基本框架如下:
public class MyException: ApplicationException
{
}
要创建该异常的有用实例,需要用来构造它的方法。标准设计模式指示所有的异常都应该具有三个基本构造函数。它们为:
public MyException() public MyException(string message) public MyException(string message, Exception inner)
前两个构造函数用于创建要引发的异常,多数时间使用第二个构造函数。
第三个构造函数用于将异常用更多信息包装起来。在大多数异常类中,这些构造函数只是将它们的参数转发到基类构造函数。例如,将第三个构造函数编码如下:
public MyException(string message, Exception inner): base(message, inner)
{
}
对于开始使用该异常类,这似乎已经足够,事实上在大多数情况下的确都可以使用这样的类。但是,也有可能有人打算将该异常从一台计算机传输到另一台计算机,这时当前版本对于此目的就不起作用了。然而更糟的是,直到有人在这种情况下引发异常并且运行库试图将其传送到另一个系统时才会失败。
因此,即使认为永远也不会在这种情况下使用该异常,使所有的异常类都能够被序列化和反序列化也是非常重要的。做到这一点不会花费很多的时间(或代码),并且代码将不会以难以调试的方式中断。
异常序列化
对于大多数类,仅仅通过将 [Serializable] 属性添加到类来启动序列化是可能的。但是,在这种情况下,Exception 类已经实现了 ISerializable 接口,并且因此已经定义了一个反序列化构造函数来创建该对象。(调用反序列化构造函数以从以前序列化的数据创建对象。)因为我们的类是从 Exception 派生的,所以需要复制一个这样的构造函数。
因为在我们的异常中没有任何字段,所以只将该调用转发到基类构造函数是安全的:
public MyException(SerializationInfo info, StreamingContext context):
base(info, context)
{
}
编写了反序列化构造函数后,我们就完成了通用用途异常类的创建。示例文件 ExcSimple.cs 包含下面的类:
// ExcSimple.cs - Sample simple exception class
//
// This exception class does everything it needs to do to be a well
// mannered exception.
using System;
using System.Runtime.Serialization;
[Serializable]
public class MySimpleException: ApplicationException
{
// Normal 3 constructors
public MySimpleException()
{
}
public MySimpleException(string message): base(message)
{
}
public MySimpleException(string message, Exception inner):
base(message, inner)
{
}
// deserialization constructor
public MySimpleException(SerializationInfo info,
StreamingContext context):
base(info, context)
{
}
}
添加附加字段
有些时候,文本消息不充分,您还希望在异常中添加更多的数据。例如,您可能希望捕获导致问题的参数名。
为此,您必须向异常类中再添加一点代码。要做的第一件事是添加字段和访问它的属性。在我们的例子中,我们只添加一个名为 value 的整数字段以及一个获取该值的只读属性:
int value;
public int Value
{
get
{
return value;
}
}
接下来,添加接受新值的构造函数。添加的构造函数的数目取决于使用附加值的方式。在此例中,我选择了添加单个接受消息和该值的构造函数:
public MyException(string message, int value): base(message)
{
this.value = value;
}
接下来,我们需要设置序列化新字段的类。将该类标记为实现 ISerializable 接口,并实现 GetObjectData() 方法:
public override void GetObjectData(SerializationInfo info,
StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("Value", value);
}
严格地讲,这将我们的类标记为重新实现接口,因为它已经从基类继承实现。此函数首先调用基函数以确保它有机会保存其数据,然后保存该值。字符串 "Value" 被作为与保存的字段关联的键传递,并且我们以后可以使用同一个键提取该值。
注意 名称 GetObjextData() 有点混淆。简言之,它被序列化函数调用以获取对象数据,因此为此名称。
接下来,我们需要修改反序列化构造函数以便它提取该值。这只不过是为键提取该值的问题。
public MyException(SerializationInfo info, StreamingContext context):
base(info, context)
{
value = info.GetInt32("Value");
}
在这两种情况下,我们都需要确保使基类可以保存或还原其字段。如果不这样做,我们的类将不会正确地序列化和反序列化。
最后,我们需要重写 Message 属性以便它包括我们添加的字段的值。这意味着如果有人对我们的异常调用 ToString(),他们将同时得到基类消息和附加信息。下面是该属性实现:
public override string Message
{
get
{
// NOTE: text should be localized...
string s = String.Format("Value: {0}", value);
return base.Message + Environment.NewLine + s;
}
}
如果字段是引用类型,则必须处理空事件,不过对于整数,不必要这样做。我们只是将整数值添加到消息中。另外请注意,应该为实际的异常本地化字符串。
完成了所有这些步骤后,我们以下面的类(可以在 ExcField.cs 中找到它)结束:
// ExcField.cs - Sample exception class with an added field.
//
// This exception class does everything it needs to do to be a well
// mannered exception.
using System;
using System.Runtime.Serialization;
[Serializable]
public class MyException: ApplicationException, ISerializable
{
int value;
// Normal 3 constructors
public MyException()
{
}
public MyException(string message): base(message)
{
}
public MyException(string message, Exception inner):
base(message, inner)
{
}
// constructors that take the added value
public MyException(string message, int value): base(message)
{
this.value = value;
}
// deserialization constructor
public MyException(SerializationInfo info,
StreamingContext context):
base(info, context)
{
value = info.GetInt32("Value");
}
public int Value
{
get
{
return value;
}
}
// Called by the frameworks during serialization
// to fetch the data from an object.
public override void GetObjectData(
SerializationInfo info,
StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("Value", value);
}
// overridden Message property. This will give the
// proper textual representation of the exception,
// with the added field value.
public override string Message
{
get
{
// NOTE: should be localized...
string s = String.Format("Value: {0}", value);
return base.Message + Environment.NewLine + s;
}
}
}
快速测试应用程序
除了这两个异常类,我还包括了一个名为 SerializeTest.cs 的小测试程序,它验证这两个异常类是否可以被序列化和反序列化。执行 test.bat 以生成这些类并运行验证程序。
异常检查程序
在写这篇文章时,我发现对于异常类必须做什么很容易弄糊涂。为了使执行正确的操作更容易些(以及拓展大家的思路),我编写了一个名为 ExceptionSurvey 的程序。
该实用工具将验证异常类的编写是否正确。它打开目录中的所有程序集,找到所有的异常类并为每个类验证以下各项:
- 异常的名称以 Exception... 结尾
- 异常实现三个标准构造函数:
class(); class(string message); class(string message, Exception inner);
- 异常被标记为 [Serializable]
- 异常实现反序列化构造函数:
class(SerializationInfo info, StreamingContext context)
- 异常没有公共字段
- 如果异常有私有字段,则它实现 GetObjectData()
- 如果异常有私有字段,则它重写 Message 属性
这将捕捉大多数在编写异常类时遇到的问题。要使用它,只需转到程序集所在的目录并执行该程序即可。它将产生关于它所发现的所有问题的报告。如果执行 test.bat,还会生成并运行检查程序。
阐释检查程序的工作机制超出了本专栏的范围,但是您会发现源文件中有很多注释,而且此操作并不是太复杂。
程序必须能够统一处理在执行期间发生的错误。公共语言运行库提供了一个模型,以统一的方式通知程序发生的错误,这样为设计容错软件提供了极大的帮助。所有的 .NET Framework 操作都通过引发异常来指示出现错误。
传统上,语言的错误处理模型依赖于语言检测错误和查找错误处理程序的独特方法,或者依赖于操作系统提供的错误处理机制。运行库实现的异常处理具有下列特点:
- 处理异常时不用考虑生成异常的语言或处理异常的语言。
- 异常处理时不要求任何特定的语言语法,而是允许每种语言定义自己的语法。
- 允许跨进程甚至跨计算机边界引发异常。
与其他错误通知方法(如返回代码)相比,异常具有若干优点。不再有出现错误而不被人注意的情况。无效值不会继续在系统中传播。不必检查返回代码。可以轻松添加异常处理代码,以增加程序的可靠性。最后,运行库的异常处理比基于 Windows 的 C++ 错误处理更快。
由于执行线程例行地遍历托管代码块和非托管代码块,因此运行库可以在托管代码或非托管代码中引发或捕捉异常。非托管代码可以同时包含 C++ 样式的 SEH 异常和基于 COM 的 HRESULT。
本节内容
- 异常概述
- 提供公共语言运行库异常的概述。
- Exception 类
- 描述异常对象的元素。
- 异常层次结构
- 描述一些异常,大多数异常都从这些异常派生。
- 异常处理基础知识
- 解释如何使用 Catch、Throw 和 Finally 语句处理异常。
- 处理异常的最佳做法
- 描述建议的异常处理方法。
- 处理 COM Interop 异常
- 描述如何处理在非托管代码中引发和捕捉的异常。
相关章节
- Exception 类
- 所有异常都从其继承的类的参考信息。
- ApplicationException 类
- 所有应用程序生成的异常都应从其派生的类的参考信息。
- SystemException 类
- 所有系统生成的异常都从其派生的类的参考信息。
- COM Interop(高级)
- 描述异常在托管代码和非托管代码之间的工作机制。
- HRESULT 和异常
- 描述异常在托管代码和非托管代码之间的映射。
设计良好的错误处理代码块集可使程序更可靠并且不容易崩溃,因为应用程序可处理这样的错误。下表包含有关处理异常的最佳做法的建议:
- 知道何时设置 Try/Catch 块。例如,可以以编程方式检查可能发生的条件,而不使用异常处理。在其他情况下,使用异常处理捕捉错误条件是适当的。
下面的示例使用 if 语句检查连接是否关闭。如果连接未关闭,可以使用此方法而不是引发异常。
[Visual Basic] If conn.State <> ConnectionState.Closed Then conn.Close() End If [C#] if(conn.State != ConnectionState.Closed) conn.Close();
在下面的示例中,如果连接未关闭,则引发异常。
[Visual Basic] Try conn.Close() Catch ex As InvalidOperationException 'Do something with the error or ignore it. End Try [C#] try { conn.Close(); } catch(InvalidOperationException ex) { //Do something with the error or ignore it. }
所选择的方法依赖于预计事件发生的频率。如果事件确实是异常的并且是一个错误(如意外的文件尾),则使用异常处理比较好,因为正常情况下执行的代码更少。如果事件是例行发生的,使用编程方法检查错误比较好。在此情况下,如果发生异常,将需要更长的时间处理。
- 在可潜在生成异常的代码周围使用 Try/Finally 块,并将 Catch 语句集中在一个位置。以这种方式,Try 语句生成异常,Finally 语句关闭或释放资源,而 Catch 语句从中心位置处理异常。
- 始终按从最特定到最不特定的顺序对 Catch 块中的异常排序。此方法在将特定异常传递给更常规的 Catch 块之前处理该异常。
- 以“Exception”这个词作为异常类名的结尾。例如:
[Visual Basic] Public Class
EmployeeListNotFoundException Inherits Exception [C#] public class MyFileNotFoundException : ApplicationException { } - 当创建用户定义的异常时,必须确保异常的元数据对远程执行的代码可用,包括当异常跨应用程序域发生时。例如,假设应用程序域 A 创建应用程序域 B,后者执行引发异常代码。应用程序域 A 若想正确捕捉和处理异常,它必须能够找到包含应用程序域 B 引发的异常的程序集。如果包含应用程序域 B 引发的异常的程序集位于应用程序域 B 的应用程序基下,而不是位于应用程序域 A 的应用程序基下,则应用程序域 A 将无法找到异常,公共语言运行库将引发 FileNotFoundException。为避免此情况,可以两种方式部署包含异常信息的程序集:
- 将程序集放在两个应用程序域共享的公共应用程序基中
- 或 -
- 如果两个应用程序域不共享一个公共应用程序基,则用强名称给包含异常信息的程序集签名并将其部署到全局程序集缓存中。
- 将程序集放在两个应用程序域共享的公共应用程序基中
- 在 C# 和 C++ 的托管扩展中创建您自己的异常类时,至少使用三个公共构造函数。有关示例,请参见使用用户定义的异常。
- 在大多数情况下,使用预定义的异常类型。仅为编程方案定义新异常类型。引入新异常类,使程序员能够根据异常类在代码中采取不同的操作。
- 不要从 Exception 基类派生用户定义的异常。对于大多数应用程序,从 ApplicationException 类派生自定义异常。
- 在每个异常中都包含一个本地化描述字符串。当用户看到错误信息时,该信息从引发的异常的描述字符串派生,而不是从异常类派生。
- 使用语法上正确的错误信息(包括结束标点符号)。在异常的描述字符串中,每个句子都应以句号结尾。
- 为编程访问提供 Exception 属性。仅当存在附加信息有用的编程方案时,才在异常中包含附加信息(不包括描述字符串)。
- 对非常常见的错误情况返回空。例如,如果没找到文件,File.Open 返回空;但如果文件被锁定,则引发异常。
- 类的设计应使在正常使用中从不引发异常。例如,FileStream 类公开另一种确定是否已到达文件尾的方法。这避免了在读取超过文件尾时引发的异常。下面的示例显示如何读到文件尾。
[Visual Basic] Class FileRead Sub Open() Dim stream As FileStream = File.Open("myfile.txt", FileMode.Open) Dim b As Byte ' ReadByte returns -1 at EOF. While b = stream.ReadByte() <> True ' Do something. End While End Sub End Class [C#] class FileRead { void Open() { FileStream stream = File.Open("myfile.txt", FileMode.Open); byte b; // ReadByte returns -1 at EOF. while ((b == stream.ReadByte()) != true) { // Do something. } } }
- 如果根据对象的当前状态,属性集或方法调用不适当,则引发 InvalidOperationException。
- 如果传递无效的参数,则引发 ArgumentException 或从 ArgumentException 派生的类。
- 堆栈跟踪从引发异常的语句开始,到捕捉异常的 Catch 语句结束。当决定在何处放置 Throw 语句时需考虑这一点。
- 使用异常生成器方法。类从其实现中的不同位置引发同一异常是常见的情况。为避免过多的代码,应使用帮助器方法创建异常并将其返回。例如:
[Visual Basic] Class File Private fileName As String Public Function Read(bytes As Integer) As Byte() If Not ReadFile(handle, bytes) Then Throw NewFileIOException() End If End Function 'Read Function NewFileIOException() As FileException Dim description As String = __unknown ' Build localized string, including fileName. Return New FileException(description) ' End Function 'NewFileIOException End Class 'File [C#] class File { string fileName; public byte[] Read(int bytes) { if (!ReadFile(handle, bytes)) throw NewFileIOException(); } FileException NewFileIOException() { string description = // Build localized string, including fileName. return new FileException(description); } }
或者,使用异常的构造函数生成异常。这更适合全局异常类,如 ArgumentException。
- 引发异常,而不是返回错误代码或 HRESULT。
- 引发异常时清理中间结果。当异常从方法引发时,调用方应该能够假定没有副作用。
请参见
http://jasmineou.cnblogs.com/archive/2005/08/23/221185.aspx

浙公网安备 33010602011771号