Java中的LBYL和EAFP是什么?

79

最近我在自学Python时,了解到关于代码执行前的错误检查中的LBYL/EAFP习惯用法。在Python中,EAFP似乎是被认可的风格,并且它与该语言的结合效果很好。

LBYL(Look Before You Leap)

def safe_divide_1(x, y):
    if y == 0:
        print "Divide-by-0 attempt detected"
        return None
    else:
        return x/y

EAFP(它的缩写来自于英语单词 Easier to Ask Forgiveness than Permission):

def safe_divide_2(x, y):
    try:
        return x/y
    except ZeroDivisionError:  
        print "Divide-by-0 attempt detected"
        return None
我的问题是:我从Java和C++的背景中甚至没有听说过使用EAFP作为主要数据验证结构。在Java中使用EAFP是否明智?或者异常处理会不会造成太多的开销?我知道只有在实际抛出异常时才会产生开销,所以我不确定为什么不使用更简单的EAFP方法。这只是偏好吗?
5个回答

146

如果你正在访问文件,EAFP比LBYL更可靠,因为LBYL涉及的操作不是原子操作,文件系统在你查看和跳转之间可能会发生变化。实际上,标准名称是TOCTOU - 检查时间,使用时间;由于检查不准确而导致的错误称为TOCTOU错误。

考虑创建一个必须具有唯一名称的临时文件。找出所选文件名是否已存在的最佳方法是尝试创建它-确保您使用选项以确保如果该文件已经存在则操作失败(在POSIX / Unix术语中,使用O_EXCL标志来打开())。如果您试图测试文件是否已经存在(可能使用access()),则在那个说“否”的时间和您尝试创建文件之间,其他人或其他事物可能已经创建了该文件。

相反,假设您尝试读取现有文件。您检查文件是否存在(LBYL)可能会说“它在那里”,但当您实际打开它时,您会发现“它不存在”。

在这两种情况下,您必须检查最终操作,并且LBYL不能自动帮助您。

(如果您正在处理SUID或SGID程序,则access()会询问不同的问题;它可能与LBYL相关,但代码仍然必须考虑失败的可能性。)


3
很好的例子,Jonathan。对于处理并发编程和双重检查锁模式的Java开发人员来说,这可能很有意义。 - David Mann
这是一个很好的观点,但我不认为这真的与LBYL vs EAFP有关。例如,每当您打开文件时,您基本上都在执行相同的操作,即返回错误的一次操作,您并没有在实际执行之前检查是否可以打开它。我认为这样的代码可以是LBYL:多个函数调用,每个函数调用都可能失败,并且每个函数都会立即进行检查。TOCTOU是一个更普遍的问题。此外,您的答案涉及正确性问题,而我认为问题是关于两种解决方案都正确的情况。 - ctn

59
除了Python和Java中异常的相对成本之外,还要记住它们之间哲学/态度上的区别。Java试图在类型(和其他方面)上非常严格,要求明确、详细地声明类/方法签名。它假定你应该知道,在任何时候,你正在使用什么类型的对象以及它能够做什么。相比之下,Python的“鸭子类型”意味着你不能确定(也不应该关心)一个对象的具体类型是什么,你只需要关心当你要求它时它是否会“嘎嘎叫”。在这种宽容的环境中,唯一明智的态度是假设事情会运行正常,但必须准备好应对如果它们不能正常工作的后果。Java天然的限制性并不适合这种随意的方法。(这不是要贬低任何一种方法或语言,而是要说这些态度是每种语言的习惯用法,将不同语言之间的习惯用法相互借鉴往往会导致尴尬和糟糕的沟通……)

5
此外,http://oranlooney.com/lbyl-vs-eafp/ 提供了关于这两种方法的优缺点。 - Tim Lewis
我不同意Python中的松散类型使得使用EAFP更或者更少明智。当你请求原谅时,你请求原谅预期的情况。例如,如果“取消”方法将要取消一个对象,我们将捕获我们预期会返回的异常。我们预计取消可能会失败,因为该对象具有防止其被取消的活动关系,并且我们希望将其传达回UI。如果取消方法由于某些未预料到的除零情况而失败,我们希望它像程序中的任何其他错误一样正常失败。 - David Baucum
链接已失效,可以在此处阅读文章:https://web.archive.org/web/20161208191318/http://www.oranlooney.com/lbyl-vs-eafp/。 - cookiemonster

12

在Python中,异常处理比Java更高效,这至少是你在Python中看到这种结构的原因之一。在Java中,使用异常处理方式更加低效(从性能方面来说)。


2
mipadi,你有任何关于Python如何实现这一点的见解吗? - duffymo
3
好的,我会尽力进行翻译。以下是需要翻译的内容:@duffymo 我有同样的问题,发现了这里的解答:https://dev59.com/M3RB5IYBdhLWcg3wiHll - Tim Lewis
2
@duffymo 大部分讨论集中在 LBYL vs EAFP 上,但其中一个答案链接到了关于异常在 CPython 中实际实现的文档:http://docs.python.org/c-api/intro.html#exceptions - Tim Lewis

10

考虑以下代码片段:

def int_or_default(x, default=0):
    if x.isdigit():
        return int(x)
    else:
        return default

def int_or_default(x, default=0):
    try:
        return int(x)
    except ValueError:
        return default

这两个看起来都正确,对吧?但其中一个是错误的。

前者使用LBYL会失败,因为isdigit和isdecimal之间存在微妙的区别。当用字符串“①²³₅”调用时,它会抛出错误而不是正确地返回默认值。

后者使用EAFTP会得出正确的结果,根据定义。因为需要该要求的代码正是断言该要求的代码,所以行为上不存在不匹配的范围。

使用LBYL意味着将内部逻辑复制到每个调用点中。而不是拥有您的要求的一个规范编码,每次调用函数时您都有免费机会搞砸。

值得注意的是,EAFTP不是关于异常的,尤其是Java代码不应普遍使用异常。它是关于给正确的代码块分配正确的工作。例如,使用Optional返回值是编写EAFTP代码的一种完全有效的方法,并且比LBYL更有效地确保正确性。


那只是因为在第一种情况下,你没有选择一个适合你的情况的函数。如果你选择了一个能够准确检查你想要的内容的函数,那么你所提出的问题就不会存在。 - Silidrone
@Silidron 所有的编程错误都是由于没有编写完全正确的代码!这就是问题所在!当函数已经可以完美地集成此检查时,为什么每次从使用它的每个程序员调用该函数时都会有机会犯这种错误呢? - Veedrac
有一个机会可以避免在int()转换中抛出ValueError,然后第二个示例也会出错。 - Silidrone
@Silidrone 是的,但这个机会只会在代码内部发生一次,而这段代码已经负责了解和处理所有特定的 int 解析细节。相比于让那些可能对这些细节知之甚少的人在每个调用点上都有这个机会。编写 int 的人自然会知道数字和小数之间的区别,因为这是他们的职责,并且代码已经在解析它。大多数调用 int 的人不会知道,因为他们正在解决其他问题,他们只想让它工作并离开。 - Veedrac
1
@Silidrone,LBYL基本上是错误的。你正在执行操作X来检查操作Y是否有效。但是操作X并不是操作Y,因此它不能确定地告诉你操作Y是否有效 - 这除了LBYL也是有缺陷的,因为它是一个TOCTOU漏洞。这个问题是LBYL根本性缺陷的完美例子。 - Andrew Henle

8

个人认为,这也得到了传统的支持,EAFP从来不是一种好的方式。 你可以将其视为以下等价物:

if (o != null)
    o.doSomething();
else
    // handle

相对于:

try {
    o.doSomething()
}
catch (NullPointerException npe) { 
    // handle
}

此外,请考虑以下内容:
if (a != null)
    if (b != null)
        if (c != null)
            a.getB().getC().doSomething();
        else
            // handle c null
    else
        // handle b null
else
    // handle a null

这可能看起来不太优雅(是的,这只是一个粗略的例子 - 请耐心等待),但与将所有内容都包装在 try-catch 中以获得 NullPointerException 并尝试弄清楚您何时以及为什么会遇到它相比,它为您处理错误提供了更高的粒度。
在我看来,除了极少数情况外,永远不应使用 EAFP。另外,因为你提出了这个问题:是的,即使未抛出异常,try-catch 块也会产生一些开销。

40
这种 EAFP 的使用方式部分取决于你所测试的异常是否经常发生。如果不太可能发生,那么采用 EAFP 是合理的。如果它们很常见,那么 LBYL 或许更好。答案可能还取决于可用的异常处理范式。在 C 语言中,需要采用 LBYL 方法。 - Jonathan Leffler
11
“常见例外”其实并不算是异常情况,因此在这种情况下采用“先检查后执行”(LBYL)的方式会更好,你觉得呢? - Fredy Treboux
9
说“即使异常未被抛出,try-catch块仍会产生一些开销”就像说“即使方法没有被调用,方法定义仍然会产生一些开销”。换句话说,在方法/异常被调用之前,这种开销是可以忽略不计的。Erickson 在我询问这个问题时解释得最清楚了。 - Iain Samuel McLean Elder
13
我完全不同意。从逻辑上讲,EAFP确实是一种很好的验证方法。因为如果已经存在会抛出异常的检查,额外的检查只会导致不同步,而且没有任何节省。然而,由于C++、Java和C#中异常相对较慢,当预计出现故障但发生频率很低时,需要使用LBYL验证。 - Jan Hudec
21
在多线程环境中,采用“EAFP”编程风格还可以提高安全性(消除一些竞态条件)。 - Mark E. Haase
显示剩余5条评论

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