抛出异常来控制流程 - 代码味道?

时间:2009-01-21 16:26:55

标签: java exception-handling

考虑这段代码(特别是Java):

public int doSomething()
{
    doA();

    try {
        doB();
    } catch (MyException e) {
        return ERROR;
    }

    doC();
    return SUCCESS;
}

doB()定义为:

private void doB() throws MyException

基本上,MyException仅在doB()满足某些条件(这不是灾难性的,但确实需要以某种方式提高此条件)的情况下存在,以便doSomething()知道退出时错误。

您是否发现使用异常,在这种情况下控制流量,是否可以接受?或者这是代码味道?如果是这样,你会如何重构这个?

16 个答案:

答案 0 :(得分:16)

当doB()失败时执行doC()真的很重要吗?如果没有,为什么不简单地让Exception在堆栈中向上传播到可以有效处理的位置。就个人而言,我考虑使用错误代码代码味道。

编辑:在评论中,您已经准确描述了应该简单声明的情节

public void doSomething() throws MyException

答案 1 :(得分:11)

这完全取决于错误条件是什么,以及方法的工作是什么。如果返回ERROR是一种处理调用函数错误的有效方法,为什么它会变坏?

然而,通常是一种气味。考虑一下:

bool isDouble(string someString) {
    try {
        double d = Convert.ParseInt32(someString);
    } catch(FormatException e) {
        return false;
    }
    return true;
}

这是一个非常大的代码味道,因为你不期望双重值。您只想知道字符串是否包含double。

有时候,你使用的框架没有其他方法可以做你想做的事情。对于上述情况,有一种更好的方法:

bool isDouble(string someString) {
    bool success;
    Convert.TryParseInt32(someString, ref success);
    return success;
}

这种例外有一个特殊的名字,由我最近阅读的博客创造。但遗憾的是,我忘记了它的名字。如果你知道的话请评论。最后但并非最不重要的是,上面是伪代码。我不是c#开发人员,所以上面的代码不能编译,我敢肯定,但TryParseInt32 / ParseInt32证明了这一点,我认为我会选择C#。

现在,到你的代码。我们来检查两个功能。一个闻起来,另一个没有:

1。气味

public int setupSystem() {
    doA();

    try { doB(); }
    catch (MyException e)
    { return ERROR; } 

    doC();
    return SUCCESS;
}

这是代码气味,因为当您想要设置系统时,您不希望它失败。未能设置系统意味着您无法在不处理该错误的情况下继续操作。

2。确定

public int pingWorkstation() {
    doA();

    try { doB(); }
    catch (MyException e)
    { return ERROR; } 

    doC();
    return SUCCESS;
}

没关系,因为该方法的目的是测试工作站是否仍然可以访问。如果不是,那么这是该方法结果的一部分,而不是需要替代返回路径的特殊情况。

答案 2 :(得分:9)

我对OP代码的唯一问题是你混合范式 - doB通过抛出异常显示错误,而doSomething通过返回代码显示错误。理想情况下,你会选择其中一个。当然,在传统维护中,您可能没有那么奢侈。

如果您选择返回错误代码,那没关系,但我不喜欢它,因为它鼓励您使用辅助通道(如全局变量)在失败时通信状态,而不是将该状态捆绑到异常中并让它冒泡堆叠,直到你可以做些什么为止。

答案 3 :(得分:6)

我从来不喜欢在控制流中使用异常(在某些语言中,比如CLR,它们很贵)。

如果您可以修改 doB(),最好的办法是更改它以返回表示成功或失败的布尔值,因此您的示例如下所示:

public int doSomething()
{
    doA();

    if (!doB()) {
        return ERROR;
    }

    doC();
    return SUCCESS;
}

答案 4 :(得分:5)

在以下情况下应使用例外:

  • 功能无法正常完成,
  • 当没有可用于指示失败的返回值时,或
  • 当抛出的异常传达的信息多于return FAILURE;时,例如,嵌套异常等。

首先要记住,异常(如返回值或方法参数)只是在代码的不同部分之间发送的消息。努力优化通过这些方法传达的信息与API的简单性之间的平衡。当只需要一个简单的SUCCESS / FAILURE标志(并且该方法不需要返回其他值)时,请使用它。如果方法已经必须返回一个值,那么通常需要使用一个异常(这是一种查看它的方法,只是方法中的“异常”返回值)。如果必须传递的故障信息太丰富而无法在返回值中传递(例如,I / O故障的原因),请使用异常。

最后,错误处理是一项设计决策,并没有一套适合所有情况的规则。

答案 5 :(得分:4)

使用控制流的例外情况绝对是不好的。只有在特殊条件下才会抛出异常。

但是,使用实用方法只是抛出异常或什么也不做也不是特别糟糕。例如,您可能希望验证关于方法的条目的所有参数,或者检查内部状态在复杂计算之间是否仍然有效(由于某种原因,您无法在内部强制执行,也许您正在接受闭包并执行它们)。在这些情况下,将一致性检查提取到自己的方法中并不是一个坏主意,以提高“实际”方法实现的可读性。

分界线实际上是在遇到正常操作参数之外的事情时应该抛出异常,而事情是如此糟糕以至于该方法没有真正的方法来继续它的工作。

作为的例子,这将使用流控制的例外:

public int calculateArraySize(Object[] array)
{
   int i = 0;
   try
   {
      array[i++];
   }
   catch (ArrayIndexOutOfBoundsException ignore) {}
   return i;
}

我相信它会返回正确的结果,但是对于那些习惯于正常使用异常的人来说,它会非常缓慢和低效,并且难以阅读。 ; - )

另一方面,在我看来,这样的事情就可以了:

public void myMethod(int count, Foobar foo) throws MyPackageException
{
   validateArgs(count, foo);

   // Stuff
}

private void validateArgs(int count, Foobar foo) throws MyPackageException
{
   if (m_isClosed)
   {
      throw new IllegalStateException("Cannot call methods on a closed widget");
   }

   if (count < 0)
   {
      throw new IllegalArgumentException("count must be greater than zero");
   }

   foo.normalise(); // throws MyPackageException if foo is not fully initialised at this point
}

尽管所有第二种方法都可能引发异常,但它并没有这样做来控制程序流程,而是为了响应异常情况而提高它们。

答案 6 :(得分:2)

如果出现错误,可以抛出异常,就像在doB()中一样。 但问题是函数doSomething()。

您不应该使用return语句指示成功或失败。你应该这样做:

try { 
    doB();
} catch (MyException e) {
    throw MyException2(e);
}

答案 7 :(得分:2)

异常不应仅用于控制流量。应使用经过检查的异常来告知调用代码未达到API合同的某些条件。我不会设计doSomething来处理doB的调用经常使用try/catch块失败的情况。如果您需要处理频繁失败的情况,我会设计doB以返回成功或失败boolean,以指示其调用方法是否继续使用自己的doC - 类型方法:

public int doSomething() {
    doA();

    if ( doB() )
       doC();
       return SUCCESS;
    } else { 
       return ERROR; 
    }
}    

答案 8 :(得分:1)

根据doB的逻辑,你可以指出一些返回值是否正常,然后doSomething可以使用返回的值以适当的方式处理这种情况。

答案 9 :(得分:1)

例外情况是非标准行为。所以可以用它们来控制流量。

正如Sun所说:

  

一般来说,不要抛出RuntimeException或创建RuntimeException的子类,因为您不希望因指定方法可以抛出的异常而烦恼。

     

以下是底线指南:如果可以合理地期望客户端从异常中恢复,请将其作为已检查的异常。如果客户端无法执行任何操作以从异常中恢复,请将其设置为未经检查的异常。

请记住不要将RuntimeException子类化,而将Exception子类化为更快。

答案 10 :(得分:0)

我认为使用异常非常简单,简单明了。我会创建返回正常值的常规函数​​,并为它们的设计保留异常。

答案 11 :(得分:0)

正如其他人所指出的,它实际上取决于此处涉及的方法的预期功能。如果doSomething()的作用是检查某些东西,返回一个允许正常执行其调用方法的代码继续(虽然根据返回的代码在不同的路径上)然后让它返回该int可能没问题。但是,在这种情况下,doB()可能应该是一致的,并返回一个布尔值或doSomething()可以检查的内容,而不是抛出一个不会发生任何其他事情的异常。

如果doB()是另一个类的公共方法,可能在doSomething()之外有其他用途(而不是您指定的同一个类中的私有方法),它会抛出异常并且你想在doSomething()(它具有上面指出的相同角色)中使用它,那么你的例子就可以了。

doB()抛出异常时返回的代码被称为ERROR这一事实,表明它可能是一个失败,会阻止任何操作doSomething()成为其中的一部分完成。在这种情况下,您可能不应该捕获doB()引发的异常,只是让它被抛出堆栈,直到它到达可以处理它的位置,或者直到它到达可以报告/记录的一般错误处理程序它或其他什么。

答案 12 :(得分:0)

在C#中,抛出异常是相对昂贵的,性能方面的 - 因此避免将其用于流量控制的另一个原因。这是Java的情况吗?是的,有一个论点是不提前过度分析性能。但是,如果你知道某些东西会花费额外的时间,而且很容易避免,你不应该这样做吗?除非你真的不在乎!

答案 13 :(得分:0)

请参阅此问题的最佳答案:

How slow are Java exceptions?

事实证明它们比正常的代码流慢至少50倍。所以我要说在通常的代码运行中使用异常绝对是个坏主意。

答案 14 :(得分:-1)

以下是我将如何处理它:

public void doSomething() 
        throws MyException{
    doA();

    try { 
        doB();
    } finally {
        doC();
    }
}

如果在doB()中发生错误,则异常将在堆栈中向上传播。

答案 15 :(得分:-2)

你在这里杀了我。您永远不想使用异常来控制流量。与在任何地方使用goto语句没有什么不同!

异常是例外,意味着发生了一些未预料到的事情。这么多,所以我们停止执行应用程序。任何曾经在应用程序上工作过的人都是这种类型的异常流程控制,它会想要追捕那些做了这个并扼杀他(或她)的开发人员。

使用异常作为流控制使维护和新功能开发成为一场噩梦!