返回不良样式/危险后使用finally子句进行工作吗?


30

作为编写迭代器的一部分,我发现自己正在编写以下代码(剥离错误处理)

public T next() {
  try {
    return next;
  } finally {
    next = fetcher.fetchNext(next);
  }
}

发现它比阅读起来稍微容易一些

public T next() {
  T tmp = next;
  next = fetcher.fetchNext(next);
  return tmp;
}

我知道这是一个简单的示例,在可读性方面的差异可能不会那么大,但我对这样的一般情况感兴趣:在没有涉及异常的情况下,使用try-finally是否不好?如果在简化代码时实际上是首选。

如果不好,为什么?风格,性能,陷阱...?

结论 谢谢大家的回答!我猜得出的结论(至少对我而言)是,如果第一个示例是通用模式,则它可能更具可读性,但事实并非如此。因此,使用超出其目的的构造所带来的混乱,以及可能造成混淆的异常流,将胜过任何简化。


10
我个人将比第一个理解更多,但是我很少使用finally块。
Christian Mann

2
返回当前= fetcher.fetchNext(当前); //这样,您真的需要预取吗?
围巾岭

1
@scarfridge这个例子是关于实现的Iterator,您实际上确实需要某种预取才能hasNext()工作。自己尝试。
maaartinus 2012年

1
@maaartinus我知道这一点。有时hasNext()仅返回true,例如冰雹序列,有时获取下一个元素的代价过高。在后一种情况下,您应采取类似于延迟初始化的方式仅按需获取元素。
围巾岭

1
@scarfridge的常见用法Iterator是,您需要获取in的值hasNext()(因为获取值通常是找出值是否存在的唯一方法),然后next()像OP一样将其返回。
maaartinus 2012年

Answers:


18

就个人而言,我会考虑一下命令查询分离的思想。当您深入到它时,next()有两个目的:通告next元素的广告目的,以及改变next内部状态的隐藏副作用。因此,您正在做的是在方法的主体中执行广告的目的,然后在finally子句中解决隐藏的副作用,这似乎...确实很尴尬,尽管并非“错误”。

我要说的是,真正的原因在于代码的可理解性,在这种情况下,答案是“ meh”。您正在“尝试”一个简单的return语句,然后在应该用于故障安全错误恢复的代码块中执行隐藏的副作用。您正在做的是“聪明”,“聪明”的代码通常会引起维护者的抱怨,“什么……哦,我想我明白了。” 对于阅读您的代码的人来说,最好是喃喃自语“是的,嗯,很有意义,是的……”

那么,如果您将状态突变与访问者调用分开,该怎么办?我以为您所关注的可读性问题将变得毫无意义,但我不知道这将如何影响您更广泛的抽象。无论如何,都需要考虑。


1
+1为“聪明”注释。这非常准确地捕获了这个示例。

2
笨拙的Java迭代器接口将next()的“返回和前进”动作强加给OP。
凯文·克莱恩

37

从样式的角度来看,我认为这三行内容:

T current = next;
next = fetcher.getNext();
return current;

...比try / finally块更明显,更短。由于您不希望引发任何异常,因此使用try块只会使人们感到困惑。


2
try / catch / finally块也可能会增加额外的开销,即使您没有引发Exception,我也不确定javac或JIT编译器是否会去除它们。
Martijn Verburg '04年

2
@MartijnVerburg是的,即使try块为空,javac仍会向异常表添加一个条目并复制最终代码。
Daniel Lubarov 2012年

@Daniel-谢谢我太懒了,无法执行javap :-)
Martijn Verburg

@Martijn Verburg:AFAIK,try-catch-finallt块基本上是免费的,只要没有实际抛出异常即可。在这里,javac不会尝试并且不需要优化任何东西,因为JIT会照顾它。
maaartinus 2012年

@maaartinus-的确很有意义-需要再次阅读OpenJDK源代码:-)
Martijn Verburg 2012年

6

那将取决于finally块中代码的目的。规范的示例是在对流进行读/写之后关闭流,这种流必须始终执行。将对象(在这种情况下为迭代器)还原到有效状态恕我直言也算作清除,因此在这里我看不到任何问题。如果OTOH return一旦找到返回值就在使用,并在finally块中添加了许多不相关的代码,那么这将使目的模糊不清,并使它们变得更难以理解。

当“不涉及异常”时,我认为使用它没有任何问题。这是很常见的使用try...finally没有catch当代码只能扔掉RuntimeException,你不如何处理它们的计划。有时,finally只是一种保障,对于程序的逻辑,您知道永远不会抛出异常(经典的“永远不应该发生”的情况)。

陷阱:try块内引发的任何异常都会使finally博克运行。那会使您处于不一致状态。因此,如果您的return语句是这样的:

return preProcess(next);

并且此代码引发了异常,fetchNext仍然可以运行。OTOH,如果您将其编码为:

T ret = preProcess(next);
next = fetcher.fetchNext(next);
return ret;

那么它将无法运行。我知道您假设try代码永远不会引发任何异常,但是对于比这更复杂的情况,您如何确定?如果您的迭代器是一个长期存在的对象,那么即使当前线程发生不可恢复的错误,该迭代器也将继续存在,因此始终保持有效状态非常重要。否则,它并没有多大关系...

性能:对这样的代码进行反编译以了解其在幕后的工作方式会很有趣,但是我对JVM的了解还不足以进行很好的猜测...异常通常是“异常”,因此处理代码它们不需要针对速度进行优化(因此建议不要在程序的正常控制流程中使用异常),但是我不知道finally


那么,您不是真的提供了一个很好的理由避免使用finally这种方式吗?我的意思是,正如您所说,代码最终会带来不良后果;更糟糕的是,您甚至可能根本没有考虑过异常。
Andres F.

2
异常处理构造不会显着影响正常的控制流速度。仅在实际抛出和捕获异常时才支付性能损失,而在正常操作中进入try块或进入finally块时则不会支付性能损失。
yfeldblum 2012年

1
@AndresF。是的,但这对任何清理代码也有效。例如,假设null一个错误代码将对开放流的引用设置为;尝试从中读取将引发异常,尝试在该finally块中将其关闭将引发另一个异常。此外,在这种情况下,计算状态无关紧要,无论是否发生异常,您都希望关闭流。可能还有其他情况。因此,我将其视为有效使用的陷阱,而不是从不建议使用的建议。
mgibsonbr

还有一个问题是,当try和finally块都抛出异常时,任何人都不会看到try中的异常,但这可能是导致问题的原因。
汉斯·彼得·斯特尔

3

标题声明:“ ...返回后完成工作的最终子句...”为假。最终块发生在函数返回之前。这实际上是最后的全部要点。

您在此处滥用的是求值顺序,其中存储了next的值,以便在您对其进行突变之前返回。这不是常见的做法,我认为这是错误的,因为它会使您的代码不按顺序进行,因此很难遵循。

finally块的预期用途是用于异常处理,无论是否引发异常,都必须发生某些后果,例如清理某些资源,关闭数据库连接,关闭文件/套接字等。


+1,我要补充一点,即使语言规范保证在返回值之前存储该值,也可能与读者的期望背道而驰,因此在合理可能的情况下应避免使用。调试的难度是编码的两倍...
jmoreno,2013年

0

我会非常谨慎,因为(通常,不在您的示例中)try中的一部分会引发异常,如果抛出异常,则不会返回任何内容,并且如果您确实愿意执行返回后的最终操作,它将在您不希望执行时执行。

蠕虫的另一种可能是,通常,某些有用的操作最终会引发异常,因此您可以使用真正混乱的方法来结束。

因此,阅读本书的人必须考虑这些问题,因此很难理解。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.