CLR via C#, 4th -- 【核心机制】 -- 第20章异常和状态管理

20.1 定义“异常”

异常是指成员没有完成它的名称所宣称的行动。

20.2 异常处理机制

.NET Framework异常处理机制是用Microsoft Windows提供的结构化异常处理(Structured Exception Handling,SEH)机制构建的。

20.2.1 try块

如果代码需要执行一般性的资源清理操作,需要从异常中恢复,或者两者都需要,就可以放到try块中。负责清理的代码应放到一个finally块中。try块还可包含也许会抛出异常的代码。

20.2.2 catch块

catch块包含的是响应一个异常需要执行的代码。

catch关键字后的圆括号中的表达式称为捕捉类型。C#要求捕捉类型必须是Systen.Exception或者它的派生类型。

在catch块的末尾,有以下三个选择:

  • 重新抛出相同的异常,向调用栈高一层的代码通知该异常的发生。
  • 抛出一个不同的异常,向调用栈高一层的代码提供更丰富的异常信息。
  • 让线程从catch块的底部退出,执行之后的语句。

20.2.3 finally块

finally块包含的是保证会执行的代码。一般在finally块中执行try块的行动所要求的资源清理操作。

20.3 System.Exception类

TABLE 20-1  Public Properties of the System.Exception  Type

Property

Access

Type

Description

Message

Read-only

String

Contains  helpful text indicating why the exception was thrown. The message is typically written to a log when a thrown exception is unhandled. Because end users do not see this message, the message should be as techni-cal as possible so that developers viewing the log can use the information in the message to fix the code when producing a new version.

Data

Read-only

IDictionary

A reference to a collection of key-value pairs. Usually, the code throwing the exception adds entries to this collection prior to throwing it; code that catches the exception can query the entries and use the information in its exception-recovery processing.

Source

Read/write

String

Contains the name of the assembly that generated the exception.

StackTrace

Read-only

String

Contains the names and signatures of methods called that led up to the exception being thrown. This property is invaluable( adj.极有用的; 极宝贵的;) for debugging.

TargetSite

Read-only

MethodBase

Contains the method that threw the exception.

HelpLink

Read-only

String

Contains a URL (such as file://C:\MyApp\Help.htm  #MyExceptionHelp) to documentation that can help a user understand the exception. Keep in mind that sound programming and security practices prevent users from ever being able to see raw unhandled exceptions, so unless you are trying to convey information to other programmers, this property is seldom used.

InnerException

Read-only

Exception

Indicates the previous exception if the current exception were raised while handling an exception. This read-only property is usually null. The  Exception type also offers a public  GetBaseException  method that traverses ( v.横过; 横越; 穿过; 横渡;) the linked list of inner exceptions and returns the originally thrown exception.

HResult

Read/write

Int32

 A 32-bit value that is used when crossing managed and native code boundaries. For example, when COM APIs return failure HRESULT values, the CLR throws an Exception-derived object and maintains the HRESULT value in this property.

System.Exception类型提供的只读StackTrace属性。catch块可读取该属性来获取一个堆栈跟踪(stack trace),它描述了异常发生前调用了哪些方法。检查异常原因并改正代码时,这些信息是很有用的。

以下代码抛出它捕捉到的相同的异常对象,导致CLR重置该异常的起点:

private void SomeMethod() {   
   try { ... }  
   catch (Exception e) {  
      ...  
      throw e;  // CLR thinks this is where exception originated.   
                // FxCop reports this as an error  
   }  
}

如果仅仅使用throw关键字本身来重新抛出异常对象,CLR就不会重置堆栈的起点。

private void SomeMethod() {   
   try { ... }  
   catch (Exception e) {  
      ...  
      throw;  // This has no effect on where the CLR thinks the exception  
              // originated. FxCop does NOT report this as an error   
   }  
}

StackTrace属性返回的字符串不包含调用栈中比接受异常对象的那个catch块高的任何方法。要获得从线程起始处到异常处理程序(catch块)之间的完整堆栈跟踪,需要使用System.Diagnostics.StackTrace类型。

20.4 FCL定义的异常类

The Framework Class Library (FCL) defines many exception types (all ultimately derived from System.Exception).

20.5 抛出异常

抛出异常时要考虑两个问题:

第一个问题是抛出什么Exception派生类型。应选择一个有意义的类型。

强烈建议定义浅而宽的异常类型层次结构",以创建尽量少的基类。原因是基类的主要作用就是将大量错误当作一个错误,而这通常是危险的。基于同样的考虑,永远都不要抛出一个System.Exception对象,抛出其他任何基类异常类型时也要特别谨慎。

第二个问题是向异常类型的构造器传递什么字符串消息。

20.6 定义自己的异常类

设计自己的异常不仅繁琐,还容易出错。主要原因是从Exception派生的所有类型都应该是可序列化的(serializable),使它们能穿越AppDomain边界或者写入日志/数据库。

写一个自己的泛型Exception<TExceptionArgs>类,它像下面这样定义:

[Serializable]
public sealed class Exception<TExceptionArgs> : Exception, ISerializable
    where TExceptionArgs : ExceptionArgs
{

    private const String c_args = "Args";  // For (de)serialization
    private readonly TExceptionArgs m_args;

    public  TExceptionArgs Args
    {
        get
        {
            return m_args;
        }
    }

    public Exception(String message = null, Exception innerException = null)
        : this(null, message, innerException) { }

    public Exception(TExceptionArgs args, String message = null,
                     Exception innerException = null): base(message, innerException)
    {
        m_args = args;
    }

    // This constructor is for deserialization; since the class is sealed, the constructor is
    // private. If this class were not sealed, this constructor should be protected
    [SecurityPermission(SecurityAction.LinkDemand,
                        Flags = SecurityPermissionFlag.SerializationFormatter)]
    private Exception(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
        m_args = (TExceptionArgs)info.GetValue(c_args, typeof(TExceptionArgs));
    }

    // This method is for serialization; it’s public because of the ISerializable interface
    [SecurityPermission(SecurityAction.LinkDemand,
                        Flags = SecurityPermissionFlag.SerializationFormatter)]
    public override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue(c_args, m_args);
        base.GetObjectData(info, context);
    }
    public override String Message
    {
        get
        {
            String baseMsg = base.Message;
            return (m_args == null) ? baseMsg : baseMsg + " (" + m_args.Message + ")";
        }
    }

    public override Boolean Equals(Object obj)
    {
        Exception<TExceptionArgs> other = obj as Exception<TExceptionArgs>;
        if (other == null) return false;
        return Object.Equals(m_args, other.m_args) && base.Equals(obj);
    }
    public override int GetHashCode()
    {
        return base.GetHashCode();
    }
}
View Code

TExceptionArgs约束为的ExceptionArgs基类非常简单,它看起来像下面这样:

[Serializable] 
public abstract class ExceptionArgs { 
   public virtual String Message { get { return String.Empty; } } 
}
View Code

要定义代表磁盘满的异常类,可以像下面这样:

[Serializable] 
public sealed class DiskFullExceptionArgs : ExceptionArgs { 
   private readonly String m_diskpath; // private field set at construction time 
 
   public DiskFullExceptionArgs(String diskpath) { m_diskpath = diskpath; } 
 
   // Public read­only property that returns the field  
   public String DiskPath { get { return m_diskpath; } }  
 
   // Override the Message property to include our field (if set) 
   public override String Message { 
      get {  
         return (m_diskpath == null) ? base.Message : "DiskPath=" + m_diskpath; 
      } 
   } 
}
View Code

现在,可以像下面这样写来抛出并捕捉这样的一个异常:

public static void TestException() {  
   try {  
      throw new Exception<DiskFullExceptionArgs>( 
         new DiskFullExceptionArgs(@"C:\"), "The disk is full"); 
   } 
   catch (Exception<DiskFullExceptionArgs> e) { 
      Console.WriteLine(e.Message); 
   } 
}
View Code

20.7 用可靠性换取开发效率

对所有可能的错误有了一个基本认识之后,就能理解为何不去追求完全健壮和可靠的代码了:因为不切实际(更极端的说法是根本不可能),不去追求完全的健壮性和可靠性,另一个原因是错误不经常发生。由于错误(比如OutOMMemoryException)极其罕见,所以开发人员决定不去追求完全可靠的代码,牺牲一定的可靠性来换取程序员开发效率的提升。

为了缓解对状态(任务意外中断后的影响)的破坏,可以做下面几件事情:

  • 执行catch或finally块中的代码时,CLR不允许线程终止。但绝对不建议将所有代码都放到finally块中!
  • 可以用System.Diagnostics.Contracts.Contract类向方法应用代码协定。通过代码协定在用实参和其他变量对状态进行修改之前,可以先对这些实参/变量进行验证。
  • 可以使用约束执行区域(Constrained Execution Region,CER),它能消除CLR的某些不确定性。
  • |取决于状态存在于何处,可利用事务(transaction)来确保状态要么都修改,要么都不修改。
public static class SomeType { 
   private static Object s_myLockObject = new Object(); 
 
   public static void SomeMethod () { 
      Monitor.Enter(s_myLockObject);  // If this throws, did the lock get taken or   
                                      // not? If it did, then it won't get released! 
      try {  
         // Do thread ­safe operation here... 
      } 
      finally { 
         Monitor.Exit(s_myLockObject);  
      } 
   } 
   // ... 
}

建议像下面这样重写以上代码:

public static class SomeType { 
   private static Object s_myLockObject = new Object(); 
 
   public static void SomeMethod () { 
      Boolean lockTaken = false;  // Assume the lock was not taken  
      try {  
         // This works whether an exception is thrown or not!  
         Monitor.Enter(s_myLockObject, ref lockTaken);  
 
         // Do thread ­safe operation here... 
      } 
      finally { 
         // If the lock was taken, release it 
         if (lockTaken) Monitor.Exit(s_myLockObject);  
      } 
   } 
   // ... 
}

如果觉得状态过于糟糕,觉得整个进程都应该终止,那么可用调用Environment的静态FailFast方法终止进程:

public static void FailFast(String message); 
public static void FailFast(String message, Exception exception);

FailFast方法将消息字符串和可选的异常(通常是catch块中捕捉的异常)写入Windows Application事件日志,生成Windows错误报告,创建应用程序的内存转储(dump),然后终止当前进程。

20.8 设计规范和最佳实践

20.8.1 善用finally块

只要使用了lock,using和foreach语句,C#编译器就会自动生成try/fnally块。

  • 使用lock语句时,锁在finally块中释放。
  • 使用using语句时,在finally块中调用对象的Dispose方法。
  • 使用foreach语句时,在finally块中调用1Enumerator对象的Dispose方法。
  • 定义析构器方法时,在finally块中调用基类的Finalize方法。

20.8.2 不要什么都捕捉

应用程序代码抛出异常,应用程序的另一部分可能预期要捕该异常。所以,绝对不要写“大小通吃”的类型,悄悄地“吞噬”异常,而是应该允许常在调用栈中向上移动,让应用程序代码针对性地处理它。

20.8.3 得体地从异常中恢复

20.8.4 发生不可恢复的异常时回滚部分完成的操作-维持状态

将文件恢复为任何对象序列化之前的状态。以下代码演示了正确的实现方式:

public void SerializeObjectGraph(FileStream fs, IFormatter formatter, Object rootObj) {   
  
   // Save the current position of the file.  
   Int64 beforeSerialization = fs.Position;   
  
   try {  
      // Attempt to serialize the object graph to the file.  
      formatter.Serialize(fs, rootObj);  
   }  
   catch {  // Catch any and all exceptions.  
      // If ANYTHING goes wrong, reset the file back to a good state.  
      fs.Position = beforeSerialization;   
  
      // Truncate the file.   
      fs.SetLength(fs.Position);   
  
      // NOTE: The preceding code isn't in a finally block because  
      // the stream should be reset only when serialization fails.    
  
      // Let the caller(s) know what happened by re ­throwing the SAME exception.  
      throw;   
   }  
}

20.8.5 隐藏实现细节来维系协定

有时,开发人员之所以捕捉一个异常并抛出一个新异常,目的是在异常中添加额外的数据或上下文。然而,如果这是你唯一的目的,那么只需捕捉希望的异常类型,在异常对象的Data属性(一个键值对的集合)中添加数据,然后重新抛出相同的异常对象。

private static void SomeMethod(String filename) { 
   try {  
      // Do whatevere here... 
   } 
   catch (IOException e) {  
      // Add the filename to the IOException object  
      e.Data.Add("Filename", filename); 
 
      throw;   // re­throw the same exception object that now has additional data in it  
   } 
}

20.9 未处理的异常

Microsoft建议应用程序开发人员接受CLR的默认策略。也就是说,应用程序发生未处理的异常时,Windows会向事件日志写一条记录。为了查看该记录,可打开“事件查看器”应用程序,然后打开树结构中的"Windows日志”->“应用程序”节点。

还可以通过"Windows操作中心”来获取更有趣的细节。为了启动操作中心,请单击系统托盘中的小旗,选择“打开操作中心”。然后,请展开“维护”,单击“查看可靠性历史记录”链接。随后,会在底部的窗格看到应用程序由于未处理的异常而终止。

Microsoft的每种应用程序模型都有自己的与未处理异常打交道的方式。需要在文档中查阅以下成员的信息。

  • 对于任何应用程序,查阅System.AppDomain的UnhandledException事件。Windows Store应用和Microsoft Silverlight应用程序访问不了该事件。
  • 对于Windows Store应用,查阅Windows.ULXaml.Application的UnhandledException事件。
  • 对于Windows窗体应用程序,查阅System.Windows.Forms.NativeWindow的OnThreadException虚方法、System.Windows.Forms.Application的OnThreadException虚方法以及System.Windows.Forms.Application的ThreadException事件
  • 对于Windows Presentation Foundation(WPF)应用程序,查阅System.Windows.Application的DispatcherUnhandledException事件和System.Windows.Threading.Dispatcher的UnhandledException和UnhandledExceptionFilter事件。
  • 对于Silverlight,查阅System.Windows.Application的UnhandledException事件。
  • 对于ASPNET Web窗体应用程序,查阅System.Web.U1.TemplateControl的Error事件。TemplateControl是System.Web.UI.Page类和System.Web.UI.UserControl类的基类。另外,还要查阅System.Web.HttpApplication的Error事件。
  • 对于Windows Communication Foundation应用程序,查阅System.ServiceModel.Dispatcher.ChannelDispatcher的ErrorHandlers属性。

20.10对异常进行调试

Visual Studio调试器为异常提供了特殊支持。在当前已打开一个解决方案的前提下,请从“调试”菜单选择“异常”。

对于任何异常类型,如果勾选了“引发”选项框,调试器就会在抛出该异常时中断。注意在中断时,CLR还没有尝试去查找任何匹配的catch块。

如果异常类型的“引发”框没有勾选,调试器只有在该异常类型未得到处理时才中断。

20.11 异常处理的性能问题

定义类型的成员时,应确保在一般使用情形中不会失败。只有用户以后因为抛出异常而对性能不满意时,才应考虑添加一些TryXxx方法。换言之,首先应建立一个最佳的对象模型。然后,只有在用户抱怨的时候,才在类型中添加一些Tryxxx方法,帮助遭遇性能问题的用户改善性能。如果用户没有遇到性能问题,那么应继续使用方法的非TryXxx版本,因为那是更佳的对象模型。

20.12 约束执行区域(CER)

private static void Demo1() { 
   try {  
      Console.WriteLine("In try"); 
   } 
   finally { 
      // Type1’s static constructor is implicitly called in here 
      Type1.M();  
   } 
} 
 
private sealed class Type1 {  
   static Type1() { 
      // if this throws an exception, M won’t get called  
      Console.WriteLine("Type1's static ctor called"); 
   } 
 
   public static void M() { } 
}

When I run the preceding code, I get the following output:

In try
Type1's static ctor called

private static void Demo2() { 
   // Force the code in the finally to be eagerly prepared  
   RuntimeHelpers.PrepareConstrainedRegions();  // System.Runtime.CompilerServices namespace 
   try {  
      Console.WriteLine("In try"); 
   } 
   finally { 
      // Type2’s static constructor is implicitly called in here 
      Type2.M();  
   } 
} 
 
public class Type2 { 
   static Type2() { 
         Console.WriteLine("Type2's static ctor called"); 
   } 
 
   // Use this attribute defined in the System.Runtime.ConstrainedExecution namespace 
   [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] 
   public static void M() { } 
}

Now, when I run this version of the code, I get the following output.
Type2's static ctor called
In try

PrepareConstrainedRegions是一个很特别的方法。JIT编译器如果发现在一个try块之前调用了这个方法,就会提前编译与try关联的catch和finally块中的代码。JIT编译器会加载任何程序集,创建任何类型对象,调用任何静态构造器,并对任何方法进行 т编译。如果其中任何操作造成异常,这个异常会在线程进入try块之前发生。

JIT编译器提前准备方法时,还会遍历整个调用图,提前准备被调用的方法,前提是这些方法应用了ReliabilityContractAttribute,而且向这个特性实例的构造器传递的是Consistency.WillNotCorruptState或者Consistency.MayCorruptInstance枚举成员。这是由于假如方法会损坏AppDomain或进程的状态,CLR便无法对状态一致性做出任何保证。在通过一个PrepareConstrainedRegions调用来保护的一个catch或finally块中,请确保只调用根据刚才的描述设置了ReliabilityContractAttribute的方法。

20.13代码协定

代码协定(code contract)提供了直接在代码中声明代码设计决策的一种方式。

前条件

一般用于对实参进行验证。

后条件

方法因为一次普通的返回或者抛出异常而终止时,对状态进行验证。

对象不变性(Object Invariant)

在对象的整个生命期内,确保对象的字段的良好状态。

代码协定的核心是静态类System.Diagnostics.Contracts.Contract:

public static class Contract
{
    // Precondition methods: [Conditional("CONTRACTS_FULL")]
    public static void Requires(Boolean condition);
    public static void EndContractBlock();

    // Preconditions: Always
    public static void Requires<TException>(Boolean condition) where TException : Exception;

    // Postcondition methods: [Conditional("CONTRACTS_FULL")]
    public static void Ensures(Boolean condition);
    public static void EnsuresOnThrow<TException>(Boolean condition)
    where TException : Exception;

    // Special Postcondition methods: Always
    public static T Result<T>();
    public static T OldValue<T>(T value);
    public static T ValueAtReturn<T>(out T value);

    // Object Invariant methods: [Conditional("CONTRACTS_FULL")]
    public static void Invariant(Boolean condition);

    // Quantifier methods: Always
    public static Boolean Exists<T>(IEnumerable<T> collection, Predicate<T> predicate);
    public static Boolean Exists(Int32 fromInclusive, Int32 toExclusive,
                                 Predicate<Int32> predicate);
    public static Boolean ForAll<T>(IEnumerable<T> collection, Predicate<T> predicate);
    public static Boolean ForAll(Int32 fromInclusive, Int32 toExclusive,
                                 Predicate<Int32> predicate);
    // Helper methods: [Conditional("CONTRACTS_FULL")] or [Conditional("DEBUG")]
    public static void Assert(Boolean condition);
    public static void Assume(Boolean condition);

    // Infrastructure event: usually your code will not use this event
    public static event EventHandler<ContractFailedEventArgs> ContractFailed;
}
View Code

Code Contracts for .NET

Code Contract Rewriter(CCRewrite.exe)

Code Contract Checker (CCCheck.exe)

Code Contract Reference Assembly Generator tool (CCRefGen.exe)

Code Contract Document Generator tool (CCDocGen.exe)

posted @ 2019-10-22 11:06  FH1004322  阅读(183)  评论(0)    收藏  举报