【问题标题】:Mixing errors and exceptions in C#在 C# 中混合错误和异常
【发布时间】:2012-02-14 10:39:00
【问题描述】:

我已经查看了一些异常问题,但找不到明确的答案,尤其是关于 C# 最佳实践的问题。

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

我有一个调用“被调用者”的函数“调用者”。 '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):
}

谢谢,马克

【问题讨论】:

  • 异常会自动在调用堆栈中冒泡。您不必重新扔掉它们。
  • @CodyGray 重新抛出的原因是添加信息,以便调用者可以区分它们。如果我从被调用者那里远程尝试/捕获,那么调用者将不知道多个操作中的哪个操作导致了异常。
  • 我不明白为什么被调用者不只是抛出一个包含有关它正在做什么的信息的异常。调用者无论如何都不应该知道被调用者在做什么!
  • 您能否举例说明调用者需要在哪里区分异常?调用者会根据异常做不同的事情吗?
  • 不控制执行流程,只是想将更多信息传递回 UI,而不是从抛出的异常中收集到。

标签: c# exception-handling error-handling


【解决方案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.
}

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

【讨论】:

    【解决方案2】:

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

    //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 正在执行各个步骤而不是调用一种无所不能的大方法。然后调用者将知道它在哪里失败并可以采取行动(可能会要求用户提供不同的登录凭据)

    【讨论】:

    • +1 如果您必须抛出异常,这是个好主意,而且我过去在文件 io 异常方面做过很多这样的事情。
    • 这实际上是我正在做的。我可能会让被调用者处理 NO 异常,让调用者尝试/捕获它们,并在需要时从日志中获取额外信息。
    • @MStodd:如果调用者知道问题发生在哪一步很重要,那么拆分被调用者可能是有意义的。
    • @ChrisWue 在这种情况下不是。此外,如果被调用者正在对列表中的对象进行操作,并且其中一个对象引发异常,那么如果被调用者想知道是哪个对象引发了异常,则不能将其分解。
    【解决方案3】:

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

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

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

    1. 在错误造成更多损害之前停止逻辑流,并且
    2. 提供有关错误发生时系统状态的信息,以便您查明错误的根源。

    考虑到这一点,您通常应该只在以下情况下捕获异常:

    1. 您计划用附加数据包装异常(如您的示例中所示)并抛出一个新异常,或者
    2. 知道继续你的逻辑流程不会产生进一步的不良影响:你认识到你试图做的事情失败了,不管它是如何失败的,你都心平气和那。在这些情况下,您应始终抓住机会记录错误。

    如果存在相当常见的失败案例(例如,用户提供了错误的登录凭据),则不应将其作为异常处理。相反,您应该构建您的 callee 方法签名,以便其返回的结果提供 caller 需要了解的有关问题所在的所有详细信息。

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

    【讨论】:

      【解决方案4】:

      如果您真正遇到调用者需要以不同方式处理异常的情况,那么典型的策略是在被调用者中“捕获并包装”底层异常。

      你可以

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

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

      【讨论】:

        【解决方案5】:

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

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

        【讨论】:

          【解决方案6】:

          没有单一的最佳实践可以涵盖 .Net 中的所有场景。任何说“总是使用异常”或“总是使用错误代码”的人都是错误的。用 .Net 编写的程序非常广泛和复杂,难以概括为这样的硬性规则。

          在很多情况下,例外都是不合适的。例如,如果我有一个非常紧凑的循环来查找 Dictionary<TKey, TValue> 中的值,我绝对更喜欢 TryGetValue 而不是投掷索引器。然而在其他情况下,如果提供给我的数据被约定已经在地图中,我宁愿扔掉。

          处理此决定的最佳方法是逐案处理。一般来说,如果情况确实非常特殊,我只会使用异常。本质上是调用者无法预测的,或者调用者明确违反合同的结果。例子

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

          【讨论】:

            【解决方案7】:

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

            【讨论】:

            • 与所有笼统的陈述一样,这也是误导性的(哦,具有讽刺意味)。如果这是真的,那么 .NET 中就不会有无数种不同的异常类型了。
            • 实际上并没有大量的异常类型。此外,这些类型并不表示“被调用者在做什么”;它们表示被调用者这样做的结果,这是完全不同的。
            • 我敢你点击here(并认为这些只是System.Exception 的直系子级——乐趣还在继续,例如here)。至于行动与结果的区别,恕我直言,这是一条红鲱鱼;除非有要报告的结果(不受欢迎的结果),否则没有人会抛出异常。
            • 这并不能帮助我解决我的问题,但这是一个有效的陈述,而且我没有考虑到这一点。有用。
            • 其实不,我相信调用者应该只关心自己的状态。它不应该知道,也不应该关心它调用的方法的状态。否则,方法会变得过于紧密耦合。你认为应该传达什么?我发现调用者很少需要区分它调用的方法引发的异常。
            猜你喜欢
            • 1970-01-01
            • 2011-08-08
            • 1970-01-01
            • 1970-01-01
            • 2017-12-13
            • 1970-01-01
            • 2015-03-07
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多