设计规范和最佳实践

善用finally块

finally块非常厉害, 无论线程抛出什么类型的异常,finally块中的代码都会执行. 经常利用finally块显式释放对象以避免资源泄露.

确保清理代码的执行时很重要的, 以至于许多语言提供了一些构造来简化这种代码. 例如:

  • lock
    • 锁在finally块中释放
  • using
    • finally块中调用对象的Dispose方法
  • foreach
    • finally块中调用IEnumerator对象的Dispose方法.
  • 定义析构器方法(Finalize方法)时
    • finally块中调用基类的Finalize方法

C#编译器会自动生成try/finally块. 重写类的析构器Finalize方法时, 也会生成try/finally块.使用这些构造时,编译器将你写的代码放到try块内部, 并将清理代码放到finally块中.

不要什么都捕捉

绝对不要写大小通吃的类型,悄悄地吞噬异常, 而是应该允许异常在调用栈中向上移动,让应用程序代码针对性地处理它.

吞噬 swallow, 也有作者喜欢说bury, 有人也翻译为隐藏, 简单的说就是自己搞定异常,然后装作异常没有发生.

确实可以在catch块中捕捉System.Exception并执行一些代码, 只要在这个catch块的末尾重新抛出异常. 千万不要捕捉System.Exception异常并悄悄吞噬它而不重新抛出, 否则应用程序不知道已经出错,还是会继续运行.

可以在一个线程中捕捉异常, 在另一个线程中重新抛出异常. 为此提供支持的是异步编程模型.

例如,假定在一个线程池线程执行的代码抛出了异常,CLR捕捉并吞噬这个异常, 并允许线程池返回线程池,稍后会有某个线程调用EndXXX方法来判断异步操作的结果, EndXXX方法将抛出与负责实际工作的那个线程池抛出的一样的异常. 所以, 异常虽然被第一个方法吞噬,但又被调用EndXXX的线程重新抛出,这样,该异常在应用程序面前就不是隐藏的了.

得体地从异常中恢复

try
{
   // 计算电子表格单元格中的值
}
catch (DivideByZeroException)
{
   "不能显示值, 除数为0";
}
catch (OverflowException)
{
   "不能显示值, 溢出"
}

捕捉具体异常时, 应充分掌握在什么时候会抛出异常, 并知道从捕捉的异常类型派生出了哪些类型, 不要捕捉System.Exception(除非你会重新抛出).

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

public void SerializeObjectGraph(FileStream fs, IFormatter formatter, Object rootObj)
{
   // 保存文件的当前位置
   Int64 beforeSerialization = fs.Position;

   try
   {
      // 尝试将对象图序列化到文件中
      formatter.Serialize(fs, rootObj);
   }
   catch // 捕捉所有异常
   {
      // 任何事情出错,就将文件恢复到一个有效状态
      fs.Position = beforeSerialization;

      // 截断文件
      fs.SetLength(fs.Position);

      // 注意: 上述代码没有放到finally块中, 因为只有在序列化失败时才对流进行重置

      // 重新抛出相同的异常,让调用者知道发生了什么
      thorw;
   }
}

为了正确回滚已部分完成的操作, 代码应捕捉所有异常. 是的,这里要捕捉所有异常, 因为你不关心发生了什么错误, 只关心如何将数据结构恢复为一致状态. 捕捉并处理好异常后.不要把它吞噬(假装没发生), 想法,要让调用者知道发生了什么异常, 只需要单独使用C#的关键字throw, 不在关键字throw后指定任何东西.

隐藏实现细节来维系协定

有时需要捕捉一个异常并重新抛出不同的异常. 这样做唯一的原因是维系方法的协定(contract). 另外,抛出的新异常类型应该是一个具体异常(不是其他异常的基类).

例子(伪代码):

文件由于任何原因未找到或者不能读取, 调用者将看到一个FileNotFoundException或者IOException异常, 因为这两个异常都不是调用者预期的, 因为文件存在与否以及能否读取不是方法的隐式协定的一部分, 调用者根本猜不到(猜不到PhoneBook类的方法要从文件中读取数据). 所以GetPhoneNumber方法会捕捉这两种异常,抛出一个新的NameNotFoundException异常.

NameNotFoundException异常为调用者提供了理解其中原因的一个抽象视图,将内部异常设为FileNotFoundException或者IOException异常 是非常重要的一环,这样才能保证不丢失造成异常的真正原因.

这个技术会使调试变得困难,务必慎用. 有时开发人员之所以捕捉一个异常并抛出一个新异常,目的是在异常中添加额外的数据或上下文. 如果只是这个目的,那么只需捕捉希望的异常类型,在异常对象的Data属性(一个键值对)中添加数据, 然后重新抛出相同的异常对象:

catch(IOException e) { e.Data.Add("Filename",filename); throw; }

将文件名添加到IOException对象中,这样重新抛出同一个异常对象,只是它现在包含额外的数据.

未处理的异常

异常抛出时, CLR在调用栈中向上查找与抛出的异常对象的类型匹配的catch块. 没有任何catch块匹配抛出的异常类型. 就会发生一个未处理的异常. CLR检测到任何线程中的未处理异常都会终止进程.

当应用程序的开发人员才需关心未处理的异常. 应用程序发生未处理的异常时, windows会向事件日志写一条记录. 查看该记录: 事件查看器-> windows日志->应用程序节点

系统托盘中的小旗->打开操作中心->展开维护->单击查看可靠性历史记录

会在底部的窗格看到应用程序由于未处理的异常而终止.

对异常进行调试

针对VS. 具体看书.

异常处理的性能问题

个别时候会遇到频繁调用但频频失败的方法, 这时抛出异常所造成的性能损失可能令人无法接受. 例如: 在调用Int32Parse方法时, 最终用户经常输入无法解析的数据, 由于频繁调用Parse方法, 抛出和捕捉异常对总体性能造成了很大影响. 为了解决这个问题,Microsoft为Int32类型添加了新的TryParse方法.

public static Boolean TryParse(String s, out Int32 result);
public static Boolean TryParse(String s, NumberStyle style, IFrmatProvider provider, out Int32 result);

这些方法都返回一个Boolean,指明传给方法的String是否包含了能解析成Int32的字符. 它们同时返回一个名为result的输出参数.

  • 如果方法返回true,result将包含字符串解析成32位整数的结果
  • 返回false,result将包含0, 这时自然不应再执行任何代码去检查result.
    • 如果TryXX方法的Boolean返回值为false,那么代表的只是一种错误. 方法仍要为其他错误抛出异常.
    • 例如: 为style参数传递的实参无效,Int32TryParse方法会抛出一个ArgumentException异常,另外调用TryParse方法时仍有可能抛出一个OutMemoryException异常

定义类型成员时,应确保在一般使用情形中不会失败. 只有用户以后因为抛出异常而堆性能不满意时, 才应考虑添加以下TryXxx方法.

约束执行区域(CER)

许多应用程序都不需要健壮到能从任何错误恢复的地步.

CLR中, 我们有包含了状态的AppDomain. 卸载时,它的所有状态都会卸载. 所以,如果AppDomain中的一个线程遭遇未处理的异常,可以在不终止整个进程的情况下卸载AppDDomain(销毁它的所有状态).

线程的整个生命周期都在一个AppDomain的内部. 这个说法是成立的.

根据定义, CER是必须对错误有所适应的代码块. 一般用CER处理多个AppDomain或进程共享的状态, 称这些异常为异步异常. CER就非常有用.

例如, 调用方法时,CLR必须加载程序集,在AppDomainLoader堆中创建类型对象, 调用类型的静态构造器,并将IL代码JIT编译成本机代码. 这些操作都可能失败,CLR通过抛出异常来报告失败.

如果任何这些操作再一个catch或finally块中失败, 你的错误恢复或资源清理代码就不会完整的执行.

private static void Demo1()
{
    Console.WriteLine("In Demo1");
    try
    {
        Console.WriteLine("In try");
    }
    finally
    {
        // 隐式调用Type1的静态构造器
        Type1.M();
    }
}

private sealed class Type1
{
    static Type1()
    {
        // 如果这里抛出异常,M就得不到调用
        Console.WriteLine("Type1's static ctor called");
    }
    public static void M()
    {}
}

上述代码存在一个问题,如果在静态构造器中发生异常,M就得不到调用, M就得不到调用.

想要达到的目的是,除非 保证(大致保证) 关联的catchfinally块中的代码得到执行, 否则上述try块的代码根本不要开始执行.为了这个目的,修改成下面这样:

保证和大致保证分别对应后文所说的WillNotCorrupStateMayCorrupInstance两个枚举成员

private static void Demo2()
{
    Console.WriteLine("In Demo2");
    // JIT编译器如果发现一个`try块`之前调用了这个方法,就会提前编译与`try块`关联的`catch`和`finally块`中的代码.
    // 强迫finally块中的代码提前准备好
    RuntimeHelpers.PrepareConstrainedRegions(); //在 System.Runtime.CompilerServices命名空间
    try
    {
        Console.WriteLine("In try");
    }
    finally
    {
        // 隐式调用Type2的静态构造器
        Type2.M();
    }
}

public class Type2
{
    static Type2()
    {
        Console.WriteLine("Type2's static ctor called");
    }

    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
    public static void M()
    {}
}

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

JIT编译器提前准备方法时, 还会遍历整个调用图, 提前准备被调用的方法, 前提是这些方法应用了[ReliabilityContract]特性. 向这个特性实例的构造器传递的是WillNotCorrupState或者MayCorrupInstance枚举成员.

这是由于加入方法会损坏AppDomain或进程的状态,CLR便无法对一致性做出任何保证. 在通过一个PrepareConstrainedRegions调用来保护的一个catch或finally块中, 请确保只条用根据刚才的描述设置了[ReliabilityContract]的方法.

此特性允许开发者向方法的潜在调用者申明方法的可靠性协定.

如何使用这个特性

RuntimeHelpers的另一个方法

代码协定

协定默认只作为文档使用, 为了发掘协定的附加价值,必须下载额外的工具和一个VS属性窗格.

以下选看:

代码协定 提供了直接在代码中声明代码设计决策的一种方式. 这些协定采取以下的形式:

  • 前条件
    • 一般用于对实参进行验证
  • 后条件
    • 方法因为一次普通的返回或者抛出异常而终止时,对状态进行验证
  • 对象不变性
    • 在对象的整个生命期内,确保对象的字段的良好状态

可将上述想象成方法签名的一部分.

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

// 源码
public static class Contract
{
   // 前条件方法: [Conditional("CONTRACTS_FULL")]
   public static void Requires(Boolean condition);
   public static void EndContractBlock();

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


   // 后条件方法: [Conditional("CONTRACTS_FULL")]
   public static void Ensures(Boolean condition);
   public static void EnsureOnThrow<TException>(Boolean condition)
        where TException : Exception;

   // 特殊后条件方法 Always
   public static T Result<T>();
   public static T OldValue<T>(T value);
   public static T ValueAtReturn<T>(out T value);

   // 对象不变性方法 [Conditional("CONTRACTS_FULL")]
   public static void Invariant(Boolean condition);

   .....//限定符方法,辅助方法,基础结构事件 请查看源码


}

[Conditional("CONTRACTS_FULL")][Conditional("DEBUG")]除非定义了恰当的符号,否则编译器会忽略调用这些方法的任何代码. 协定默认只作为文档使用, 因为生成项目没有定义CONTRACTS_FULL符号

标记Always的任何方法意味着编译器总是生成调用方法的代码.

Requires,Requires<TException>,Ensures,EnsureOnThrow<TException>,Invariant,Assert,Assume方法有一个额外的重载版本这里没有列出, 它获取一个String实参,用于显式指定违反协定时显示的字符串消息.

internal static class CodeContracts
{
    public static void Go()
    {
        var shoppingCart = new ShoppingCart();
        shoppingCart.AddItem(new Item());
    }

    public sealed class Item
    {
        /* ... */
    }

    public sealed class ShoppingCart
    {
        private List<Item> m_cart      = new List<Item>();
        private Decimal    m_totalCost = 0;

        public ShoppingCart()
        {
        }

        public void AddItem(Item item)
        {
            AddItemHelper(m_cart, item, ref m_totalCost);
        }

        private static void AddItemHelper(List<Item> m_cart, Item newItem, ref Decimal totalCost)
        {
            // 前条件
            // 1. 前条件指出newItem不能为null
            // 2. 而且要添加的商铺不在购物车中
            Contract.Requires(newItem != null);
            Contract.Requires(Contract.ForAll(m_cart, s => s != newItem));

            // 后条件
            // 1. 新商品必须在购物车中
            // 2. 而且总价格至少要与将商铺添加到购物车之前一样多
            // 3. 因为某个原因抛异常,那么totalCost不发生变化
            Contract.Ensures(Contract.Exists(m_cart, s => s == newItem));
            Contract.Ensures(totalCost >= Contract.OldValue(totalCost));
            Contract.EnsuresOnThrow<IOException>(totalCost == Contract.OldValue(totalCost));

            // 做一些事情,可能抛出IOException
            m_cart.Add(newItem);
            totalCost += 1.00M;
            //throw new IOException(); // 证明违反协定
        }

        // 对象不变性
        [ContractInvariantMethod]
        private void ObjectInvariant()
        {
            // 确保对象的m_totalCost永远不包含负值
            Contract.Invariant(m_totalCost >= 0);
        }
    }

后续不知道在讲啥玩意… 工作原理..