这是滥用try/finally吗?

32

假设可以使用多个返回语句(我有点不同意,但让我们偏离一下话题),我正在寻找一种更可接受的方法来实现以下行为:

选项A:多个返回语句,重复代码块

public bool myMethod() {
    /* ... code ... */

    if(thisCondition) {
        /* ... code that must run at end of method ... */
        return false;
    }

    /* ... more code ... */

    if(thatCondition) {
        /* ... the SAME code that must run at end of method ... */
        return false;
    }

    /* ... even more code ... */

    /* ... the SAME CODE AGAIN that must run at end of method ... */
    return lastCondition;
}

看到同一个(小)代码块在每次方法返回时都被重复三次,让我感到很不舒服。此外,我想澄清上面的两个 return false 语句可以被描述为在方法中间返回... 它们绝对不是“保护语句”。

选项B是否稍微可接受一些?我感觉可能滥用了 try/finally,并且希望有完全不同的东西我应该做。

选项B: 多个返回,try/finally 块(没有 catch 块 / 异常)

public bool myMethod() {
    try {
        /* ... code ... */

        if(thisCondition) {
            return false;
        }

        /* ... more code ... */

        if(thatCondition) {
            return false;
        }

        /* ... even more code ... */

        return lastCondition;
    } finally {
        /* ... code that must run at end of method ... */
    }
}

最后,在我看来,选项C是最佳方案,但由于某种原因,我的团队不喜欢这种方法,因此我正在寻求妥协。

选项C:单个返回值,条件块

public bool myMethod() {
    /* ... code ... */

    if(!thisCondition) {
        /* ... more code ... */
    }

    if(!thisCondition && !thatCondition) {
        /* ... even more code ... */
    }

    /* ... code that must run at end of method ... */
    return summaryCondition;
}

如果您想讨论多个返回语句,请在此问题中进行。


1
如果你没有提供选项C,我本来会选择它的!你的队友对选项C有什么反对意见?如果“必须在方法结束时运行的代码”需要更改,他们的答案是什么? - Dónal Boyle
我同意Donal的意见。我喜欢选项C。他们有什么具体的反对意见? - Geo Ego
我不想代表他们讲话,但据我所知,他们不喜欢选项 C,仅仅是因为它没有多个返回语句。我不确定这是一个可维护的立场,但那是另一个话题。 - Dolph
如果是类似于if(!save(foo)) return false;的情况,我建议从save(..)中抛出异常,以迫使人们处理这种情况,而不是忽视它(或忘记它!)。 如果更接近于if (count() == 0) return false;,那么显然不适合使用异常,C语言会是我的选择(采用@Loadmaster的简化方法)。 - sfussenegger
1
值得一提的是,选项B中使用的try-finally与其他语言中经常赞扬的作用域绑定资源管理(SBRM,有时称为RAII)等效。 - Roger Pate
如果在抛出异常时需要执行相关代码,那么毫无疑问应该选择 b。如果不能重新排列代码,我会使用 c。选项 A 对我来说是不可接受的。 - user34537
11个回答

30
如果代码需要在其他代码抛出异常时仍然运行,则finally块是正确的解决方案。
如果它不需要在异常情况下运行(即仅对“正常”返回有必要),则使用finally将滥用此功能。
个人而言,我会以单一返回点风格重写该方法。并非因为我坚定地认同这个想法(我不是),而是因为它最适合这类方法末尾的代码。
当发现代码过于复杂时(这是非常真实的可能性),那么就该重构该方法,提取一个或多个方法。
最简单的重构如下:
public boolean  myMethod() {
    boolean result = myExtractedMethod();
    /* ... code that must run at end of method ... */
    return result;
}

protected boolean myExtractedMethod() {
    /* ... code ... */

    if(thisCondition) {
        return false;
    }

    /* ... more code ... */

    if(thatCondition) {
        return false;
    }

    /* ... even more code ... */
    return lastCondition;
}

那我应该做什么呢?这里没有任何异常需要捕获。 - Dolph

28

异常应该是例外情况,所以如果没有其他异常情况,我不喜欢选项B。(对于给出负评的人-我并不是说加上finally语句是不正确的,只是在没有异常情况的情况下我更倾向于不加-如果你有理由请评论)

如果代码总是需要,那么重构成两个函数怎么样?

public bool myMethod() {
    bool summaryCondition = myMethodWork();
    // do common code
    return summaryCondition;
}

private bool myMethodWork() {
   /* ... code ... */

    if(thisCondition) {
        return false;
    }

    /* ... more code ... */

    if(thatCondition) {
        return false;
    }

    /* ... even more code ... */

    return lastCondition;
}

19
如果正确回答了问题的后半部分,请点赞(+1)。但是,如果没有异常情况下的注释最终是错误的。try/finally存在的目的是执行某些代码块,然后无论该块如何退出始终执行其他操作——finally通常与异常处理相关联,但不限于此;也可以完全合法地使用finally进行退出处理。 - Lawrence Dol
接受这个答案,因为这段代码比Joachim Sauer的发表早了五分钟。这是我一直希望得到的显然的解决方案。 - Dolph
2
我同意@SoftwareMonkey的观点; finally块独立于任何catch块。实际上,在Java代码中有太多的try-catch实例,这些实例实际上应该是try-finally。换句话说,异常经常被捕获和错误处理,而需要做的是清理资源并让具有更多上下文的调用者处理异常。 - erickson
1
@Mark:抱歉,我可能误读了你没有想到的推论(但是嘿,我还是点了+1!) - Lawrence Dol
myMethodWork()太长了。应该重构以提取像这个答案https://dev59.com/zXE95IYBdhLWcg3wn_Rr#2230932中的较小方法。 - Erik Madsen
显示剩余3条评论

15
这是一个完美的地方放置一个 GOTO *躲避*

1
嘿,这笑话真的很值得笑 :P - Dolph
5
我认为并且希望David是认真的。 - Lawrence Dol
在C或C++中,使用goto是可以接受的,因为你试图从方法中尽早退出。 - David R Tribble
3
在C++中,通常会使用类似于“ScopeGuard”的东西来执行最后的代码块。 - Jerry Coffin

3

你的C选项解决方案不算离最优太远,因为它可以充分编码你试图完成的正确执行序列。

同样,使用嵌套if语句也可以达到相同的效果。它可能在视觉上不那么吸引人,但更容易理解,并且使执行流程非常明显:

public bool myMethod() { 
    boolean  rc = lastCondition; 

    /* ... code-1 ... */ 

    if (thisCondition) { 
        rc = false;
    } 
    else {  
        /* ... code-2 ... */ 

        if (thatCondition) { 
            rc = false;
        } 
        else {  
            /* ... code-3 ... */ 
            rc = ???;
        }  
    }

    /* ... the code that must run at end of method ... */ 
    return rc;  
}

简化代码如下:

public bool myMethod() { 
    boolean  rc = false; 

    /* ... code-1 ... */ 

    if (!thisCondition) { 
        /* ... code-2 ... */ 

        if (!thatCondition) { 
            /* ... code-3 ... */ 
            rc = lastCondition;
        }  
    }

    /* ... the code that must run at end of method ... */ 
    return rc;  
}

简化后的代码也揭示了您实际想要实现的目标:您使用测试条件来避免执行代码,因此当条件为false时应该执行该代码而不是在条件为true时执行某些操作。
至于try-finally块的问题:是的,您可以滥用它们。您的例子并不足够复杂,不需要使用try-finally。但如果更复杂,可能需要使用。
请参考我的看法:Go To Statement Considered Harmful: A Retrospective, "Exception Handling"

3
如果代码即使出现异常也需要运行,那么finally不仅是个好选择,而且必须使用。如果不是这种情况,finally就不是必要的。看起来你想找一个“看起来”最好的格式,但这里还有更多的问题需要考虑。

2

要不把它分解得更细一些,以提供更多的东西(请原谅我很长时间没有使用Java的逻辑运算符了),就像这样:

public bool findFirstCondition()
{
   // do some stuff giving the return value of the original "thisCondition".
}

public bool findSecondCondition()
{
   // do some stuff giving the return value of the original "thatCondition".
}

public bool findLastCondition()
{
   // do some stuff giving the return value of the original "lastCondition".
}

private void cleanUp() 
{
   // perform common cleanup tasks.
}


public bool myMethod() 
{ 


   bool returnval = true;
   returnval = returnval && findFirstCondition();
   returnval = returnval && findSecondCondition();

   returnval = returnval && findLastCondition();
   cleanUp();
   return returnval; 
}

@Dolph Mathews,这样看起来更整洁,但是每个方法都会被执行,无论返回值如何! - sfussenegger
1
@Dolph Mathews - 我最初也是这样想写的,但后来我想不起 Java 是否实际上有 &= 运算符,正如 sfussenegger 所提到的,它会失去短路评估的好处... - glenatron
感谢您帮忙解决技术术语的问题 ;) 我仍然觉得这有点像 Perl 的写法:$val = foo() unless !$val; - 至少我还记得这么多 - 谢天谢地,我已经忘了大部分了 :) - sfussenegger
没有所谓的“perlish”。Perl 包含几乎每种可能的做事方式。这就是为什么读别人写的 Perl 代码非常困难和令人烦恼的原因 :) - glenatron
这是一个微小的事情(在这个问题的上下文中),但我可能不会将提取的方法公开为public。它们仍然是实现细节,而不是合同的一部分。但肯定是迄今为止最干净的解决方案。 - Erik Madsen

2

除非您需要退出内部循环,否则不要滥用try/finally。请使用do/while。

bool result = false;
do {
  // Code
  if (condition1) break;
  // Code
  if (condition2) break;
  // . . .
  result = lastCondition
} while (false);

5
你不能认真吧!任何人都会立刻知道finally被用来做什么。而do { ... } while (false);的意图就不那么明显了。 - sfussenegger
但是任何人都不会立即知道try的用途 - 你会认为"前方有危险的异常抛出代码",而不是"一些常规的控制流结构",你可能希望真正的异常在执行finally之前跳过(即它们是真正未捕获的异常)。由于这两个结构都很奇怪,我的真正建议是重构成两个方法,但有时需要维护大量内部状态,如果你的编码团队拒绝使用if语句,像这样的选项可能会有所帮助。 - Rex Kerr
如果你不能立即理解finally块在做什么,那么代码本身就有缺陷:要么a)try块太长(从try块的开头滚动到结尾是不可取的-提取一些方法!),要么b)catch块太长(它不应该做更多的基本清理工作,这也应该很快理解)。由于我以前从未见过这种对do/while的滥用,所以我花了两秒钟来理解它的意图-这表明它不是一个很好的选择。 - sfussenegger
除非在整个代码库中广泛使用,否则这是一个糟糕的选择;那么至少那些程序员会知道发生了什么。鉴于示例中的所有代码块,我认为try块太长了。 - Rex Kerr

0

你不能简单地存储返回值并从if语句中退出吗?

   bool retVal = true;
   if (retVal && thisCondition) {
   }

   /* more code */

   if ( retVal ) {
     /* ... code that must run at end of method, maybe inside an if or maybe not... */

   }
   return retVal;

2
因为这是可怕的代码,随着存在的条件越来越多,它变得越来越糟糕。 - Lawrence Dol
有时候你必须打破规则。如果它解决了你的问题,那就去做吧。架构纯粹性和抽象概念有时会妨碍现实。 - chris
当然可以。但是使用try/finally代码比这个要好得多。 - Lawrence Dol
这就是我的意思:如果它能够解决你的问题并且能够正常工作,那么使用try/finally可能会被视为"滥用"也无妨。 - chris

0

除非在方法结束时必须运行的代码使用方法局部变量,否则您可以将其提取到一个类似于以下的方法中:

public boolean myMethod() {
    /* ... code ... */

    if(thisCondition) {
        return myMethodCleanup(false);
    }

    /* ... more code ... */

    if(thatCondition) {
        return myMethodCleanup(false);
    }

    /* ... even more code ... */

    return myMethodCleanup(lastCondition);
}

private boolean myMethodCleanup(boolean result) {

    /* ... the CODE that must run at end of method ... */
    return result;
}

这看起来仍然不太好,但它比使用类似于goto的结构要好。为了说服你的团队成员,一个1-return解决方案可能并不那么糟糕,你还可以展示一个使用2个do { ... } while (false);break的版本(*邪恶的笑容*)。


0

对我来说,使用try/finally控制流程就像使用GOTO一样。


合理使用goto语句可以使程序流程更清晰、更少出错,这有什么问题呢?毕竟,break、continue和return也是同样的作用,特别是带标签的break和continue。 - Lawrence Dol
我认为break是可以的,continue也可以,return也可以,但如果你在同一个方法中同时使用它们... 天啊! - Dolph
1
@Dolph:如果你还没有在同一个方法中使用过它们,那么你可能还没有写太长时间的代码(微笑)。再过10或20年回来找我吧。 - Lawrence Dol
我认为GOTO有时非常有用,但像生活中的大多数事物一样,只有适度使用才是好的。在正确的时间使用工具,你将会得到回报,但如果你总是使用它,你可能会发现自己处于一个糟糕的境地。 - Brian T Hannan
1
@软件猴子:如果我在Java中看到一个方法中同时出现这三个东西,那么必定会有人头落地。 - Dolph
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接