异常和状态管理

定义错误,如何从错误中恢复. 错误在不恰当的时候发生,代码可能在状态改变的中途发生错误,这时需要将一些状态还原为改变之前的样子. 还要通知调用者有错误发生.

定义”异常”

设计类型时要想好各种使用情况. 当一个类型的成员(如方法,属性)不能完成任务时,就应抛出异常。

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

internal class Account
{
   // 两个账户,转账金额
   // 此方法的作用是从一个函数扣钱,添加到另一个账户中
   public static void Transfer(Account from, Account to, Decimal amount)
   {
      from -= amount;
      to +=amount;
   }
}

以上转账方法可能有多种原因导致失败:

  • from,to实参可能是0
  • from账户没有足够的资金
  • to账户可能资金过多,以至于增加资金时导致账户溢出
  • amount参数为0,负数或者小数超过2位

Boolean f = "Jeff".Substring(1,1).ToUpper().EndsWith("E"); // true

利用C#扩展方法将多个操作链接到一起, 但是有一个重要的前提,这行代码没有操作失败和中途出错. 为了让不返回错误代码的方法报告错误, .NetFramework通过异常处理来解决这个问题.

异常处理机制

本文旨在提供何时以及如何使用异常处理的设计规范.

.NetFramework异常机制使用Windows提供的结构化异常处理(Structured Exception Handling,SEH)机制构建的. (Windows核心编程(第5版)一书,其中有3章专门谈论SEH).

异常处理机制的标准用法:

public static void SomeMethod()
{
    try
    {
        // 需要进行恢复 和/或 清理的代码放在这里
    }
    catch (InvalidOperationException)
    {
        // 从 InvalidOperationException 恢复的代码放这
    }
    catch (IOException)
    {
        // 从 IOException 恢复的代码放这
    }
    catch
    {
        // 从除了上述异常之外的其他所有异常恢复的代码放在这里
        throw; // 如果什么异常都捕捉,通常需要重新抛出异常
    }
    finally
    {
        // 这里的代码对始于try块的任何操作进行清理
        // 这里的代码总是执行,不管是不是抛出了异常
    }

    // 如果try块没有抛出异常, 或者某个catch块捕捉到异常,但没有抛出或重新抛出就执行下面的代码
    ...
}

大多数情况只有一个try块和一个匹配finally块, 或者一个try块和一个匹配的catch块

try块

如果代码需要执行一般性的资源清理操作,需要从异常中恢复,或者两者都需要,就可以放到try块. 负责清理的代码应放到一个finally块. try块还可包含也许会抛出异常的代码, 负责异常恢复的代码应放到一个或多个catch块中. 针对应用程序能从安全恢复的每一种异常,都应该创建一个catch块中. 单独的try块没有意义C#也不允许.

重要提示: 开发人员有时不知道应该在一个try块中放入多少代码, 这具体取决于状态管理. 如果在一个try块中执行多个可能抛出同一个异常类型的操作, 但不同的操作有不同的异常恢复措施,就应该将每个操作都放到它自己的try块中, 这样才能正确地恢复状态.

catch块

catch包括的是响应一个异常需要执行的代码. 如果try块没有抛出异常,CLR永远不会执行它的任何catch块. 线程将跳过所有的catch块,直接执行finally(如果有的话). 然后继续执行finally之后的语句.

catch关键字后的圆括号中的表达式称为捕捉类型. C#要求捕捉类型必须是System.Exception或者是它的派生类型. 最后一个catch块没有指定捕捉类型,能处理除了前面的catch块指定的之外的其他所有异常. 这相当于捕捉System.Exception(只是catch块大括号中的代码访问不了异常信息).

可以在监视窗口中添加特殊变量名称$exception来查看当前抛出的异常对象

CLR自上而下搜索匹配的catch块. 较具体的异常类型放在顶部, 最后是System.Exception.

try块的代码(或者从try块调用的任何代码)中抛出异常, CLR将搜索捕捉类型与抛出的异常相同的catch块. 如果没有匹配的,CLR回去调用更高的一层去搜索与异常匹配的捕捉类型, 如果到了栈的顶部还是没有找到匹配的catch块,就会发生未处理的异常.

一旦CLR找到匹配的catch块,就会执行内层所有finally块中的代码..

所谓内层finally块是指从抛出异常的try块开始, 到匹配异常的catch块之间的所有finally

注意,匹配异常的那个catch块所关联的finally块尚未执行,该finally块中的代码一直要等到这个catch块中的代码执行完毕后才会执行.

所有内层finally块执行完毕之后,匹配异常的那个catch块中的代码开始执行. catch块中的代码通常执行一些对异常处理的操作, 在catch块的末尾,有以下三个选择:

  • 重新抛出相同的异常,向调用栈高一层的代码通知该异常的发生
  • 抛出一个不同的异常,想调用栈高一层的代码提供更丰富的异常信息.
    • 这两种技术将抛出异常,CLR的行为就和之前说的,回溯调用栈(去更高一层查找捕捉类型和抛出的异常类型匹配的catch块).
  • 让线程从catch块的底部退出(正常执行下去,并执行匹配的finally块).
    • 最后一种是当线程catch块的底部退出后,立即执行包含在finally块中的代码. 执行完毕后,线程退出finally块.

C#允许在捕捉类型后指定一个变量. 该变量引用抛出异常的对象,catch块可以通过引用此变量来访问异常的具体信息(例如发生时的堆栈跟踪), 最好把它当成是只读的,不建议修改.

你的代码可以向AppDomain的FirstChanceException事件登记, 这样只要AppDomain中发生异常,就会收到通知. 这个通知是在CLR开始搜索任何catch块之前 发生的.

finally块

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

例如,在try块中打开了文件,就应该将关闭文件的代码放到finally块中.

try
{
   // 不管此句代码是否会发生异常, 文件保证会在finally块中被关闭.
   fs = FileStream(pathname, FileMode.Open);
}
catch(IOException)
{
   // 在此添加从IOException恢复的代码
}
finally
{
   // 确保文件被关闭
   if(fs != null) fs.Close();
}

// 这句话不能发在外部, 因为如果没有捕捉到异常, finally块中后面的语句就不会执行
// 以至于文件不会被关闭, 造成文件一直保持打开状态(直到下一次垃圾回收)
// if(fs != null) fs.Close();

一个try块最多只能关联一个finally块,而且必须出现在所有的catch块之后.

记住finally块中的代码是清理代码. 这些代码只需对try块中发起的操作进行清理.

如果在catch块和finally块内部抛出了异常,CLR的异常机制仍会正常运转. 就好像异常是在finally块之后抛出的一样. 出现这种情况,CLR不会记录对应的try块中抛出的第一个异常,关于第一个异常的所有堆栈跟踪信息都将丢失, 并且程序也会被CLR终止.

CLS和非CLS异常

C#编译器值允许代码抛出从Exception派生的对象,而用其他一些语言写的代码不仅允许抛出Exception派生对象,还允许抛出非Exception派生对象.

CLR2.0的版本中引入了新的RuntimeWrappedException类,派生自Exception. WrappedException含有一个Object类型的私有字段, 在非CLS相容的一个异常被抛出时, CLR会自动构造这个类的实例, 初始化这个字段,使之引用实际抛出的对象. 这样就能捕捉非CLS相容的异常, 任何捕捉Exception类型的代码都能捕捉非CLS相容的异常.消除了安全隐患.

Exception类

Exception是一个很简单的类型,一般不写任何代码以任何方式查询或访问这些属性. 在应用程序因为未处理的异常而终止时, 可以在调试器中查看这些属性,或者在Windows应用程序事件日志或崩溃转储(crash dump)中查看.

  • 只读StackTrace属性:
    • catch块可读取该属性来获取一个堆栈跟踪(stack trace). 它描述了异常发生前调用了哪些方法.
    • 访问该属性实际会调用CLR中的代码
    • 该属性并不是简单地返回一个字符串
      • 构造Exception派生类型的新对象时,StackTrace属性被初始化为null

一个异常抛出时, CLR在内部记录throw指令的位置(抛出位置). 一个catch块捕捉到该异常时, CLR会记录捕捉位置. 在catch块内访问被抛出的异常对象的StackTrace属性, 负责实现该属性的代码会调用CLR内部的代码, 后者创建一个字符串来指出从异常抛出位置到异常捕捉位置的所有方法.

抛出异常时, CLR会重置异常起点,也就是说,CLR只记录最新的异常对象的抛出位置.

private void SomeMethod()
{
   try { ... }
   catch (Exception e)
   {
      ...
      // 抛出
      throw e; // CLR认为这是异常的起点, Fxcop报错
   }
}

private void SomeMethod2()
{
   try { ... }
   catch (Exception e)
   {
      ...
      // 重新抛出异常
      throw; // 不影响CLR对异常起点的认知,Fxcop不再报错
   }
}

两个方法的区别是: CLR对于异常起始抛出位置的认知. window都会重置栈的起点. 如果有一个异常成为未处理的异常, 那么向Window Error Reporting报告的栈位置就是最后一次抛出或重新抛出的位置(即使CLR知道异常的原始抛出位置).

StackTrace属性返回的字符串不包含调用中比接受异常对象的那个catch高的任何方法. 要获得从线程起始处到异常处理程序(catch块)之间的完整堆栈跟踪, 需要使用System.Diagnostics.StackTrace类型, 该方法定义了一些属性和方法,允许开发人员程序化地处理堆栈以及构成堆栈跟踪的栈帧.

栈帧: 代表当前线程调用栈中的一个方法调用. 执行线程的过程中进行的每个方法调用都会在调用栈中创建并压入一个StackFrame.

如果CLR能找到你的程序集的调试符号(存储在.pdb文件中), 那么在System.ExceptionStackTrace属性或者System.Diagnostics.StackTrace的ToString方法返回的字符串中,将包括源代码文件路径和代码行号. 这些信息对于调试很有用.

获得堆栈跟踪后,可能发现一些方法没有出现在堆栈跟踪字符串中. 有两方面的原因:

  • 调用栈记录的是线程的返回位置(而非来源位置)
  • JIT编译器可能进行了优化
    • 将一些方法内联,以避免调用单独的方法并从中返回的开销.
    • /debug命令行开关,使用这个开关可以在生成的程序集中嵌入信息,告诉编译器不要内联程序集中的任何方法. 确保调试人员获得完整的更有意义的堆栈跟踪.

用特性禁止方法内联

设置/debug命令行开关就是设置了DebuggableAtrriable定制特性,如果指定了DisableOptimizations标志,JIT编译器就不会对程序集的方法进行任何内联.

单独方法则应用MethodImpIAttribute特性.

FCL定义的异常类

Exception派生类的结构层次:

//#define TEST
//#define VERIFY

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;


public static class Program
{
    public static void Main()
    {
        // 显式加载想要反射的程序集
        LoadAssemblies();

        // 对所有类型进行筛选和排序
        var allTypes =
            (from a in new[] {typeof(Object).Assembly} // AppDomain.CurrentDomain.GetAssemblies()
                from t in a.ExportedTypes
                where typeof(Exception).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo())
                orderby t.Name
                select t).ToArray();

        // 生成并显示继承层次结构
        Console.WriteLine(WalkInheritanceHierarchy(new StringBuilder(), 0, typeof(Exception), allTypes));
    }

    private static StringBuilder WalkInheritanceHierarchy(StringBuilder sb, Int32 indent, Type baseType,
        IEnumerable<Type> allTypes)
    {
        String spaces = new String(' ', indent * 3);
        sb.AppendLine(spaces + baseType.FullName);
        foreach (var t in allTypes)
        {
            if (t.GetTypeInfo().BaseType != baseType) continue;
            WalkInheritanceHierarchy(sb, indent + 1, t, allTypes);
        }

        return sb;
    }

    private static void LoadAssemblies()
    {
        String[] assemblies =
        {
            "System,                        PublicKeyToken={0}",
            "System.Core,                   PublicKeyToken={0}",
            "System.Data,                   PublicKeyToken={0}",
//            "System.Design,                 PublicKeyToken={1}",
//            "System.DirectoryServices,      PublicKeyToken={1}",
//            "System.Drawing,                PublicKeyToken={1}",
//            "System.Drawing.Design,         PublicKeyToken={1}",
//            "System.Management,             PublicKeyToken={1}",
//            "System.Messaging,              PublicKeyToken={1}",
//            "System.Runtime.Remoting,       PublicKeyToken={0}",
//            "System.Runtime.Serialization,  PublicKeyToken={0}",
//            "System.Security,               PublicKeyToken={1}",
//            "System.ServiceModel,           PublicKeyToken={0}",
//            "System.ServiceProcess,         PublicKeyToken={1}",
//            "System.Web,                    PublicKeyToken={1}",
//            "System.Web.RegularExpressions, PublicKeyToken={1}",
//            "System.Web.Services,           PublicKeyToken={1}",
//            "System.Xml,                    PublicKeyToken={0}",
//            "System.Xml.Linq,               PublicKeyToken={0}",
            "Microsoft.CSharp,              PublicKeyToken={1}",
        };

        const String EcmaPublicKeyToken = "b77a5c561934e089";
        const String MSPublicKeyToken   = "b03f5f7f11d50a3a";

        // 获取包含 System.Object的程序集的版本
        // We'll assume the same version for all the other assemblies
        Version version = typeof(System.Object).Assembly.GetName().Version;

        // 显式加载想要反射的程序集
        foreach (String a in assemblies)
        {
            String AssemblyIdentity =
                String.Format(a, EcmaPublicKeyToken, MSPublicKeyToken) +
                ", Culture=neutral, Version=" + version;
            Assembly.Load(AssemblyIdentity);
        }
    }
}


System.Exception
   System.AggregateException
   System.ApplicationException
      System.Reflection.InvalidFilterCriteriaException
      System.Reflection.TargetException
      System.Reflection.TargetInvocationException
      System.Reflection.TargetParameterCountException
      System.Threading.WaitHandleCannotBeOpenedException
   System.Diagnostics.Contracts.ContractException
   System.Diagnostics.Tracing.EventSourceException
   System.InvalidTimeZoneException
   System.Threading.LockRecursionException
   System.Runtime.CompilerServices.RuntimeWrappedException
   System.SystemException
      System.Threading.AbandonedMutexException
      System.AccessViolationException
      System.Reflection.AmbiguousMatchException
      System.ArgumentException
         System.ArgumentNullException
         System.ArgumentOutOfRangeException
         System.Globalization.CultureNotFoundException
         System.Text.DecoderFallbackException
         System.DuplicateWaitObjectException
         System.Text.EncoderFallbackException
      System.ArithmeticException
         System.DivideByZeroException
         System.NotFiniteNumberException
         System.OverflowException
      System.ArrayTypeMismatchException
      System.BadImageFormatException
      System.Security.Cryptography.CryptographicException
      System.DataMisalignedException
      System.ExecutionEngineException
      System.Runtime.InteropServices.ExternalException
         System.Runtime.InteropServices.COMException
         System.Runtime.InteropServices.SEHException
      System.FormatException
         System.Reflection.CustomAttributeFormatException
      System.IndexOutOfRangeException
      System.InsufficientExecutionStackException
      System.InvalidCastException
      System.Runtime.InteropServices.InvalidComObjectException
      System.Runtime.InteropServices.InvalidOleVariantTypeException
      System.InvalidOperationException
         System.ObjectDisposedException
      System.InvalidProgramException
      System.IO.IOException
         System.IO.DirectoryNotFoundException
         System.IO.EndOfStreamException
         System.IO.FileLoadException
         System.IO.FileNotFoundException
         System.IO.PathTooLongException
      System.Collections.Generic.KeyNotFoundException
      System.Runtime.InteropServices.MarshalDirectiveException
      System.MemberAccessException
         System.FieldAccessException
         System.MethodAccessException
         System.MissingMemberException
            System.MissingFieldException
            System.MissingMethodException
      System.Resources.MissingManifestResourceException
      System.Resources.MissingSatelliteAssemblyException
      System.MulticastNotSupportedException
      System.NotImplementedException
      System.NotSupportedException
         System.PlatformNotSupportedException
      System.NullReferenceException
      System.OperationCanceledException
         System.Threading.Tasks.TaskCanceledException
      System.OutOfMemoryException
         System.InsufficientMemoryException
      System.RankException
      System.Reflection.ReflectionTypeLoadException
      System.Runtime.InteropServices.SafeArrayRankMismatchException
      System.Runtime.InteropServices.SafeArrayTypeMismatchException
      System.Security.SecurityException
      System.Threading.SemaphoreFullException
      System.Runtime.Serialization.SerializationException
      System.StackOverflowException
      System.Threading.SynchronizationLockException
      System.Threading.ThreadAbortException
      System.Threading.ThreadInterruptedException
      System.Threading.ThreadStartException
      System.Threading.ThreadStateException
      System.TimeoutException
      System.TypeInitializationException
      System.TypeLoadException
         System.DllNotFoundException
         System.EntryPointNotFoundException
         System.TypeAccessException
      System.TypeUnloadedException
      System.UnauthorizedAccessException
      System.Security.VerificationException
   System.Threading.Tasks.TaskSchedulerException
   System.TimeZoneNotFoundException

Microsoft本来是打算将System.Exception类型作为所有异常的基类型, 而另外两个类型System.SystemExceptionSystem.ApplicationException是唯一直接从Exception派生的类型.另外CLR抛出的所有异常都从SystemException派生,应用程序抛出的异常都从ApplicationException派生, 这样就可以写一个catch块来捕捉CLR抛出的所有异常或者应用程序抛出的所有异常.

从上段代码中看出, 规则没有得到严格遵守. 有的异常直接从System.Exception派生, 有的CLR异常从ApplicationException中派生, 这根本就是一团糟, 所以上述2个类型现在没有特殊含义, 也不方便去改正,这会破坏现有的代码对这两个类型的引用.

抛出异常

实现自己的方法时, 如果方法无法完成方法名所指明的任务, 就应抛出一个异常. 要考虑两个问题:

  1. 是抛出什么Exception派生类型. 应选择一个有意义的类型.
    1. 强烈建议定义浅而宽的异常类型层次结构, 以创建尽量少的基类.
    2. 原因是基类的主要作用就是将大量错误当做一个错误,而这通常是危险的.
    3. 基于这样的考虑永远不要抛出一个Exception对象,抛出其他任何基类异常类型时也要特别谨慎.

事实上,System.Exception 类标记为 abstract 在编译时旧禁止代码试图抛出它.

  1. 向异常类型的构造器传递什么字符串消息
    1. 抛出异常时应包含一条字符串消息,详细说明方法为什么无法完成任务. 在未处理的情况下通常会被写入日志.

定义自己的异常类

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

以下写了一个自己的泛型Exception<TExceptionArgs>类:

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

/// <summary>表示在应用程序执行期间发生的错误</summary>
/// <typeparam name="TExceptionArgs">异常的类型和与之关联的任何附加参数</typeparam>
[Serializable]
public sealed class Exception<TExceptionArgs> : Exception, ISerializable where TExceptionArgs : ExceptionArgs
{
    private const    String         c_args = "Args"; // 用于序列化和反序列化
    private readonly TExceptionArgs m_args;

    /// <summary>返回对此异常的附加参数的引用</summary>
    public TExceptionArgs Args
    {
        get { return m_args; }
    }

    /// <summary>
    /// 使用指定的错误消息和对导致此异常的内部异常的引用初始化异常类的新实例。
    /// </summary>
    /// <param name="message">解释异常原因的错误消息.</param>
    /// <param name="innerException">导致当前异常的异常,如果没有指定内部异常,则为空引用
    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;
    }

    // 这个构造器用于反序列化: 由于类是密封的,所以构造器是私有的
    // 如果这个类不是密封的,这个构造器就应该受保护的
    [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
    private Exception(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
        // 让基类反序列化它的字段
        m_args = (TExceptionArgs) info.GetValue(c_args, typeof(TExceptionArgs));
    }

    // 这个方法用于序列化,由于ISerializable接口,所以它是公共的
    [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();
    }
}

/// <summary>
/// 自定义异常派生的基类,以便添加自己的异常参数。
/// </summary>
[Serializable]
public abstract class ExceptionArgs
{
    /// <summary>The string message associated with this exception.</summary>
    public virtual String Message
    {
        get { return String.Empty; }
    }
}

[Serializable]
public sealed class DiskFullExceptionArgs : ExceptionArgs
{
    private readonly String m_diskpath; // 构造时设置私有字段

    public DiskFullExceptionArgs(String diskpath)
    {
        m_diskpath = diskpath;
    }

    // 返回字段的公共只读属性
    public String DiskPath
    {
        get { return m_diskpath; }
    }

    // 重写Message属性来包含我们的字段
    public override String Message
    {
        get { return (m_diskpath == null) ? base.Message : "DiskPath=" + m_diskpath; }
    }
}

用可靠性换取开发效率

编译器隐式地做下面这些事情:

  • 调用方法时插入可选参数
  • 对值类型的实例进行装箱
  • 构造/初始化参数数组
  • 绑定到dynamic变量/表达式的成员
  • 绑定到扩展方法
  • 绑定/调用重载的操作符(方法)
  • 构造委托对象
  • 在调用泛型,声明局部变量和使用lambda表达式时推断类型
  • 为lambda表达式和迭代器定义/构造闭包类
    • 闭包(closure)是由编译器生成的数据结构(一个C#类), 其中包含一个表达式以及对表达式进行求值所需的变量(C#的公共字段). 变量运行在不改变表达式签名的前提下,将数据从表达式的一次调用传递到下一次调用.
  • 定义/构造/初始化匿名类型及其实例
  • 重写代码来支持LINQ查询表达式和表达式树

CLR隐式做了下面这些事情:

  • 调用虚方法和接口方法
  • 加载程序集并对方法进行JIT编译,可能会抛出以下异常
    • FileLoadException
    • BadImageFormatException
    • InvalidProgramException
    • FieldAccessException
    • MethodAccessException
    • MissingFieldException
    • VerificationException
  • 访问MarshalByRefObject派生类型的对象时穿越AppDomain边界
    • 可能抛出AppDimainUnloadedException
  • 穿越AppDomain边界时序列换和反序列化对象
  • 调用Thread.AbortAppDomain.Unload时造成线程抛出ThreadAbortException
  • 垃圾回收之后,在回收对象的内存之前调用Finalize方法
  • 使用泛型类型时,在Loader堆中创建类型对象.
    • 每个AppDomain都有一个自己的托管堆,这个托管堆内部又按照功能进行了不同的划分,其中最重要的就是GC堆Loader堆,Loader堆负责存储类型的元数据,也就是所谓的类型对象, 在每个类型对象的末尾,都含有一个方法表.
  • 调用类型的静态构造器(类型构造器)
    • 可能抛出TypeInitalizationException
  • 抛出各种异常

异常的好处在于,未处理的异常会造成应用程序终止,再是但测试和部署之后不希望发生应用程序终止,就会插入System.Exception,也就是所有溢出的基类, 捕捉到异常并使程序能够继续运行.

例如前面的转账代码: 如果在扣钱后,添加给to钱之前发生异常,如果捕捉异常继续运行,就会发生安全性bug, 如果捕捉Exception,并将钱还给from账户,如果转账这个方法简单,这个方案确实可行. 但是Transfer方法还要生成关于取钱的审计记录,或者其他线程同时操作同一个账户,那么撤销undo操作本身就可能失败,造成抛出其他异常.

为了缓解对状态的破坏, 可以做下面几件事情.

  • 执行catch或Finally块中的代码时, CLR不允许线程终止. 所以可以像下面这样使Transfer方法变得更健壮
    • 但绝对不建议将所有代码都放到finally块中,这个技术只适合修改极其敏感的状态.
try{/* 什么都不做 */}
finally
{
    from -= amount;
    // 现在,这里不可能因为 Thread.Abort或AppDomain.Unload而发生线程终止
    to += amount;
}
  • 可以用System.Diagnostics.Contracts.Contract类向方法应用代码协定. 通过代码协定,在用实参和其他变量对状态进行修改之前, 可以先对这些实参和变量进行验证. 如果实参/变量遵守协定,状态被破坏的可能性将大幅降低(但不能完全消除). 如果不准守协定,则在修改任何状态前抛出异常.
  • 可以使用约束执行区域(Constrained Execution Region,CER), 它能消除CLR的某些不确定性. 例如, 可让CLR在进入try块之前加载与这个try块关联的任何catchfinally块需要的程序集. 此外CLR会编译catch``和finally块中的所有代码, 包括从这些块中调用的所有方法. 这样尝试执行catch块的错误回复代码或者finally块中的清理代码时,可以消除众多潜在的异常(包括FileLoad,MissingMember等异常).
  • 取决于状态存在于何处, 可以利用事务(transaction)来确保状态要么都修改,要么都不修改.
    • 例如, 如果数据在数据库中,事务能很好地工作. WINDOW现在还支持事务式的注册表和文件操作(仅限NTFS卷), 但是.NetFramework目前没有直接公开这个功能, 必须P/Invoke本机代码才行.
      • P/Invoke 平台调用
      • 参考TransactionScope类了解细节.
  • 使自己的方法设计更明确.
public static class SomeType
{
   private static Object s_myLockObject = new Object();

   // 不建议的做法
   public static void SomeMethod()
   {
      // 这个方法已经不建议使用,
      // 如果抛出异常,是否获取了锁? 如果已经获取了锁,它就得不到释放
      Monitor.Enter(s_myLockObject);

      try
      {
         // 在这里执行线程安全的操作...
      }
      finally
      {
         Monitor.Exit(s_myLockObject);
      }
   }

   // 建议做法
   public static void SomeMethod()
   {
      Boolean lockTaken = false; // 假定没有获取锁
      try
      {
         // 无论是否抛出异常, 以下代码都能正常工作!
         Monitor.Enter(s_myLockObject, ref lockTaken);
         // 在这里执行线程安全的操作...
      }
      finally
      {
         // 如果已获取锁,就释放它
         if (lockTaken)
         {
            Monitor.Exit(s_myLockObject);
         }

      }
   }
}

虽然上述建议的写法使方法变得更明确, 但在线程同步锁的情况下, 现在的建议是根本不要随同异步处理使用它们.(参考30章混合线程同步构造).

关于状态已损坏之后的处理

如果确定状态已经损坏到无法修复的成都,就应该销毁所有损坏的状态,防止它造成更多的伤害. 然后重启应用程序,将状态初始化到良好状态.

  1. 因为托管的状态泄露不到AppDomain外部,所以为了销毁AppDomain中的所有损坏状态, 调用AppDomian.Unload方法来卸载整个AppDomain.

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

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

这个方法终止进程时, 不会允许任何活动的try/catch块或者Finalize方法(注意不是finally块). 之所以这样做, 是因为在状态已损坏的前提下执行更多的代码, 很容易使局面变得更坏. 不过FailFast为从CriticalFinalizaerObject派生的任何对象提供了进行清理的机会, 因为它们一般只是关闭本机资源; 而即使CLR或者你的应用程序的状态发生损坏.window状态可能是好的. FailFast方法将消息字符串和可选的异常(通常是catch捕捉到的异常)写入Windows Application事件日志,生成window错误报告,创建应用程序的内存转储(dump),然后终止当前进程.

以上讨论主要是为了让你意识到CLR异常处理机制存在的一些问题. 大多数应用程序都不能容忍状态受损而继续运行. 因为这样会造成不正确的数据,甚至可能造成安全漏洞.