使用异常进行流量控制

时间:2012-02-16 07:02:31

标签: exception language-agnostic exception-handling

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

在我看来,一个方法应该在遇到情况时抛出异常,这种情况无法在内部处理,或者调用方可以更好地处理


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

  • 我应该何时抛出异常,何时应该使用强烈的非保证编写代码,这可能只返回 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来表示操作的成功或失败。

谢谢!

5 个答案:

答案 0 :(得分:3)

异常的问题在于它们本质上是美化的,有能力解开程序的调用堆栈。因此,如果您“使用流控制的异常”,您可能将它们用作getos而不是异常条件的指示。这正是异常点,以及它们名称的原因:它们应该仅用于例外情况。因此,除非设计一个方法不抛出异常(例如.NET的int.TryParse),否则可以抛出异常以响应异常情况。

与Java相反,C#的优点在于,在C#中,您可以通过返回元组类型或使用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,我强烈推荐。

答案 1 :(得分:2)

异常没有任何本质上的邪恶。正确使用后,它们可以极大地简化代码中的错误处理。异常的问题,特别是在Java中,是因为它们容易被滥用和过度使用,导致各种anti patterns

关于你的具体问题,我会就每一个问题提出自己的意见。


  

什么时候应该抛出异常,什么时候应该编写代码   强烈的nothrow保证,可能只是返回bool表示   成功还是失败?

你不能用Java编写一个带有'no throw'保证的方法。至少JVM可以随时抛出运行时错误,例如OutOfMemoryError。压制这些内容并不是你的责任,只要让他们泡到你的呼叫层,直到你到达最合适的位置来处理它们。将方法的返回类型更改为bool以指示成功或失败实际上是良好设计的对立面,您的方法返回类型应由其合同(他们应该做什么)决定,而不是他们如何做到。


  

我应该尝试最小化情况的数量,当方法时   抛出一个例外,或相反,如果它被最大化   在处理这些情况时提供灵活性?

既不!根据合同(即它应该做什么),你的方法应该按预期抛出 完全 异常数量。以下是一些一般规则:

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

  4.   

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

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


      

    我还被建议使用错误代码进行错误处理   非常有效,但从语法的角度来看很难看(同样,   使用它们时,开发人员失去了指定输出的能力   对于一种方法)。你怎么看待这个?

    我还没有在Java中看到过很多成功的错误代码框架,并且说实话,在大多数情况下它总是过度杀伤。可以对错误消息的内化和本地化进行更大的论证。

答案 2 :(得分:1)

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

答案 3 :(得分:1)

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

对于您的其他问题:

  

我应该尝试最小化情况的数量,当方法时   抛出一个例外,或相反,如果它被最大化   在处理这些情况时提供灵活性?

如果我正确地理解了你的问题,它就会在问题1中得到答案。你抛出异常的情况处于特殊情况,这当然不是很多情况。除非太阳和土星对齐,否则你应该确保你的程序几乎不会遇到异常。但是,您应该有测试用例来测试异常情况实际上会引发异常。

  

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

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

  

我还被建议使用错误代码进行错误处理   非常有效,但从语法的角度来看很难看(同样,   使用它们时,开发人员失去了指定输出的能力   对于一种方法)。你怎么看待这个?

错误代码效率很高,但可能会使您的代码难以阅读。异常是处理此问题的一种非常好的方法,但可能效率低下。如果你没有在你的课程中做出性能关键部分,我对此并不在意。

答案 4 :(得分:0)

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

要说明我的意思,这是一个例子

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如果他们成功了,那就是他们所做的,他们会尝试做一些事情,如果他们没有成功,他们会报告回来。 在这种情况下,您根本不关心出了什么问题以及为什么导致提供的数据以错误的格式解析的大部分时间。换句话说,你可以使用这种方法检查情况,把一切都掌控在你的控制之下。 但是如果你有一个方法,它采用字符串格式的整数列表并尝试对它们进行计算,那么如果你的方法无法解析其中的一些值,你应该抛出异常。你甚至应该报告哪一个没有正确的格式。