【问题标题】:Using exceptions for flow control使用异常进行流量控制
【发布时间】:2012-02-16 07:02:30
【问题描述】:

我最近被告知我正在滥用异常来控制我的应用程序中的流程,所以我试图以某种方式澄清这种情况。

在我看来,一个方法应该抛出一个异常,当它遇到一个无法在内部处理或者可能由调用方更好地处理的情况。


那么 - 是否存在任何特定的规则集,这些规则可用于在开发应用程序时回答以下一组问题:

  • 什么时候应该抛出异常,什么时候应该编写带有strong nothrow 保证的代码,这可能简单地返回bool 以指示成功或失败?

  • 当方法抛出异常时,我应该尽量减少情况的数量,或者相反,在处理这些情况时是否应该最大化以提供灵活性?

  • 我应该坚持我在开发应用程序时使用的框架/运行时设置的异常抛出约定,还是应该包装所有这些调用,以便它们匹配我自己的异常抛出策略?

  • 还建议我使用 错误代码 进行错误处理,这看起来非常有效,但从句法的角度来看很难看(此外,当使用它们时,开发人员失去了指定方法的输出)。您对此有何看法?


示例第三个问题(我使用的是I/O框架,遇到以下情况):

所描述的框架不使用异常来处理错误,但是 其他代码确实使用它们。我应该包装每一个可能的失败吗 用'???' 表示并在这种情况下抛出异常? 或者我应该将我的方法的签名更改为bool PrepareTheResultingOutputPath,并且只表明该操作是否是 成功与否?

public void PrepareTheResultingOutputFile(
    String templateFilePath, String outputFilePath)
{
    if (!File.Exists(templateFilePath))
        // ???

    if (!Directory.MakePath(outputFilePath))
        // ???

    if (File.Exists(outputFilePath))
        if (!File.Remove(outputFilePath))
            // ???

    if (!File.Copy(templateFilePath, outputFilePath)
        // ???
}

另一个示例 - 即使.NET Framework 也没有遵循一些严格的异常抛出策略。一些方法被记录为抛出 10 多种不同的异常类型,包括像 NullArgumentException 这样的普通异常类型,但其中一些只是简单地返回 bool 以指示操作成功或失败。

谢谢!

【问题讨论】:

标签: exception language-agnostic exception-handling


【解决方案1】:

异常的问题在于,它们本质上是美化的 goto,能够展开程序的调用堆栈。因此,如果您“使用异常进行流控制”,您可能会将它们用作 goto,而不是作为异常条件的指示。这正是例外的意义所在,也是它们命名的原因:它们应该只用于例外情况。因此,除非方法被设计为不引发异常(例如 .NET 的 int.TryParse),否则可以在异常情况下引发异常。

与 Java 相比,C# 的优点在于,在 C# 中,您可以通过返回元组类型或使用 out 参数来返回两个或多个值。因此,将错误代码作为方法的主要返回值返回并没有太多丑陋,因为您可以使用 out 其余参数。例如,调用int.TryParse 的常见范式是

string s = /* Read a string from somewhere */;
int n;
if (int.TryParse(s, out n))
{
    // Use n somehow
}
else
{
    // Tell the user that they entered a wrong number
}

现在是您的第三个问题,这似乎是最重要的。参考您的示例代码,您询问是否应该返回 bool 以指示成功/失败,或者是否应该使用异常来指示失败。不过,还有第三种选择。您可以定义一个枚举来说明该方法如何失败,并将该类型的值返回给调用者。然后,调用者有一个广泛的选择:调用者不必使用一堆 try/catch 语句,或者 if 几乎无法了解方法是如何失败的,但可以选择编写任何一个

if (PrepareTheResultingOutputFile(templateFilePath, outputFilePath) == Status.Success)
    // Do  something
else
    // It failed!

switch (PrepareTheResultingOutputFile(templateFilePath, outputFilePath))
{
    case Status.Success:
        // Do something
        break;
    case Status.FileNotPresent:
        // Do something else
        break;
    case Status.CannotMakePath:
        // Do something else
        break;
    // And so on
    default:
        // Some other reason for failure
        break;
}

您可以在herehere 中找到有关此问题的更多信息,尤其是Joel Spolsky's post,我强烈推荐。

【讨论】:

  • 非常感谢您的回答和参考。但是,我意识到,使用错误代码和Status enum 之间没有太大区别(因为这个 enum 纯粹作为“特定于操作”的错误代码)。更重要的是,如果您使用这种方法设计一个库,任何添加另一个操作状态的尝试都可能成为重大更改。基本上,开发人员最终可以得到代码,它只是忽略某些类型的错误,这是朝着未定义行为迈出的一步。
  • @Yippie-Kai-Yay 是的,Status 枚举和退出代码之间几乎没有区别。唯一的区别是枚举在用代码编写时更加安全和更具表现力。对于您的第二点:添加另一个操作状态确实是一个重大更改(尽管我在答案中编辑了 switch 语句以包含一个默认值,这会阻止更改导致中断),但添加另一个异常也是如此。如果您突然开始抛出另一个异常,您将再次中断未捕获该异常的调用者。面向未来的验证通常是一个难题。
  • “美化 goto”的说法似乎很弱。分支和循环结构也需要跳转。
  • @AndyThomas-Cramer 分支和循环结构以及确实 goto 和异常是否通过跳转实现是编译器和处理器的实现细节。 goto 被认为有害的全部原因是它混淆了代码中的控制流。基本的 for 循环清楚地表明“执行此代码 N 次”,而 if 则表明“基于此表达式的值,执行两个代码块之一”。异常说:“如果我们遇到异常情况并抛出异常,请转到调用者的catch。”所以异常比循环和分支更难阅读。
  • @Adam - 肆无忌惮的 goto 确实令人困惑。例外不是肆无忌惮的goto。它们只是一种新的结构化控制流。它们并不被普遍认为是令人困惑的。并且它们通常被认为是指示功能失败的更好解决方案,而不是错误代码。简而言之,“被认为有害的例外”被认为是有害的。
【解决方案2】:

异常本身并没有什么坏处。如果使用得当,它们可以极大地简化代码中的错误处理。异常的问题,特别是在 Java 中,是它们很容易被滥用和过度使用,导致各种anti patterns

至于您的具体问题,我将就每个问题发表自己的看法。


什么时候应该抛出异常,什么时候应该用 强 nothrow 保证,它可能简单地返回 bool 来表示 成功还是失败?

您不能在 Java 中编写具有“不抛出”保证的方法。 JVM 至少可以随时抛出运行时错误,例如 OutOfMemoryError。压制这些不是您的责任,只需让它们冒泡您的呼叫层次结构,直到您到达最合适的位置来处理它们。将方法的返回类型更改为 bool 以指示成功或失败实际上是良好设计的对立面,您的方法返回类型应该由它们的合同(它们应该做什么)来规定,而不是它们是如何做到的。


我应该尽量减少情况的数量,当方法 抛出异常,或者相反,它应该被最大化到 在处理这些情况时提供灵活性?

没有!您的方法应该完全抛出预期的异常数量,给定它的合同(即它应该做什么)。以下是一些一般规则:

  1. 处理发生的异常不是您的方法的责任 由于它没有采取行动。即 OutOfMemory 或 JVM 抛出 StackOverflow 错误
  2. 方法的责任来处理所有 异常,作为其执行的一部分抛出,由 延迟调用其他不明确可见的模块 方法的调用者。例如,如果您使用 Apache Commons 用于处理输入流的 IO 库,该方法旨在读取 文件,您需要处理库抛出的任何异常。这是 因为该方法的调用者无法知道您正在使用它 您的方法中的库。最典型的处理方式 这些类型的异常是通过在某些实例中重新包装它们 运行时(未经检查的)异常。您也可以将这些包装在选中的 异常,如果您想向方法调用者明确指示 它需要准备好应对特殊情况。
  3. 抛出异常方法的责任 (选中或未选中)如果由于任何原因无法履行 它的合同(又名,它不能成功完成)。举个例子, PrepareTheResultingOutputFile 中的每个条件 (if) 语句 方法是在所需失败时抛出异常的有效点 结果。

我是否应该坚持由 我在开发应用程序时使用或应该使用的框架/运行时 我包装了所有这些调用,以便它们匹配我自己的异常抛出 策略?

如果方法和方法调用者都使用同一个框架,那么在重新抛出它们之前完全没有必要包装框架异常。反之亦然 - 如果您在方法中使用调用者不知道的框架,那么您应该通过包装框架抛出的异常来隐藏该实现细节。


还建议我使用错误代码进行错误处理,这似乎 相当有效,但从句法的角度来看很难看(另外, 使用它们时,开发人员失去了指定输出的能力 一种方法)。您对此有何看法?

我还没有看到很多成功的 Java 错误代码框架,老实说,在大多数情况下,它完全是矫枉过正。可以为错误消息的内部化和本地化提出更大的论据。

【讨论】:

  • 我要补充一点,你的程序中没有任何地方应该处理OutOfMemoryErrorStackOverflowException 之类的异常,除非可能向用户显示错误对话框并关闭程序。即使这样也很冒险,因为如果您的内存不足,您应该如何创建一个新的错误对话框并将其显示给用户?
【解决方案3】:

对异常的错误使用异常是可以的。例如,如果在托管环境中内存分配失败,则抛出异常是可以的(在嵌入式环境中,最好以不同的方式处理)。同样,如果没有遵守合同,则抛出(例如,在接收到预期有效指针的空指针时抛出)可能是合理的(可能会中止)。对预期错误或控制流使用异常会起作用,但会破坏任何可接受性能的希望。

【讨论】:

  • 更不用说破坏任何对可读性、可接受或其他方面的希望了。
【解决方案4】:

我觉得第一个问题已经回答了,而且很简单。仅在特殊情况下使用异常。

关于您的其他问题:

我应该尽量减少情况的数量,当方法 抛出异常,或者相反,它应该被最大化到 在处理这些情况时提供灵活性?

如果我对您的问题的理解正确,它会通过问题 1 自行回答。您抛出异常的情况属于异常情况,这当然不是很多情况。您应该确保您的程序几乎不会遇到异常,除非太阳和土星对齐。但是,您应该有测试用例来测试异常情况实际上会引发异常。

我是否应该坚持由 我在开发应用程序时使用或应该使用的框架/运行时 我包装了所有这些调用,以便它们匹配我自己的异常抛出 策略?

视情况而定。在您的示例中,这取决于找不到文件是否异常?如果您希望该文件存在,而它不存在(例如与您的程序一起安装的文件),它应该抛出异常。如果它是用户希望打开的文件,您无法确定并且必须考虑到这一点。我不认为这种用户错误是异常的,并且可能会为此使用返回码。其他要考虑的事情是:这对程序的执行是否关键?使用该参数调用此函数是否违反合同?你的问题的答案不是直截了当的,你必须根据具体情况来做。再说一遍:这是一种特殊情况吗?

还建议我使用错误代码进行错误处理,这似乎 相当有效,但从句法的角度来看很难看(另外, 使用它们时,开发人员失去了指定输出的能力 一种方法)。您对此有何看法?

错误代码很有效,但可能会使您的代码难以阅读。异常是处理此问题的一种非常好的方法,但可能效率低下。但是,如果您不将程序的性能作为关键部分,我不会太在意。

【讨论】:

    【解决方案5】:

    正如您所说,如果存在无法处理的内部情况,则应抛出异常,但如果存在外部问题(对另一个模块的调用失败),则应让模块本身处理这种情况,除非抛出异常模块对上下文没有意义,那么你应该用一个有意义的异常包装异常。这样你不应该为可能发生的每个可能的错误抛出异常。有时来自数据访问的简单“连接超时”异常layer 就足够了,没有必要用另一个 excption 包装它。

    为了说明我的意思,这里举个例子

    var pop3Clinet=new pop3Client();
    
        try
        {
        pop3Client.SetServer("server-address");
        pop3Client.SetUserName("username");
        pop3Client.SetPassword("password");
        var mails=pop3Client.ReceiveMails();
        }
        catch(NullReferenceException exp)
        {
         throw new Exception("can not connect to server.server-address is wrong or the server is down",exp);
        }
        catch(UnauthorizedAccess)  //the exception is meaningful and can be rethrown or this block can be removed.
        {
        throw;
        }
    

    这个例子是真实的 :) 我们使用的这个 pop3client 奇怪地在每次尝试连接到服务器时都无法抛出与服务器相关的异常,因此我们必须将 NullReference 异常包装成一个有意义的异常。

    不推荐返回 bool 来表明一个方法已经成功,除非这是该方法被编写的原因。例如在 .net 中,我们有一些原始类型的 TryParse 方法,例如 int , long 等,它们返回一个bool 如果他们成功并且他们就是这样做的,他们会尝试做某事,如果他们不成功,他们会报告回来。 在这种情况下,您根本不关心出了什么问题以及为什么在大多数情况下导致所提供数据的解析格式错误。换句话说,您可以使用这种方法来检查情况,以控制一切。 但是,如果您有一个方法以字符串格式获取整数列表并尝试对它们进行计算,那么如果您的方法无法解析其中一些值,您应该抛出异常。您甚至应该报告哪个没有正确的格式。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-03-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多