在C#中混合错误和异常

时间:2012-01-19 18:56:23

标签: c# exception-handling error-handling

我看过了,但是对于我的一些异常问题找不到明确的答案,特别是关于C#最佳实践。

也许我总是对使用异常的最佳方式感到困惑,我碰到了这篇文章,基本上说'总是使用异常,从不使用错误代码或属性'http://www.eggheadcafe.com/articles/20060612.asp。我肯定会买,但这是我的困境:

我有一个调用'callee'的函数'caller'。 'callee'执行一些不同的操作,每个操作都可能抛出相同类型的异常。如何将有意义的信息传回“调用者”,了解“被调用者”在异常时正在做什么?

我可以抛出一个像下面这样的新例外,但是我担心我会搞乱堆栈跟踪,这很糟糕:

//try to log in and get SomeException  
catch(SomeException ex)
{
  throw new SomeException("Login failed", ex);
}

...

//try to send a file and get SomeException
catch(SomeException ex)
{
    throw new SomeException("Sending file failed", ex):
}

谢谢,Mark

7 个答案:

答案 0 :(得分:7)

没有单一的最佳做法可以涵盖.Net中的所有方案。任何说“总是使用例外”或“总是使用错误代码”的人都是错误的。用.Net编写的程序是广泛而复杂的,可以概括为类似的硬性规则。

在许多情况下,例外情况根本不合适。例如,如果我在Dictionary<TKey, TValue>中有一个非常紧密的循环查找值,我绝对会比投掷索引器更喜欢TryGetValue。然而在其他情况下,如果提供给我的数据已经签约已经在地图中,我宁愿抛出。

处理此决定的最佳方式是逐案处理。一般来说,如果情况真的特殊,我只使用例外。一个基本上是呼叫者无法预测的或来自呼叫者的结果明确违反合同。实例

  • 无法预测:找不到文件
  • 合同违规:将否定索引传递给索引器

答案 1 :(得分:5)

调用者不需要知道被调用者在异常时正在做什么。它不应该知道被调用者是如何实现的。

答案 2 :(得分:3)

在这种情况下,我通常会注入一个记录器并记录特定错误并重新抛出原始异常:

//try to log in and get SomeException  
catch(SomeException ex)
{
    logger.LogError("Login failed");
    throw;
}

...

//try to send a file and get SomeException
catch(SomeException ex)
{
    logger.LogError("Sending file failed");
    throw;
}

throw;将使原始堆栈跟踪保持活动状态(而不是将创建新堆栈跟踪的throw ex;)。

修改

根据您的实际代码执行操作,将callee实际分解为多个调用(Login()SendFile()等)是有意义的,因此caller正在执行各个步骤,而不是调用一个正在做所有事情的大方法。然后呼叫者将知道它失败的地方并可以对其采取行动(可能要求用户提供不同的登录凭证)

答案 3 :(得分:2)

  

'callee'执行一些不同的操作,每个操作都可能抛出相同类型的异常。

我认为这是真正的罪魁祸首:callee应根据已发生的故障类型抛出不同类型的异常,或者同一类型的异常应携带足够的额外信息以让调用者认真对待如何处理这个特殊的异常。当然,如果调用者根本不打算处理异常,那么它不应该首先捕获它。

答案 4 :(得分:2)

为什么caller方法关注callee正在做什么?如果在发送文件时出现问题,是否需要采取不同的措施?

try
{
    callee(..);
}
catch (SomeException e)
{
   if (e.Message == "Sending file failed")
   {
      // what would you do here?
   }
}

作为一般规则,例外应为例外。您的典型逻辑流程不应该要求它们被抛出和捕获。当它们被抛出时,通常应该是因为代码中的错误*。所以例外的主要目的是:

  1. 在错误造成更多伤害之前停止逻辑流程,
  2. 提供有关错误发生时系统状态的信息,以便您可以查明错误的来源。
  3. 考虑到这一点,通常只应在以下情况下捕获异常:

    1. 您计划用其他数据包装异常(如您的示例所示)并抛出新异常,或
    2. 知道继续你的逻辑流程不会产生进一步的不良影响:你认识到你试图做的事情失败了,不管它如何失败,你都很平静那。在这些情况下,您应该始终抓住机会记录错误。
    3. 如果存在相当常见的失败案例(例如,用户提供了错误的登录凭据),则不应将其作为例外处理。相反,您应该构建callee方法签名,以便其返回的结果提供caller需要了解的错误的所有详细信息。

      * 值得注意的例外是当发生真正无法预料的事情时,例如在交易过程中有人从计算机上拔下网线。

答案 5 :(得分:1)

Exception类提供了一个字典,用于向异常添加其他数据,可通过Exception.Data属性访问。

如果您只想将额外的信息位传递给调用者,那么就没有理由包装原始异常并抛出一个新异常。相反,你可以这样做:

//try to log in and get SomeException  
catch(SomeException ex)
{
  ex.Data.Add("action", "Login");
  throw;
}

...

//try to send a file and get SomeException
catch(SomeException ex)
{
    ex.Data.Add("action", "Sending of file");
    throw;
}

...

// somewhere further up the call stack
catch(SomeException ex)
{
    var action = ex.Data["action"] ?? "Unknown action";
    Console.WriteLine("{0} failed.", action); // ... or whatever.
}

这样您就可以保留原始堆栈跟踪,同时仍然可以向用户提供额外信息。

答案 6 :(得分:0)

如果真的有一个调用者需要以不同方式处理异常的场景,那么典型的策略是“捕获并包装”被调用者中的基础异常。

你可以

  • 将所有捕获的基础异常包装在单个自定义异常类型中,并使用状态代码指示抛出了哪个基础异常。这在使用提供者模型设计模式时很常见。例如,Membership.CreateUser可以抛出MembershipCreateUserException,其中包含状态代码以区分失败的常见原因。提供商实施者应遵循这种模式,以便消费者的异常处理独立于提供者实施。

  • 或将每个基础异常类型包装在单独的自定义异常中。如果您希望某些调用者可能希望处理其中一个异常(例如发送文件失败)而不是其他异常(例如登录失败),则这可能是合适的。通过使用不同的异常类型,调用者只需捕获感兴趣的异常类型。