• 《Effective C#》笔记(5)


    程序总是会出错的,因为即便开发者做得再仔细,也还是会有预料不到的情况发生。令代码在发生异常时依然能够保持稳定是每一位C#程序员所应掌握的关键技能。
    .NET Framework Design Guidelines建议,如果方法不能完成调用者所请求的操作,那就可以考虑抛出异常,此时必须提供各种信息,使得调用者能够据此诊断问题。
    此外,还必须保证如果应用程序能够从错误中恢复,那么必须处在某种已知的状态。

    考虑在方法约定遭到违背时抛出异常

    如果方法不能够履行它与调用者所订立的契约,那就应该让它其抛出异常。这些无法履约的情况都应该通过异常来表示。然而要注意,由于异常并不适合当作控制程序流程的常规手段,因为抛出异常开销很大,而且会导致代码中很多try-catch。因此,还应该同时提供另外一套方法,使得开发者可以在执行操作之前先判断该操作能否顺利执行,以便在无法顺利执行的情况下采取相应的措施,而不是等到抛出了异常之后再去处理。
    用类库的的File.Open来举例,它在无法完成操作时会抛出异常;但同时也提供了File.Exists来判断文件是否存在。所以调用者可以在Open前先判断Exists,当然除了文件不存在,文件被占用、没有权限等也会导致Open失败,这是文件操作的细节问题,但这种设计思路是可以借鉴的。
    假设提供DoWork方法,按照前面的思路,可以这样实现

    public bool TryDoWork()
    {
      if(!TestConditions())
        return false;
      DoWork();
      return true;
    }
    
    public void DoWork(){...}
    
    public bool TestConditions()
    {
      ...
    }
    

    专门针对应用程序创建异常

    异常是一种用来报告错误的机制,有时需要创建自定义的异常。但首先要明确,并不是所有错误都必须表示成异常,至于哪些错误才需要用异常来表示,并没有固定的规律可循。一般来说,如果某种状况必须立刻得到处理或汇报,否则将长期影响应用程序,那么就应该抛出异常,比如数据库发生了数据完整性问题,就需要立刻抛出异常;但如果只是无法把某个试图的折叠、打开状态记录下来,因为不会造成严重的影响,则可以考虑只返回错误码。

    然后,也不需要为所有的throw语句都新建一种异常类,但统统用Exception基类来抛出也不合适。
    之所以要创建不同的异常类,主要原因就是为了令调用端能够通过不同的catch子句去捕获那些状况,从而采用不同的处理方式,所以可以基于这一点来判断要新建异常类,还是复用已有的类。

    一旦决定自己来创建异常类,就必须遵循相应的原则:

    • 继承Exception基类
    • 子类应该提供与Exception基类相同的构造函数重载,然后把相应的工作委托基类完成

    优先考虑做出强异常保证

    某个操作在抛出异常的时候,要负责把自身的状态管理好,这将直接关系到捕获异常的人有没有较大的余地来处理该异常。

    针对异常所做的保证分成三种:

    • 基本保证(basic guarantee),确保当异常离开了产生该异常的函数后,程序中的资源不会泄漏,而且所有的对象都处在有效状态。这相当于规定了抛出异常的那个方法在运行完其finally子句之后所必须达成的效果。
    • 强保证(strong guarantee),强保证是在基本保证的基础上做出的,它要求整个程序的状态不能因为某操作抛出异常而有所变化。
    • no-throw保证,执行该操作的那个方法绝对不会抛出异常。

    .NET CLR做出了一些基本的保证,例如会在发生异常时把内存管理好。除非你的资源实现了IDisposable接口,否则不太会在这种情况下出现资源泄漏问题。

    no-throw保证的例子有finalizer、Dispose方法、catch的when子句,此外编写委托目标方法时也应对遵守no-throw保证,在这些场合,绝对不应该令任何异常脱离其范围。

    在这三种态度中,强保证是较为折中的,它既允许程序抛出异常并从中恢复,又使得开发者能够较为简便地处理该异常。

    在强异常保证下,如果某操作抛出异常,那么应用程序的状态必须和执行该操作之前相同。这项操作要么完全成功,要么彻底失败。如果失败,那么程序的状态应与执行操作之前一模一样,而不会出现部分成功的情形。
    比如在修改集合数据时,为了实现强异常保证,可以考虑先对有待修改的数据做防御式的拷贝(defensive copy),然后在拷贝出来的数据上面执行操作。如果该操作顺利执行而没有抛出异常,那么就用这份数据把原数据替换掉,令程序的状态得以改变;在发生异常时,原数据还是完整的。
    从上面的例子也可知,要想做到强异常保证,往往会降低程序的性能,不过很多时候,从错误中恢复的能力,要比性能稍稍得到提升更为重要。

    考虑用异常筛选器来改写先捕获异常再重新抛出的逻辑

    在catch异常时,有时需要先判断程序状态、对象状态或异常中的属性,然后再加以处理。通常会想到在catch块进行判断,最后再把这个异常重新抛出。
    但更推荐异常筛选器来做,因为使用异常筛选器后,编译器所生成的代码会先评判异常筛选器的值,然后再考虑要不要执行栈展开(stackunwinding),因此,发生异常的原始位置能够保留下来,而且调用栈中的所有信息(包括局部变量的值)也可以保持不变。

    与之相对的,如果在catch块中使用throw e重新抛出,那么系统所报告的异常发生地点就是throw语句所在的位置,这会导致丢失异常的堆栈信息,直接throw虽然可以保留原始堆栈的信息,但这种在catch块中处理的写法,每次都会进入catch块、发生栈展开,这会产生较大的运行开销。

    合理利用异常筛选器的副作用

    一般来说,异常筛选器中的条件总是应该能在某些情况下得以满足,如果永远都无法满足,那么这个筛选器就失去了意义。然而有的时候,为了能监控程序中所发生的异常,还是可以考虑编写这种永远返回false的筛选器,此时调用栈还没有真正展开,但却可以获取到异常的信息。
    比如可以用于异常的记录:

    public static void Filter()
    {
      try
      {
        // ...
      }
      catch (Exception e) when (ForWhen(e)) { }
      catch (FormatException e)
      {
        // handle exception
      }
    }
    
    public static bool ForWhen(Exception e)
    {
      Console.WriteLine($"captured in when, msg:{e.Message}");
      return false;
    }
    

    catch (Exception e) when (ForWhen(e)) { }放到所有的catch之前,可以将所有的异常记录下来,但这里也需要注意:

    • 这行代码catch的应该是Exception基类,除非有特殊目的只catch某些异常
    • when条件始终返回false
    • 执行when条件判断的代码应做no-throw保证

    参考书籍

    《Effective C#:改善C#代码的50个有效方法(原书第3版)》 比尔·瓦格纳

  • 相关阅读:
    Java Learning (201108025)
    Java Learning (20110808)
    Negative numbers and binary representation
    “this” pointer
    NullPointerException
    Special Swiss Education
    Java Learning (20110802)
    More about Swiss keyboard
    About memory leak
    Application Verifier
  • 原文地址:https://www.cnblogs.com/zhixin9001/p/14364247.html
Copyright © 2020-2023  润新知