问题 异常+信令结束迭代器:为什么它在Java中是坏的而在Python中是正常的?


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

示例:Effective Java,第57项(“仅针对特殊情况使用例外”)和 Java专家新闻通讯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

例外StopIteration

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

为什么它对Java不好但对Python有好处?


13050
2017-10-17 21:10


起源

这两条规则都只是意见。也没有客观的技术原因。在一个部落中,意见往往会融合成一种社会规范,但在不同的部落中,可能会出现不同的规范。这只是一个例子。 - Tom Anderson
@Jason S:不能代表Python方面,但我认为 “在数组上迭代” 可能是一个不好的例子用于你的问题(很好的问题顺便说一句)。它的不好之处在于包装内容 试着抓 block可以阻止现代VM进行很酷的优化。 Joshua Block,in 有效的Java,也指出了这一点 “创建,抛出和捕获异常通常很昂贵”。因此,性能方面看起来非常糟糕(布洛赫称其整体“非常可怕”)。仅凭这个原因就可以了 “使用异常的数组迭代” 对Java来说似乎是一个坏主意。 - TacticalCoder
@ user988052:在Python中,设置try / except块的速度非常快,而且raise + catch也非常快。两者都是一次性操作;当然它比所有那些都快 if 陈述,至少对于多个​​要素。 - Petr Viktorin
@Petr Viktorin:非常感谢。因此,如果它在Python下快速发展,并且在Java中可能性能很差(通过 也 防止JVM优化进入,除了可能让try / throw / catch本身有点慢,这似乎是一个很好的理由,在这 “在数组上迭代”,它在Python中很好但在Java中很糟糕。 - TacticalCoder
@ user988052:真正的原因是可读性,预防性 TOCTTOU 错误和竞争条件,以及EAFP与鸭子打字相处得很好的事实。 Python关心的不仅仅是性能。但这是一个很好的奖金:) - Petr Viktorin


答案:


Python和Java有很多不同的异常方法。在Python中,异常是正常的。抬头 EAFP (Python术语表中更容易请求宽恕而非许可)。还检查一下 维基百科 不得不说。

StopIteration只是EAFP的一个例子 - 只需继续从迭代器中获取下一个东西,如果失败,则处理错误。

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


现在针对StopIteration的特定情况:考虑一下 发电机功能

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

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

我发现了Python的语义 - 只有在需要下一个值时才进行计算 - 这是最好的选择。

当然Java没有可恢复的生成器,所以这里很难比较。但是有一些传闻证据表明StopIteration比hasNext()更好地概括。


6
2017-10-17 21:40



has_next()方法的另一种方法,也许是更清晰的方法:生成器coult在达到函数结束后在内部设置一个标志。这将遵循仅在请求时计算值的语义。 - Rocoty
@Rocoty不完全 - 如果函数的结尾和函数中的最后一个yield之间存在语句,例如(可能)资源清理或某种日志记录,则不会按照您的预期设置该标志。这是否是一种好的做法......并非真正的重点;我认为没有任何东西可以允许标志方法并且在最后一次收益之后允许代码而不改变生成器从“在屈服”上暂停到“在实际屈服之后的下一个收益之前”的点。 - David Heyman
@DavidHeyman Kotlin 1.1实现了类似于我所说的协同程序。它在调用hasNext()时计算值,并在调用next()时返回计算值(如果尚未计算值,则next()将首先计算它)。 hasNext()(几乎)总是与next()一起调用。不过现在打字这个,我意识到这几乎就是Python的功能,但是有一个警卫而不是一个例外。 (请求许可而不是宽恕) - Rocoty


没有什么 停止 你使用java中的异常, 它只是看起来很难看,至少对于java开发人员来说。

主要原因是来自异常的堆栈跟踪是昂贵的, 也可能是Java开发人员可能会稍微担心一下 关于花费计算资源比Python开发人员。

Java也是一种相当“干净”的语言 - 有人会说基础主义, 这是一个很好的语言的原因之一。 (*见评论)

无论如何。原教旨主义者(和一些普通人)认为使用正常流量的例外不是正确的方式... :-)

但除此之外,最近的jvm检测到你正在产生大量的堆栈跟踪 对于相同的代码点,并且实际上会在没有它们的情况下“暂时”抛出异常来加快速度。


2
2017-10-17 22:18



*)“清洁度”也是锅炉板代码打字的原因,同时与地狱框架和xml配置作斗争,这些框架和xml配置是由一群似乎喜欢java的建筑宇航员,代码原教旨主义者和unix-hackers的邪恶集团所编写的。我在看你Maven!今天,我认为最糟糕的复杂因素是关注更新的更酷的东西,我们其余的人都花时间清理他们留下的混乱,比如早期的EJB等等...... - KarlP


Java有时候 可以也是:“所有的实现 DataInput 方法使用 EOFException 而不是返回值。“在这种情况下,没有办法使用通常的哨兵值, -1


1
2017-10-17 21:22



我猜测Python中会出现类似的用例。 - trashgod
不是真的 - 在Python中你不仅限于单一的返回类型。您可以从通常返回整数的函数中返回None。 (如果你想要惯用的Python代码,你应该引发异常,但这不是技术限制。) - Petr Viktorin


不推荐它的原因是因为在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...");

正如您从比较这两个片段中看到的那样,异常处理有点荒谬 - 对样本打算做什么有点过分。对于您发布的示例,更好的方法是这样的:

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

当事情真的出错时,例外情况更好用,例如IOException(“设备上没有剩余空间”),ClassNotFoundException(“找不到要运行的代码”)或NoRouteToHostException(“无法连接到主机”)。


1
2017-10-17 22:27





StopIteration 存在于Python中,可以在任何序列上进行简单迭代。

使用异常来实现这是一个设计选择,它实际上并不与Java的异常心态不一致。迭代器 next() 当没有更多元素时调用的方法是一个异常条件,并在一个场景中捕获该异常 for 循环是一种非常简单的方法来实现“在没有任何东西之前取物品”。


0
2017-10-17 21:28



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


对此没有正确或错误的答案。异常处理是一种中立的控制流构造,它的最佳用途取决于上下文和样式。

在这种情况下,Java和Python都做同样的事情,原因相同: java.util.Iteratornext() 使用 NoSuchElementException 发信号通知结束。这在两种语言中都是很好的风格:出于各种原因,使用特殊哨兵返回值的替代方案要差得多。

根据经验,只要编写想要发出信号的函数,就应该考虑使用异常 不止一种控制流程返回。这包括异常错误情况,但是异常信令的良好使用当然不限于此。


0
2017-10-17 22:02





Java中的异常捕获执行堆栈,这非常慢: Java异常有多慢?

如果在控制流程中使用异常 null 如 Throwable。否则,将通过层次结构调用标准Java librery代码 super 要求:

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);

0
2017-07-11 09:44