【问题标题】:exceptions + signaling end-of-iterator: why is it bad in Java and normal in Python?异常+信号迭代器结束:为什么它在Java中不好而在Python中正常?
【发布时间】:2011-12-09 14:53:39
【问题描述】:

我真的很困惑:Java 中的标准方法是仅在“异常”条件下抛出异常,而不是使用它们来表示迭代器结束。

示例:Effective Java,第 57 条(“仅在异常情况下使用异常”)和JavaSpecialists newsletter 162

流量控制

我们绝不应该引发本来可以预防的异常。我已经看到代码不是检查边界,而是假设数据是正确的,然后捕获 RuntimeExceptions:

这是一个错误代码的例子(请不要这样编码):

public class Antipattern1 {
   public static void main(String[] args) {
     try {
       int i = 0;
       while (true) {
         System.out.println(args[i++]);
       }
     } catch (ArrayIndexOutOfBoundsException e) {
       // we are done
    }
  }
}

在 Python 中使用这个成语是标准的,例如StopIteration:

异常停止迭代

由迭代器的 next() 方法引发,表示没有其他值。这是从 Exception 而不是 StandardError 派生的,因为这在其正常应用程序中不被视为错误。

为什么它对 Java 不利而对 Python 有利?

【问题讨论】:

  • 这两个规则都只是意见。背后也没有客观的技术原因。在一个部落内,意见往往会合并为一种社会规范,但在不同的部落中,可能会出现不同的规范。这只是一个例子。
  • @Jason S:不能代表 Python 方面,但我认为 "iteration over an array" 可能是一个不好的例子,用于您的问题(很好问题顺便说一句)。它不好的原因是在 try/catch 块中包装东西会阻止现代虚拟机进行很酷的优化。 Joshua Block 在Effective Java 中也指出“创建、抛出和捕获异常通常很昂贵”。所以从性能上看,它看起来真的很糟糕(布洛赫称它总体上是“可怕的”)。仅仅因为这个原因,它就使得 “使用异常的数组迭代” 对于 Java 来说似乎是个坏主意。
  • @user988052:在 Python 中,设置 try/except 块非常快,提升+捕获也非常快。两者都是一次性操作;当然,它比所有那些 if 语句都要快,至少对于几个元素而言。
  • @Petr Viktorin:非常感谢。因此,如果它在 Python 下非常快,而在 Java 中的性能可能很糟糕(通过阻止 JVM 优化启动,除了可能让 try/throw/catch 本身有点慢),那这似乎是一个很好的理由,为什么在这个 “数组迭代” 中,它在 Python 中很好但在 Java 中不好。
  • @user988052:真正的原因是可读性,防止TOCTTOU 错误和竞争条件,以及 EAFP 与鸭子打字配合得很好。 Python 更关心这些而不是性能。但这是一个不错的奖励:)

标签: java python exception


【解决方案1】:

Python 和 Java 处理异常的方法大不相同。在 Python 中,异常是正常的。在 Python 词汇表中查找 EAFP(请求宽恕比请求许可更容易)。还要检查Wikipedia 的内容。

StopIteration 只是 EAFP 的一个示例 - 继续从迭代器获取下一件事,如果失败,则处理错误。

如果代码通过非本地出口更具可读性,则在 Python 中使用异常。你不写支票,如果事情不成功,你只是处理失败。这绝对没有什么可耻的,事实上它是被鼓励的。与 Java 不同。


现在来看一个特定的 StopIteration 案例:考虑 generator functions

def generator():
    yield 1
    print('Side effect')
    yield 2

为了支持某种has_next() 方法,生成器必须检查下一个值,在请求2 之前触发print。必须在迭代器中记住该值(或引发的异常)。如果has_next 被调用了两次,那么只有第一个会触发副作用。或者下一个值总是可以预先计算,即使它不需要。

我发现 Python 的语义——只在需要下一个值时才计算——是最好的选择。

当然,Java 没有可恢复的生成器,所以在这里很难比较。但有一些轶事证据表明 StopIteration 比 hasNext() 泛化更好。

【讨论】:

  • has_next() 方法的另一种可能更简洁的方法:一旦到达函数的末尾,生成器就会在内部设置一个标志。这将遵循仅在请求时计算值的语义。
  • @Rocoty 不完全-如果函数结束和函数中的最后一个 yield 之间有语句,例如(可能)资源清理或某种日志记录,则标志不会是按照您的预期设置。这是否是好的做法……并不是重点;我不认为任何东西都可以允许标志方法并允许在最后一次 yield 之后的代码,而不改变生成器从“on yield”到“在实际 yield 之后的下一个 yield 之前”暂停的点。
  • @DavidHeyman Kotlin 1.1 实现了与我所说的协程类似的东西。它在调用 hasNext() 时计算值,并在调用 next() 时返回计算值(如果尚未计算值,则 next() 将首先计算它)。 hasNext() (几乎)总是与 next() 一起调用。不过,现在输入这个,我意识到这几乎正是 Python 的做法,但有一个警卫而不是例外。 (请求许可而不是宽恕)
【解决方案2】:

没有什么可以阻止你在java中使用类似的异常, 它只是看起来很难看,至少对于 Java 开发人员来说是这样。

主要原因是异常的堆栈跟踪很昂贵, 并且可能Java开发人员可能会更加关注 比 Python 开发者花费计算资源。

Java 也是一种相当“干净”的语言——有些人会说是原教旨主义的, 这是它是一门好语言的原因之一。 (*见评论)

无论如何。原教旨主义者(和一些普通人)认为对正常流程使用异常并不是正确的方法...... :-)

但除此之外,最近的 jvm 检测到您正在生成大量堆栈跟踪 对于相同的代码点,实际上会在“一段时间后”在没有它们的情况下抛出异常以加快处理速度。

【讨论】:

  • *) “清洁度”也是大量编写样板代码的原因,同时还要与由架构师、代码原教旨主义者和 unix- 组成的邪恶团体编写的地狱框架和 xml 配置作斗争似乎喜欢java的黑客。我在看你,马文!今天,我想最糟糕的复杂因素正在关注更新更酷的东西,而我们其他人正在花时间清理他们留下的烂摊子,比如早期的 EJB 等等......
【解决方案3】:

Java 有时也会 does it:“DataInput 方法的所有实现都使用 EOFException 而不是返回值。”在这种情况下,无法使用通常的标记值-1

【讨论】:

  • 我猜 Python 中也会出现类似的用例。
  • 并非如此——在 Python 中,您不仅限于单一的返回类型。您可以愉快地从通常返回整数的函数中返回 None 。 (如果你想要惯用的 Python 代码,你应该引发异常,但这不是技术限制。)
【解决方案4】:

不推荐它的原因是因为在 java 中,异常处理的处理和恢复成本通常很高。当抛出异常时,它会导致 jvm 回溯正在执行的操作,以提供堆栈跟踪,这对性能来说绝不是一件好事。简而言之,这是语言滥用——通常会有一种更清洁、更有效的逻辑处理方式。考虑以下代码:

try {

    int x = service.getValue();

    if(x > 5)
        throw new NumberTooBigException("Too Big");
    else
        throw new NumberTooSmallException("Too Small");

} catch (NumberTooBigException e) {

    System.out.println("i think it's too big...");

} catch (NumberTooSmallException e) {

    System.out.println("i think it's too small...");

}

更好的方法是只使用 java 的预期控制逻辑:

if(x > 5)
    System.out.println("i think it's too big...");
else
    System.out.println("i think it's too small...");

从比较这两个 sn-ps 可以看出,异常处理有点荒谬——对于示例打算做的事情来说太过分了。您发布的示例的更好方法是这样的:

String[] args = {"one", "two", "three"};
for(String arg : args)
    System.out.println(args);
}

异常更适合用于真正出错的情况,例如 IOException("no space left on device")、ClassNotFoundException("can't find your code to run") 或 NoRouteToHostException("Can't connect to host" )。

【讨论】:

    【解决方案5】:

    StopIteration 存在于 Python 中,用于对任何序列进行简单迭代。

    使用异常来实现这一点是一种设计选择,它确实与 Java 的异常心态并没有那么矛盾。当没有更多元素时调用迭代器的 next() 方法是一种异常情况,在 for 循环中在幕后捕获该异常是实现“获取项目直到没有任何剩余”的一种非常简单的方法.

    【讨论】:

    • 这是一个非常糟糕的比较。正确的应该是for (Item i : itr) System.out.println(i)。在这两种情况下,编译器都很好地隐藏了实现细节——通常,无论有无异常,您都可以很好地实现相同的代码。一切都取决于您如何定义“异常”。而 python - java/c# 只是有不同的范例;在这种情况下,“错误”和“正确”没有多大意义。
    • @Voo - 好点,我有一段时间没有用 Java 编码了。我删除了示例并留下了其余部分,因为它仍然适用。
    【解决方案6】:

    对此没有正确或错误的答案。异常处理是一种中性的控制流结构,它的最佳使用取决于上下文和风格。

    在这种情况下,Java 和 Python 都出于相同的原因做同样的事情:java.util.Iteratornext() 使用 NoSuchElementException 来表示迭代器结束。这在两种语言中都是一种很好的风格:由于各种原因,使用特殊的哨兵返回值的替代方案要糟糕得多。

    根据经验,每当您编写一个想要通知一种以上控制流返回的函数时,都应该考虑使用异常。这包括异常错误情况,但对异常信号的良好使用当然不限于此。

    【讨论】:

      【解决方案7】:

      Java 中的异常捕获执行堆栈,非常慢:How slow are Java exceptions?

      如果您在控制流中使用异常,则将null 传递为Throwable。否则,将通过 super 调用的层次结构调用以下标准 Java 库代码:

      public Throwable() {
          fillInStackTrace();
      }
      
      public synchronized Throwable fillInStackTrace() {
          if (stackTrace != null ||
              backtrace != null /* Out of protocol state */ ) {
              fillInStackTrace(0);
              stackTrace = UNASSIGNED_STACK;
          }
          return this;
      }
      
      private native Throwable fillInStackTrace(int dummy);
      

      【讨论】:

        猜你喜欢
        • 2018-04-12
        • 1970-01-01
        • 1970-01-01
        • 2011-03-13
        • 2016-12-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多