异常引发准则:
不要返回错误代码。异常是报告框架中的错误的主要手段。通过引发异常来报告执行故障。如果某一成员无法按预期方式成功执行,则应将这种情况视为一个执行故障并引发一个异常。如果代码遇到继续执行则不安全的情况,应考虑通过调用 System.Environment.FailFast(System.String)(.NET Framework 2.0 中的一种功能)来终止进程,而不是引发异常。尽可能不对正常控制流使用异常。除了系统故障及可能导致争用状态的操作之外,框架设计人员还应设计一些 API 以便用户可以编写不引发异常的代码。例如,可以提供一种在调用成员之前检查前提条件的方法,以便用户可以编写不引发异常的代码。注:对于上面一段话,用一个比较典型的例子可以充分说明:
1 public class User 2 { 3 private bool Exist(string userCode) 4 { 5 // 判断用户是否存在 6 ...... 7 return true; 8 } 9 public void Add(string userCode, string psw)10 {11 if(Exist(userCode))12 {13 // 很多人就在此处纠结,到底是通过异常返回已存在信息,还是通过返回值14 }15 } 16 }
根据准则中的建议,我们应该这样做:
1 public class User 2 { 3 // 首先提供一个可以判断用户是否存在的公开方法 4 public bool Exist(string userCode) 5 { 6 // 判断用户是否存在 7 ...... 8 return true; 9 }10 public void Add(string userCode, string psw)11 {12 if(Exist(userCode))13 {14 // 直接抛出异常,当然不要抛出Exceptio异常,可以自定义一个异常15 throw new Exception("用户名已存在");16 }17 } 18 }
看到这里,可能很多人会问,这种做法不就是用异常来控制使用流程吗?别着急,往下看:
1 public class Frm 2 { 3 public static Main() 4 { 5 User u = new User(); 6 if(u.Exist(userCode)) 7 { 8 // 直接提示用户已存在 9 MessageBox("用户已存在");10 return;11 }12 // 此时再调用Add,就不用担心是否会因为用户名重复而抛出异常了13 u.Add(userCode, psw);14 }15 }
上面这样做的好处是:
(1)程序的流程由自己控制而不是由异常控制。
(2)只要调用者按照规定的流程来使用方法,就不会产生任何异常性能损耗。
(3)异常只是在使用者随意调用(不调用Exist方法,而直接调用Add方法)方法时起作用,在正常使用时不会产生任何性能损耗。
(3)不需要在Add方法中再用一个丑陋的错误字符串来返回错误消息
考虑引发异常的性能影响。记录公共可调用的成员因成员协定冲突(而不是系统故障)而引发的所有异常,并将这些异常视为协定的一部分。包含在协定中的异常不应从一个版本更改到下一个版本。不要包含可以根据某一选项引发或不引发异常的公共成员。
异常处理准则:
不要在框架代码中捕捉非特定异常(如 System.Exception、System.SystemException 等)以至忽略错误。下面的代码示例演示的异常处理是不正确的。1 public class BadExceptionHandlingExample1 2 { 3 public void DoWork() 4 { 5 // Do some work that might throw exceptions. 6 } 7 public void MethodWithBadHandler() 8 { 9 try 10 {11 DoWork();12 }13 catch (Exception e)14 {15 // Swallow the exception and continue16 // executing.17 }18 }19 }
避免在应用程序代码中捕捉非特定异常(如 System.Exception、System.SystemException 等)以至忽略错误。某些情况下,可以在应用程序中忽略错误,但这种情况极少。
如果捕捉异常是为了传输异常,则不要排除任何特殊异常。下面的代码示例演示对以再次引发为目的特殊异常进行的不正确测试。1 public class BadExceptionHandlingExample2 2 { 3 public void DoWork() 4 { 5 // Do some work that might throw exceptions. 6 } 7 public void MethodWithBadHandler() 8 { 9 try 10 {11 DoWork();12 }13 catch (Exception e)14 {15 if (e is StackOverflowException ||16 e is OutOfMemoryException)17 throw;18 // Handle exception and continue 19 // executing.20 }21 }22 }
如果了解特定异常在给定上下文中引发的条件,请考虑捕捉这些异常。
不要过多使用 catch。通常应允许异常在调用堆栈中往上传播。使用 try-finally 并避免将 try-catch 用于清理代码。在书写规范的异常代码中,try-finally 比 try-catch 使用得更多。捕捉并再次引发异常时,首选使用空引发。这是保留异常调用堆栈的最佳方式。下面的代码示例演示一个可引发异常的方法。1 public void DoWork(Object anObject) 2 { 3 // Do some work that might throw exceptions. 4 if (anObject == null) 5 { 6 throw new ArgumentNullException("anObject", 7 "Specify a non-null argument."); 8 } 9 // Do work with o.10 }
下面的代码示例演示捕捉一个异常,并在再次引发该异常时对它进行错误的指定。这会使堆栈跟踪指向再次引发作为错误位置,而不是指向 DoWork 方法。
1 public void MethodWithBadCatch(Object anObject) 2 { 3 try 4 { 5 DoWork(anObject); 6 } 7 catch (ArgumentNullException e) 8 { 9 System.Diagnostics.Debug.Write(e.Message);10 // This is wrong.11 throw e; 12 // Should be this:13 // throw;14 }15 }
不要使用无参数 catch 块来处理不符合 CLS 的异常(不是从 System.Exception 派生的异常)。支持不是从 Exception 派生的异常的语言可以处理这些不符合 CLS 的异常。
捕捉和引发标准异常类型:Exception 和 SystemException不要引发 System.Exception 或 System.SystemException。不要在框架代码中捕捉 System.Exception 或 System.SystemException,除非打算再次引发。避免捕捉 System.Exception 或 System.SystemException,在顶级异常处理程序中除外。ApplicationException不要引发 System.ApplicationException 或从该异常进行派生。InvalidOperationException如果处于不适当的状态,则引发 System.InvalidOperationException 异常。如果没有向属性集或方法调用提供适当的对象当前状态,则应引发 System.InvalidOperationException。例如,向已打开用于读取的 System.IO.FileStream 写入时,应引发 System.InvalidOperationException 异常。ArgumentException、ArgumentNullException 和 ArgumentOutOfRangeException如果向成员传递了错误的参数,则引发 System.ArgumentException 或其子类型之一。如果适用,首选派生程度最高的异常类型。在引发 System.ArgumentException 或其派生类型之一时,设置 System.ArgumentException.ParamName 属性。此属性存储导致引发异常的参数的名称。注意,使用其中一个构造函数重载可以设置该属性。使用属性 setter 的隐式值参数的名称的值。不要允许公开可调用的 API 显式或隐式引发 System.NullReferenceException、System.AccessViolationException、System.InvalidCastException 或 System.IndexOutOfRangeException。进行参数检查以避免引发这些异常。引发这些异常会公开方法的实现细节,这些细节可能会随时间发生更改。StackOverflowException不要显式引发 System.StackOverflowException。此异常只应由公共语言运行库 (CLR) 显式引发。不要捕捉 System.StackOverflowException。以编程方式处理堆栈溢出极为困难。应允许此异常终止进程并使用调试确定问题的根源。OutOfMemoryException不要显式引发 System.OutOfMemoryException。此异常只应由 CLR 基础结构引发。ComException 和 SEHException不要显式引发 System.Runtime.InteropServices.COMException 或 System.Runtime.InteropServices.SEHException。这些异常只应由 CLR 基础结构引发。不要显式捕捉 System.Runtime.InteropServices.SEHException。ExecutionEngineException不要显式引发 System.ExecutionEngineException。设计自定义异常:避免使用深的异常层次结构。一定要从 System.Exception 或其他常见基本异常之一派生异常。请注意,捕捉和引发标准异常类型中有一条规定,不应从 ApplicationException 派生自定义异常。异常类名称一定要以后缀 Exception 结尾。应使异常可序列化。异常必须可序列化才能跨越应用程序域和远程处理边界正确工作。一定要在所有异常上都提供(至少是这样)下列常见构造函数。请确保参数的名称和类型与使用者相同.代码如下:1 public class NewException : BaseException, ISerializable 2 { 3 public NewException() 4 { 5 // Add implementation. 6 } 7 public NewException(string message) 8 { 9 // Add implementation.10 }11 public NewException(string message, Exception inner)12 {13 // Add implementation.14 }15 16 // This constructor is needed for serialization.17 protected NewException(SerializationInfo info, StreamingContext context)18 {19 // Add implementation.20 }21 }
一定要只在要求适合的权限后,才通过 System.Object.ToString 的重写报告安全敏感信息。如果权限要求失败,则返回一个不包括安全敏感信息的字符串。
一定要以私有异常状态存储有用的安全敏感信息。请确保只有受信任的代码才能获取该信息。考虑提供异常属性,以便可以以编程方式访问除消息字符串之外与异常相关的额外信息。异常和性能:不要由于担心异常可能会对性能造成不良影响而使用错误代码。对于可能在常见方案中引发异常的成员,可以考虑使用 Tester-Doer 模式来避免与异常相关的性能问题。Doer的代码如下:1 public class Doer 2 { 3 // Method that can potential throw exceptions often. 4 public static void ProcessMessage(string message) 5 { 6 if (message == null) 7 { 8 throw new ArgumentNullException("message"); 9 }10 }11 // Other methods...12 }
Tester代码如下:
1 public class Tester 2 { 3 public static void TesterDoer(ICollectionmessages) 4 { 5 foreach (string message in messages) 6 { 7 // Test to ensure that the call 8 // won't cause the exception. 9 if (message != null)10 {11 Doer.ProcessMessage(message);12 }13 }14 }15 }
注:Tester-Doer模式指出,在一个方法内部,首先要检查输入参数的合法性,如果发现存在不合法参数,则抛出异常;而在调用此方法之前,也需要检查传入参数是否符合要求。
这样做的好处在于,程序不会因为任何一方(方法提供者和方法是用者)错误的应用而出现无法预知的错误。而且这样还可以减少方法内部抛出异常的可能性,进而减少因异常而引出的性能损耗。