例外处理:合同与例外方法

例外处理:合同与例外方法

Exception handling: Contract vs Exceptional approach

我知道两种处理异常的方法,让我们来看看它们。

  • 合同方式。

    当某个方法不执行其在方法标头中将要执行的操作时,它将引发异常。因此,该方法"承诺"它将执行该操作,并且如果由于某种原因而失败,则将引发异常。

  • 出色的方法。

    仅当发生真正奇怪的事情时才抛出异常。当您可以使用常规控制流(If语句)解决问题时,不应使用异常。您不会像在合同方法中那样将异常用于控制流。

  • 让我们在不同情况下使用两种方法:

    我们有一个Customer类,该类具有一种称为OrderProduct的方法。

    合同方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Customer
    {
         public void OrderProduct(Product product)
         {
               if((m_credit - product.Price) < 0)
                      throw new NoCreditException("Not enough credit!");
               // do stuff
         }
    }

    特殊方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Customer
    {
         public bool OrderProduct(Product product)
         {
              if((m_credit - product.Price) < 0)
                       return false;
              // do stuff
              return true;
         }
    }

    if !(customer.OrderProduct(product))
                Console.WriteLine("Not enough credit!");
    else
       // go on with your life

    在这里,我更喜欢采用特殊的方法,因为如果客户没有赢得彩票,他就没有钱并不是真正的例外。

    但是,我在合同风格上存在这种情况。

    极好:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class CarController
    {
         // returns null if car creation failed.
         public Car CreateCar(string model)
         {
             // something went wrong, wrong model
             return null;
         }
     }

    当我调用一个名为CreateCar的方法时,我该期望一个Car实例而不是一些糟糕的null指针,后者可能会在十行之后破坏我正在运行的代码。因此我更喜欢合同:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class CarController
    {
         
         public Car CreateCar(string model)
         {
             // something went wrong, wrong model
             throw new CarModelNotKnownException("Model unkown");

             return new Car();
         }
     }

    您使用哪种风格?您认为对异常的最佳通用方法是什么?


    我赞成您所说的"合同"方法。在支持异常的语言中,无需返回null或其他特殊值以指示错误。当代码中没有很多" if(result == NULL)"或" if(result == -1)"子句以及非常简单,直接的逻辑时,我发现代码更容易理解。


    我通常的方法是使用合同来处理由于"客户端"调用(即由于外部错误(即ArgumentNullException))引起的任何类型的错误。

    参数上的每个错误都不会处理。引发异常,由"客户端"负责处理。另一方面,对于内部错误,请始终尝试更正它们(就像由于某种原因而无法获得数据库连接一样),并且只有在您无法处理它时才引发异常。

    重要的是要记住,客户端始终无法处理此类级别的大多数未处理的异常,因此它们很可能会由最通用的异常处理程序处理,因此,如果发生此类异常,则无论如何您都可能是FUBAR。


    两种方法都是正确的。这就是说,应该以这样一种方式来写合同:对于并非真正例外的所有情况,都指定不需要抛出异常的行为。

    请注意,根据代码调用者的期望,某些情况可能会或可能不会例外。如果呼叫者期望字典中将包含某个项目,而没有该项目则表示存在严重问题,则找不到该项目是一种特殊情况,并且应引发异常。但是,如果调用者实际上不知道某个项目是否存在,并且同样准备处理该项目的存在或不存在,则该项目的缺失将是预期的情况,并且不会引起异常。处理呼叫者期望中的此类变化的最佳方法是让合同指定两种方法:DoSomething方法和TryDoSomething方法,例如

    1
    2
    TValue GetValue(TKey Key);
    bool TryGetValue(TKey Key, ref TValue value);

    请注意,尽管标准的"尝试"模式如上所述,但是如果有人正在设计一个生成项目的接口,则某些替代方法也可能会有所帮助:

    1
    2
    3
    4
    5
     // In case of failure, set ok false and return default<TValue>.
    TValue TryGetResult(ref bool ok, TParam param);
    // In case of failure, indicate particular problem in GetKeyErrorInfo
    // and return default<TValue>.
    TValue TryGetResult(ref GetKeyErrorInfo errorInfo, ref TParam param);

    注意,在接口中使用类似普通的TryGetResult模式的东西会使接口相对于结果类型不变。使用上述模式之一将允许接口相对于结果类型协变。同样,它将允许在'var'声明中使用结果:

    1
    2
      var myThingResult = myThing.TryGetSomeValue(ref ok, whatever);
      if (ok) { do_whatever }

    并不是完全标准的方法,但是在某些情况下,其优势可能是合理的。


    如果您实际上对异常感兴趣,并想考虑如何使用它们来构建健壮的系统,请考虑阅读存在软件错误时制作可靠的分布式系统。


    我相信,如果您要构建一个将由外部程序使用(或将由其他程序重用)的类,则应使用合同方法。一个很好的例子是任何一种API。


    推荐阅读