我应该遵循正常的道路还是要早点失败?


73

代码完整》一书附有以下引文:

“将正常情况放在if而不是之后else

这意味着在这种else情况下,应该放置与标准路径的异常/偏离。

但是,实用程序员教会我们“尽早崩溃”(第120页)。

我应该遵循哪一条规则?


15
@gnat不重复
BЈовић

16
两者不是互斥的,没有歧义。
jwenting 2014年

6
我不太确定代码完整的报价是否是很好的建议。大概是为了提高可读性,但是在某些情况下,您可能希望先将不常见的案例放在文本上。请注意,这不是答案。现有的答案已经可以很好地说明您的问题所引起的困惑。
2014年

14
早崩溃,难崩溃!如果您的一个if分支返回,请首先使用它。并避免else其余的代码,如果前提条件失败,您已经返回。代码更易于阅读,缩进更少...
CodeAngry 2014年

5
只是我认为这与可读性无关,而是可能只是针对静态分支预测进行优化的误导尝试?
Mehrdad 2014年

Answers:


189

“早期崩溃”与文本中哪一行代码更早有关。它告诉您在处理的最早步骤中检测到错误,这样您就不会无意间根据已经存在的错误状态进行决策和计算。

if/ else构造中,仅执行一个块,因此不能说它们构成了“较早”或“较晚”的步骤。因此,如何订购它们是易读性的问题,并且“不及早失效”不会成为决定。


2
您的总体观点很好,但我看到一个问题。如果具有(if / else if / else),则在“ if”语句中的表达式之后对“ else if”中计算的表达式进行求值。正如您所指出的,重要的一点是可读性与处理的概念单位。仅执行一个代码块,但可以评估多个条件。
Encaitar 2014年

7
@Encaitar,该粒度级别远小于使用短语“早期失败”时通常预期的粒度。
riwalk

2
@Encaitar这是编程的艺术。当面临多项检查时,其想法是首先尝试最有可能成为现实的一项。但是,该信息可能不是在设计时而是在优化阶段才知道的,但要提防过早的优化。
BPugh 2014年

公平的评论。这是一个很好的答案,我只想尝试使其更好地以备将来参考
Encaitar 2014年

像JavaScript,Python,perl,PHP,bash等脚本语言是例外,因为它们是线性解释的。在小型if/else构造中,这可能并不重要。但是在循环中或在每个块中包含很多语句的调用可能会以最常见的条件优先运行得更快。
DocSalvager 2014年

116

如果您的else语句仅包含失败代码,则很可能不存在该失败代码。

而不是这样做:

if file.exists() :
  if validate(file) :
    # do stuff with file...
  else :
    throw foodAtMummy
else :
  throw toysOutOfPram

做这个

if not file.exists() :
  throw toysOutOfPram

if not validate(file) :
  throw foodAtMummy

# do stuff with file...

您不想仅仅为了包含错误检查而深深地嵌套代码。

而且,正如其他人已经说过的那样,两条建议并不矛盾。一种是关于执行顺序,另一种是关于代码顺序


4
值得一提的是,如果您没有!,建议不要在块后放置正常流量,而在块后放置if异常流量。这样的Guard语句是处理大多数编码样式中的错误条件的首选形式。elseelse
Jules 2014年

+1这是一个很好的观点,实际上为如何订购带有错误条件的物料这一实际问题提供了某种答案。
ashes999 2014年

绝对更清晰,更易于维护。这就是我喜欢的方式。
约翰(John)

27

您应该同时遵循它们。

“早期崩溃” /“早期失败”建议意味着您应该尽快测试输入内容是否存在错误。
例如,如果您的方法接受应该为正数(> 0)的大小或计数,则早期失败建议意味着您在方法开始时就测试该条件,而不是等待算法产生废话结果。

将正常情况放在第一位的建议意味着,如果您测试某种状况,那么最可能的路径应该放在第一位。这有助于提高性能(因为处理器的分支预测将更加正确)和可读性,因为在尝试弄清楚函数在正常情况下的工作时,您不必跳过代码块。
当您测试前提条件并立即纾困(通过使用断言或if (!precondition) throw构造)时,此建议实际上并不适用,因为在读取代码时不会跳过任何错误处理。


1
您能否详细介绍分支预测部分?我不希望在if情况下运行的代码比在else情况下运行的代码更快。我的意思是,这就是分支预测的全部要点,不是吗?
Roman Reiner 2014年

@ user136712:在现代(快速)处理器中,在上一条指令完成处理之前就获取了指令。分支预测用于增加在执行条件分支时获取的指令也是要执行的正确指令的可能性。
Bart van Ingen Schenau 2014年

2
我知道什么是分支预测。如果我正确阅读了您的帖子,您说的if(cond){/*more likely code*/}else{/*less likely code*/}运行速度比if(!cond){/*less likely code*/}else{/*more likely code*/}分支预测要快。我认为分支预测不会偏向ifelse语句,而只会考虑历史。因此,如果else更有可能发生,那么它应该能够预测出同样好的结果。这个假设是错误的吗?
Roman Reiner 2014年

18

我认为@JackAidley 已经说过了要点,但让我这样制定:

没有例外(例如C)

在常规代码流中,您具有:

if (condition) {
    statement;
} else if (less_likely_condition) {
    less_likely_statement;
} else {
    least_likely_statement;
}
more_statements;

在“尽早出错”的情况下,您的代码突然显示为:

/* demonstration example, do NOT code like this */
if (condition) {
    statement;
} else {
    error_handling;
    return;
}

如果您发现了这种模式– returnelse(甚至是if)块中,请立即对其进行重新处理,以使所涉及的代码中没有else块:

/* only code like this at University, to please structured programming professors */
function foo {
    if (condition) {
        lots_of_statements;
    }
    return;
}

在现实世界…

/* code like this instead */
if (!condition) {
    error_handling;
    return;
}
lots_of_statements;

这样可以避免嵌套太深,满足“尽早爆发”的情况(有助于保持头脑清醒,使代码流向清晰)并且不会违反“将更多可能的东西放入if零件中”,因为根本就没有else零件。

C 和清理

受到一个类似问题的答案的启发(错了),这是使用C进行清理的方法。您可以在其中使用一个或两个出口点,这里是两个出口点之一:

struct foo *
alloc_and_init(size_t arg1, int arg2)
{
    struct foo *res;

    if (!(res = calloc(sizeof(struct foo), 1)))
        return (NULL);

    if (foo_init1(res, arg1))
        goto err;
    res.arg1_inited = true;
    if (foo_init2(&(res->blah), arg2))
        goto err;
    foo_init_complete(res);
    return (res);

 err:
    /* safe because we use calloc and false == 0 */
    if (res.arg1_inited)
        foo_dispose1(res);
    free(res);
    return (NULL);
}

如果清理工作较少,可以将它们折叠到一个出口点:

char *
NULL_safe_strdup(const char *arg)
{
    char *res = NULL;

    if (arg == NULL)
        goto out;

    /* imagine more lines here */
    res = strdup(arg);

 out:
    return (res);
}

goto如果可以处理的话,这种用法是非常好的。建议不要使用的建议goto针对无法自行决定使用的是好,可接受,不良,意大利面条式代码或其他用途的人。

例外情况

上面谈到了无例外的语言,我非常喜欢我自己(我可以更好地使用显式错误处理,并且惊喜更少)。引用igli:

<igli> exceptions: a truly awful implementation of quite a nice idea.
<igli> just about the worst way you could do something like that, afaic.
<igli> it's like anti-design.
<mirabilos> that too… may I quote you on that?
<igli> sure, tho i doubt anyone will listen ;)

但是,这里有一个建议,说明您如何使用一种例外的语言来表现出色,以及何时要很好地使用它们:

遇到异常时返回错误

您可以return通过引发异常来替换大多数早期的。但是,您的常规程序流,即程序未遇到的任何代码流,嗯,一个异常……一个错误条件或类似情况,都不会引发任何异常。

这意味着…

# this page is only available to logged-in users
if not isLoggedIn():
    # this is Python 2.5 style; insert your favourite raise/throw here
    raise "eh?"

……还可以,但是……

/* do not code like this! */
try {
    openFile(xyz, "rw");
} catch (LockedException e) {
    return "file is locked";
}
closeFile(xyz);
return "file is not locked";

… 不是。基本上,异常不是控制流元素。这也使Operations对您看起来很奇怪(“那些Java™程序员总是告诉我们这些异常是正常的”),并且可能阻碍调试(例如,告诉IDE仅在发生任何异常时中断)。异常通常要求运行时环境释放堆栈以产生回溯等。可能有更多的理由对此加以反对。

归结为:在支持异常的语言中,使用与现有逻辑和样式匹配的任何东西,感觉自然。如果要从头开始编写内容,请尽早达成协议。如果要从头开始编写库,请考虑一下您的消费者。(也永远不要abort()在库中使用……)但是,根据经验,如果操作通常在操作后(或多或少)继续进行,则不会抛出异常。

一般建议 例外情况

首先尝试使整个开发团队都同意在程序中使用所有Exception。基本上,计划一下。请勿大量使用它们。有时,即使在C ++,Java™,Python中,返回错误也更好。有时不是;用思想去使用它们。


通常,我将早期的回报视为代码的味道。如果以下代码由于不满足前提条件而失败,我将抛出异常。只是说
一声

1
@DanMan:我的文章是考虑到C编写的……我通常不使用Exceptions。但是我已经用(哎呀,它很长)建议wrt扩展了这篇文章。例外情况;顺便说一句,我们昨天在公司内部开发人员邮件列表上也有同样的问题……
mirabilos 2014年

另外,即使在单行ifs和fors上也要使用大括号。您不希望另一个goto fail;隐藏在标识中。
布鲁诺·金

1
@BrunoKim:这完全取决于您使用的项目的样式/编码约定。我在BSD上工作,但对此不满意(更多的光学混乱和垂直空间的损失);在$ dayjob上,但是我按照我们的约定将它们放置(对新手来说难度较小,出现错误的机会较少,如您所说)。
mirabilos 2014年

3

在我看来,“警卫条件”是使代码具有可读性的最好,最简单的方法之一。我真的很讨厌在if方法的开头看到else代码,因为它不在屏幕上,所以看不到代码。我必须向下滚动才能看到throw new Exception

将检查内容放在开头,这样阅读代码的人不必跳过整个方法来阅读它,而总是从上到下进行扫描。


2

(@mirabilos的回答非常好,但是我想如何得出相同的结论:)

稍后,我正在考虑自己(或其他人)阅读我的函数的代码。当我阅读第一行时,我无法对我的输入做任何假设(除非我不会进行任何检查)。所以我的想法是:“好吧,我知道我要用我的论点做事。但是首先让我们'清理它们'-即杀死不喜欢我的控制路径。”但是同时,我认为正常情况不是有条件的;我想强调它是正常的。

int foo(int* bar, int baz) {

   if (bar == NULL) /* I don't like you, leave me alone */;
   if (baz < 0) /* go away */;

   /* there, now I can do the work I came into this function to do,
      and I can safely forget about those if's above and make all 
      the assumptions I like. */

   /* etc. */
}

-3

这种条件排序取决于所讨论代码部分的关键性,以及是否存在可以使用的默认值。

换一种说法:

A.关键部分,没有默认值=>早期失败

B.非关键部分和默认值=>在else部分中使用默认值

C.中间案例=>根据需要决定每个案例


这仅仅是您的意见,还是您可以以某种方式解释/支持它?
蚊蚋

如每个选项所解释的(实际上没有很多词)为什么不使用它,这到底有没有备份?
Nikos M.

我不想这么说,但是在(我)这个答案中的不足是不合情理的:)。这是OP提出的问题,您是否还有其他答案是另一回事
Nikos M.

老实说,我看不到这里的解释。说,如果有人写另一种意见,例如“关键部分,没有默认值=>不要早失败”,那么这个答案将如何帮助读者选择两种相反的意见?考虑将其编辑为更好的形状,以适应“ 如何回答”准则。
gna 2014年

确定我看,这确实可能是另一种解释,但至少你了解哪些单词“临界区”和“不违约” 可以意味着一个战略,早失败,这的确是虽然是简约之一expanation
尼科斯·M.
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.