否则块会增加代码复杂度吗?[关闭]


18

这是一个非常简化的示例。这不一定是特定于语言的问题,我想请您忽略函数编写的许多其他方式以及可以对其进行的更改。。颜色是一种独特的类型

string CanLeaveWithoutUmbrella()
{
    if(sky.Color.Equals(Color.Blue))
    {
        return "Yes you can";
    }
    else
    {
        return "No you can't";
    }
}

我遇到的很多人,ReSharper和这个人(其评论提醒我我一直想问这个问题已经有一段时间了)建议重构代码以删除else留下的代码块:

(我不记得大多数人所说的话,否则我可能不会问这个)

string CanLeaveWithoutUmbrella()
{
    if(sky.Color.Equals(Color.Blue))
    {
        return "Yes you can";
    }
    return "No you can't";
}

问题:不包括else块会导致复杂性增加吗?

else通过说明两个模块中的代码直接相关的事实,我给人的印象更直接地表明了意图。

另外,我发现我可以防止逻辑上的细微错误,尤其是在以后对代码进行修改之后。

以我的简化示例的这种变化形式为例(or由于这是故意简化的示例,因此请忽略运算符):

bool CanLeaveWithoutUmbrella()
{
    if(sky.Color != Color.Blue)
    {
        return false;
    }
    return true;
}

现在,有人可以if在第一个示例之后基于条件添加新的块,而无需立即正确地认识到第一个条件正在对自己的条件施加约束。

如果else存在一个块,则无论谁添加了新条件,都将被迫去移动该else块的内容(如果他们以某种方式掩盖了该启发式,将显示代码无法访问,在一个if约束另一个约束的情况下不会) 。

当然,无论如何都应定义特定示例的其他方式,所有这些方式都可以避免这种情况,但这只是一个示例。

我给出的示例的长度可能会歪曲此示例的视觉效果,因此假设占据方括号的空间相对于该方法的其余部分而言无关紧要。

我忘记提及我省略了else块的情况,并且是在使用if块来施加逻辑上必须满足所有后续代码(例如,空检查(或任何其他保护措施)的约束)的情况。


实际上,当有“ if”语句和“ return”语句时,在所有情况中> 50%的情况下,可以将其视为“防护块”。因此,我认为您的误解是,“警戒线”是例外情况,而您的例子是常规情况。
布朗

2
@AssortedTrailmix:我同意在编写该else子句时,上述情况可能更具可读性(根据我的喜好,在省略不必要的括号时,它甚至更具可读性。但是我认为示例的选择不正确,因为在上面的案例我会写return sky.Color == Color.Blue(没有if / else),返回类型与bool不同的示例可能会更清楚这一点
Doc Brown

1
正如上面评论中指出的那样,建议的示例过于简化,引起了推测性的答案。至于以“有人现在可以添加一个新的if块...”开头的问题部分,它已经被问过很多次并回答过,例如,参见多种条件检查方法?以及与此相关的多个问题。
t

1
我看不到其他与DRY原理有什么关系。DRY与抽象和以函数,方法和对象形式引用常用代码有关,比您要的要多。您的问题与代码的可读性以及可能的约定或惯用语有关。
Shashank Gupta 2014年

2
投票重新开放。这个问题的好答案并非特别基于观点。如果问题中给定的示例不够充分,那么通过编辑进行改进将是合理的做法,而不是出于实际上并非如此的理由而投票反对关闭。
Michael Shaw

Answers:


27

我认为,显式的else块是可取的。当我看到这个:

if (sky.Color != Blue) {
   ...
}  else {
   ...
}

我知道我正在处理互斥选项。我不需要阅读if块中的内容就可以知道。

当我看到这个:

if (sky.Color != Blue) {
   ...
} 
return false;

乍一看,它看起来无论如何都会返回false,并且具有可选的副作用,天空不是蓝色。

如我所见,第二个代码的结构并未反映该代码的实际功能。那很糟。而是选择反映逻辑结构的第一个选项。我不想在每个块中检查返回/中断/继续逻辑以了解逻辑流程。

为什么有人会偏爱另一个人,这对我来说还是个谜,希望有人会采取相反的立场会给我启发。

我忘了提到我同意省略else块的情况,并且是在使用if块应用必须在逻辑上满足所有后续代码(例如,空检查(或任何其他保护措施)的约束)的情况)。

我想说的是,如果您离开了幸福的道路,则警卫条件还可以。发生错误或故障,阻止了常规执行的继续。发生这种情况时,您应该引发异常,返回错误代码或适合您的语言的任何内容。

在此答案的原始版本之后不久,在我的项目中发现了如下代码:

Foobar merge(Foobar left, Foobar right) {
   if(!left.isEmpty()) {
      if(!right.isEmpty()) {
          Foobar merged = // construct merged Foobar
          // missing return merged statement
      }
      return left;
   }
   return right;
}

通过不将返回值放在其他值中,忽略了返回语句丢失的事实。如果雇用其他人,则代码甚至都不会编译。(当然,我更担心的是,用此代码编写的测试非常糟糕,无法检测到此问题。)


关于这一点的总结,我很高兴我不是唯一这样认为的人。我与程序员,工具和IDE一起工作,更喜欢删除该else块(当我被迫这样做只是为了保持与代码库的约定一致时,可能会很麻烦)。我也很想在这里看到对策。
Selali Adob​​or 2014年

1
我对此有所不同。在某些情况下,显式的其他方法并不能使您受益匪浅,因为它不太可能触发OP提及的细微逻辑错误-只是噪音。但是,大多数情况下,我都同意,有时我会做一些类似} // else的事情,作为两者之间的折衷方案。
Telastyn 2014年

3
@Telastyn,但是跳过其他会得到什么?我同意,在许多情况下,这种好处是微不足道的,但即使是微不足道的好处,我也认为值得这样做。当然,在可能是代码的地方加一些注释对我来说似乎很奇怪。
Winston Ewert 2014年

2
我认为您与OP有相同的误解-带有“ return”的“ if”通常可以看作是保护块,因此您在这里讨论的是例外情况,而恕我直言,更频繁地使用保护块的情况下,如果没有“其他”。
布朗

...所以您写的内容没有错,但是恕我直言,不值得您进行很多投票。
布朗

23

删除else我发现的块的主要原因是多余的缩进。else块的短路使代码结构更加平坦。

许多if陈述本质上是警惕。他们正在防止取消引用空指针和其他“不要这样做!”。错误。并且它们导致当前功能的快速终止。例如:

if (obj === NULL) {
    return NULL;
}
// further processing that now can assume obj attributes are set

现在看来这里有一个else子句很合理:

if (obj === NULL) {
    return NULL;
}
else if (obj.color != Blue) {
   // handle case that object is not blue
}

确实,如果这就是您要做的一切,那么与上面的示例相比,它的缩进没有太多。但是,这几乎是您使用真正的多级数据结构所做的全部工作(处理JSON,HTML和XML等常见格式时,这种情况一直存在)。因此,如果要修改给定HTML节点的所有子级的文本,请说:

elements = tree.xpath(".//p");
if (elements === NULL) {
    return NULL;
}
p = elements[0]
if ((p.children === NULL) or (p.children.length == 0)) {
    return NULL;
}
for (c in p.children) {
    c.text = c.text.toUpperCase();
}
return p;

防护措施不会增加代码的缩进。有了一个else子句,所有实际工作开始向右移动:

elements = tree.xpath(".//p");
if (elements === NULL) {
    return NULL;
}
else {
    p = elements[0]
    if ((p.children === NULL) or (p.children.length == 0)) {
        return NULL;
    }
    else {
        for (c in p.children) {
            c.text = c.text.toUpperCase();
        }
        return p;
    }
}

这开始有明显的向右运动,这是一个很简单的例子,只有两个后卫条件。每增加一个防护,就会增加一个缩进级别。我编写的处理XML或使用详细的JSON feed的真实代码可以轻松叠加3、4或更多保护条件。如果您始终使用else,则最终会缩进4、5或6级。也许更多。这无疑会增加代码的复杂性,并花费更多的时间来理解什么与什么保持一致。快速,扁平,提前退出的样式消除了一些“多余的结构”,并且看起来更简单。

附录有关此答案的评论使我意识到,可能尚不清楚NULL /空处理不是保护条件的唯一原因。挺远的!虽然这个简单的示例着重于NULL /空处理,并且不包含其他原因,但是例如在XML和AST之类的深层结构中搜索“相关”内容,例如,通常会有一系列特定测试来剔除不相关的节点和不感兴趣的内容。案件。一系列的“如果此节点不相关,则返回”测试将导致与NULL /空防护一样的向右漂移。在实践中,随后的相关性测试通常与NULL /空防护符结合使用,以搜索相应的更深的数据结构-向右漂移的双重打击。


2
这是一个详细而有效的答案,但是我将其添加到问题中(起初我没想到);有一个“例外”,当我else使用一个if块来施加必须所有后续代码在逻辑上都满足的约束(例如,空检查(或任何其他保护措施))时,我总是会发现一个多余的块。
Selali Adob​​or 2014年

@AssortedTrailmix我相信,如果排除常规可以在then子句中提前退出的情况(例如连续的保护语句),那么避免使用该else子句没有什么特别的价值。确实,这将降低清晰度,并可能带来危险(通过未能简单说明存在/应该存在的替代结构)。
乔纳森·尤尼斯

我认为,一旦您离开了幸福的道路,使用警卫条件就可以退出。但是,对于处理XML / JSON的情况,我还发现,如果您首先根据模式验证输入,则会得到更好的错误消息和更清晰的代码。
Winston Ewert 2014年

这是避免其他情况的完美解释。
克里斯·希夫豪尔

2
我会包装API而不产生愚蠢的NULL。
Winston Ewert 2014年

4

当我看到这个:

if(something){
    return lamp;
}
return table;

我看到一个常见的成语。一种常见模式,在我看来,它转换为:

if(something){
    return lamp;
}
else return table;

因为这是编程中非常普遍的习惯用法,所以习惯于看到这类代码的程序员在看到它们时,脑海中就会隐含着“翻译”,将其“转换”为正确的含义。

我不需要显式的else,我的头为我“把它放在那里”,因为它可以将整个模式识别出来。我了解整个通用模式的含义,因此不需要小细节来阐明它。

例如,阅读以下文本:

在此处输入图片说明

你读过这个吗?

我在春天爱巴黎

如果是这样,那就错了。再次阅读文本。实际写的是

春天爱巴黎

文本中有两个 “ the”词。那你为什么错过这两个之一?

这是因为您的大脑看到了共同的模式,并立即将其转换为已知的含义。它不需要仔细阅读整个事物来找出含义,因为它已经在看到模式时就识别出了模式。这就是为什么它忽略了多余的“ the ”。

与上述习语相同。曾经阅读大量代码的程序员习惯于看到这种模式if(something) {return x;} return y;。他们并不需要else了解它的含义,因为他们的大脑“为他们准备了它”。他们认识到它与已知的意义的图案:“什么是大括号后才会运行作为隐含else先前的if”。

因此,我认为这else是不必要的。但是,只需使用看起来更易读的内容即可。


2

它们在代码中增加了视觉混乱,因此,是的,它们可能会增加复杂性。

但是,相反的情况也是如此,过度的重构以减少代码长度也会增加复杂性(当然,在这种简单情况下不是这样)。

我同意该声明else表明意图的说法。但是我的结论是不同的,因为您要在代码中添加复杂性以实现此目的。

我不同意您的说法,即它可以防止逻辑上的细微错误。

让我们看一个例子,现在仅当天空为蓝色且湿度较高,湿度不高或天空为红色时才返回true。但是,然后,您错误地认为一个括号并且把它放在错误的地方else

bool CanLeaveWithoutUmbrella() {
    if(sky.Color == Color.Blue)
    {
        if (sky.Humidity == Humidity.High) 
        {
            return true;
        } 
    else if (sky.Humidity != Humidity.High)
    {
        return true;
    } else if (sky.Color == Color.Red) 
    {
            return true;
    }
    } else 
    {
       return false;
    }
}

我们已经在生产代码中看到了所有这些愚蠢的错误,并且可能很难发现。

我建议以下内容:

我发现它更易于阅读,因为我一眼就能看到默认值为false

另外,强制移动块的内容以插入新子句的想法容易出错。这在某种程度上与开放/封闭原则有关(代码应该开放以进行扩展,但封闭以进行修改)。您被迫修改现有代码(例如,编码器可能在花括号平衡中引入错误)。

最后,您正在引导人们以您认为合适的预定方式回答。例如,您提供了一个非常简单的示例,但随后您声明人们不应该考虑它。但是,示例的简单性影响了整个问题:

  • 如果这种情况与提出的情况一样简单,那么我的答案将是完全省略任何if else条款。

    return(sky.Color == Color.Blue);

  • 如果情况比较复杂,有各种条款,我会回答:

    bool CanLeaveWithoutUmbrella(){

    if(sky.Color == Color.Blue && sky.Humidity == Humidity.High) {
        return true;
    } else if (sky.Humidity != Humidity.High) {
        return true;
    } else if (sky.Color == Color.Red) {
        return true;
    }
    
    return false;
    

    }

  • 如果情况很复杂,并且并非所有子句都返回true(也许它们返回一个double),并且我想引入一些断点或loggin命令,我会考虑以下内容:

    double CanLeaveWithoutUmbrella() {
    
        double risk = 0.0;
    
        if(sky.Color == Color.Blue && sky.Humidity == Humidity.High) {
            risk = 1.0;
        } else if (sky.Humidity != Humidity.High) {
            risk = 0.5;
        } else if (sky.Color == Color.Red) {
            risk = 0.8;
        }
    
        Log("value of risk:" + risk);
        return risk;
    

    }

  • 如果我们不是在讨论返回值的方法,而是在if-else块中设置一些变量或调用一个方法,那么是的,我将包含final else子句。

因此,示例的简单性也是要考虑的因素。


我觉得您在犯别人犯的同样错误,使示例过度变形,因此研究了一个新问题,但是我需要花一点时间并研究获取示例的过程,因此现在看来对我有效。另外,这与打开/关闭原理无关。范围太狭窄,无法应用这种习语。但是有趣的是,它在更高层次上的应用将消除问题的整个方面(通过消除对功能的任何修改)。
Selali Adob​​or 2014年

1
但是,如果我们不能修改这个过分简化的示例添加子句,也不能修改它的编写方式,也不能对其进行任何更改,那么您应该考虑到这个问题仅与那些精确的代码行有关,而不再是一个普遍的问题了。在这种情况下,您是完全正确的,它不会增加else子句的复杂性(也不会删除它),因为开头没有复杂性。但是,您并不公平,因为您只是暗示可能会添加新子句的想法。
FranMowinckel 2014年

而且,正如我要指出的那样,我并不是要说这与众所周知的打开/关闭原理有关。我认为引导人们按照您的建议修改代码的想法很容易出错。我只是从那个原则中得出这个想法。
FranMowinckel 2014年

继续进行编辑,您的工作进展顺利,我现在要输入评论
Selali Adob​​or 2014年

1

尽管所描述的if-else情况具有更大的复杂性,但是在大多数(但不是全部)实际情况下,它并不重要。

几年前,当我在为航空业使用软件(必须经过认证过程)时,您的问题浮出水面。事实证明,该“ else”语句存在财务成本,因为它增加了代码复杂性。

这就是为什么。

'else'的出现创建了一个隐式的第三种情况,必须对其进行评估-'if'和'else'都不被采用。您可能在想(就像我当时一样)WTF?

解释是这样的...

从逻辑的角度来看,只有两个选项-“ if”和“ else”。但是,我们要证明的是编译器正在生成的目标文件。因此,当出现“ else”时,必须进行分析以确认所生成的分支代码正确并且没有出现神秘的第三条路径。

将其与只有“ if”而没有“ else”的情况相反。在这种情况下,只有两个选项-“ if”路径和“ non-if”路径。评估两种情况比三种情况复杂。

希望这可以帮助。


在目标文件中,两种情况都不会呈现为相同的jmp?
Winston Ewert 2014年

@WinstonEwert-人们期望他们是一样的。但是,无论重叠多少,渲染的内容和期望渲染的内容都是两件不同的事情。
闪亮的

(我想SE吃了我的评论...)这是一个非常有趣的答案。虽然我会说,它暴露的情况是有点小众,但它确实表明一些工具会发现(两者之间的差异,即使基于流我看,圈复杂度,将测量没有区别最常见的代码度量。
Selali Adobor 2014年

1

代码的最重要目标是被提交时理解为可理解的(相对于易于重构,这是有用的,但重要性不大)。稍微复杂一点的Python示例可以用来说明这一点:

def value(key):
    if key == FROB:
        return FOO
    elif key == NIK:
        return BAR
    [...]
    else:
        return BAZ

这很清楚-相当于case语句或字典查找,它是的更高版本return foo == bar

KEY_VALUES = {FROB: FOO, NIK: BAR, [...]}
DEFAULT_VALUE = BAZ

def value(key):
    return KEY_VALUES.get(key, default=DEFAULT_VALUE)

这更清楚了,因为即使令牌的数量更高(带有两个键/值对的原始代码的令牌数量为32 vs 24),该函数的语义现在也很明确:这只是一个查找。

(这会对重构产生影响-如果您想对if产生副作用,则key == NIK有以下三种选择:

  1. 恢复为if/ elif/ else样式,并在key == NIK情况下插入一行。
  2. 在返回之前,将查找结果保存为一个值,并为特殊情况添加一条if语句value
  3. if调用方中添加一条语句

现在我们知道为什么查找功能是一个强大的简化方法:它使第三个选项显然是最简单的解决方案,因为除了产生比第一个选项小的差异外,它还是唯一确保value仅执行一项操作的选项。

回到OP的代码,我认为这可以作为else语句复杂性的指南:具有显式else语句的代码在客观上更加复杂,但是更重要的是它是否使代码的语义清晰和简单。由于每段代码都有不同的含义,因此必须逐案回答。


我喜欢您的查询表重写了简单的开关盒。而且我知道这是最受欢迎的Python习惯用法。但是,在实践中,我经常发现多向条件(aka caseswitch语句)的同质性较低。几个选项只是直接的值返回,但是一两种情况需要额外的处理。而当你发现查找策略不适合,你需要在一个完全不同的重写if elif...... else语法。
乔纳森·尤尼斯

这就是为什么我认为查找通常应该是单行或函数的原因,因此查找结果周围的逻辑与首先查找它的逻辑是分开的。
l0b0 2014年

该问题为绝对应提及并牢记的问题带来了更高层次的观点,但是我认为,在您可以根据自己的立场随意扩展的情况下,已经施加了足够的限制
Selali Adob​​or

1

我认为这取决于if您使用哪种声明。一些if语句可以看作是表达式,而其他语句只能看作是控制流语句。

您的示例对我来说似乎是一种表达。在类似C的语言中,三元运算符?:非常类似于if语句,但是它是一个表达式,并且else部分不能省略。在表达式中不能省略else分支是有道理的,因为表达式必须始终具有值。

使用三元运算符,您的示例将如下所示:

bool CanLeaveWithoutUmbrella()
{
    return (sky.Color == Color.Blue)
               ? true
               : false;
}

由于它是一个布尔表达式,因此实际上应该一直简化为

bool CanLeaveWithoutUmbrella()
{
    return (sky.Color == Color.Blue);
}

包含一个明确的else子句可以使您的意图更加清晰。警卫在某些情况下可能很有意义,但是在两个可能的值(无论是异常值还是异常值)之间进行选择时,它们没有帮助。


+ 1,OP正在恕我直言,正在讨论一种病理病例,应该更好地按照上面的方式编写。以我的经验,“ if”和“ return”更为常见的情况 “后卫”情况。
布朗

我不知道为什么这种情况一直在发生,但是人们一直无视问题的第一段。实际上,您都在使用我明确声明不使用的确切代码行。I ask that you ignore the many other ways the function can be written, and changes that can be made to it. (I know most people are itching to write return sky.Color == Color.Blue.)
Selali Adob​​or 2014年

重新阅读最后一个问题,您似乎会误解该问题的意图。
Selali Adob​​or 2014年

您使用了一个实际上希望简化的示例。我阅读并关注了您的第一段,这就是为什么我的第一个代码段完全存在的原因,而不是直接跳到执行该特定示例的最佳方法。如果您认为我(或任何其他人)误解了您的问题的意图,也许您应该对其进行编辑以使您的意图明确。
迈克尔·肖

我会对其进行编辑,但是我不知道如何使它变得更加清晰,因为我使用的是您执行的确切代码行,然后说“不要这样做”。您可以通过多种方式来重写代码并消除问题,我无法涵盖所有​​问题,但是有很多人理解我的发言并受到尊重……
Selali Adob​​or 2014年

1

在这一论点中要考虑的另一件事是每种方法所养成的习惯。

  • if / else从分支的角度重构编码样本。正如您所说,有时这种明确性是好的。确实有两种可能的执行路径,我们要强调一下。

  • 在其他情况下,只有一条执行路径,然后程序员必须担心所有这些异常的事情。如您所述,保护子句对此非常有用。

  • 当然,存在3个或更多的情况(n),我们通常鼓励放弃if / else链,并使用case / switch语句或多态性。

我在这里要牢记的指导原则是,一种方法或函数应仅具有一个目的。任何带有if / else或switch语句的代码仅用于分支。因此,它应该短得短(4行),并且仅委托给正确的方法或产生明显的结果(如您的示例)。当您的代码这么短时,很难错过那些多重返回:)就像“ goto被认为是有害的”一样,任何分支语句都可能被滥用,因此,将一些批判性思想放入else语句中通常是值得的。正如您所提到的,仅具有else块就为代码聚集提供了空间。您和您的团队将必须决定您对此的感觉。

该工具真正抱怨的是,如果在if块中有返回/中断/抛出的事实。因此,就您而言,您写了一个保护条款,但搞砸了。您可以选择使该工具满意,也可以“接受”其建议而不接受它。


我从未考虑过这样的事实,即该工具一旦适合某个模式就可能会考虑是否将其作为保护子句,事实可能就是这种情况,并且它解释了一个“支持者”的原因,因为它不存在
Selali Adob​​or 2014年

@AssortedTrailmix是的,您正在从else语义上考虑,代码分析工具通常会寻找无关的指令,因为它们经常会引起对控制流程的误解。返回的else之后if是浪费的位。 if(1 == 1)通常会触发同样的警告。
史蒂夫·杰克逊

1

我认为答案取决于它。您的代码示例(如您间接指出的那样)只是返回条件的评估,并且如您所知,最好以单个表达式表示。

为了确定添加else条件是否使代码清晰或模糊,您需要确定IF / ELSE表示什么。它是一个表达式(如您的示例所示)吗?如果是这样,则应提取并使用该表达式。这是警卫条件吗?如果是这样,则ELSE是不必要的并且具有误导性,因为这会使两个分支显得相等。它是程序多态性的一个例子吗?提取它。是否设置了前提条件?那么这可能是必不可少的。

在决定包含或限制ELSE之前,请先确定其代表什么,这将告诉您是否应显示ELSE。


0

答案有点主观。一个else块可以通过建立的代码执行一个块专门到另一个代码块可读性,而不需要通过两个块来读取。例如,如果两个块都相对较长,这将很有帮助。在某些情况下,具有ifs而不带elses的可读性更高。将以下代码与许多if/else块进行比较:

if(a == b) {
    if(c <= d) {
        if(e != f) {
            a.doSomeStuff();
            c.makeSomeThingsWith(b);
            f.undo(d, e);
            return 9;
        } else {
            e++;
            return 3;
        }
    } else {
        return 1;
    }
} else {
    return 0;
}

与:

if(a != b) {
    return 0;
}

if(c > d) {
    return 1;
}

if(e != f) {
    e++;
    return 3;
}

a.doSomeStuff();
c.makeSomeThingsWith(b);
f.undo(d, e);
return 9;

第二段代码将嵌套if语句更改为保护子句。这减少了缩进,并使一些返回条件更清晰(“ if a != b,then we return 0!”)


评论中已经指出,我的示例可能存在一些漏洞,因此我将进一步充实它。

我们可以这样写出第一段代码,以使其更具可读性:

if(a != b) {
    return 0;
} else if(c > d) {
    return 1;
} else if(e != f) {
    e++;
    return 3;
} else {
    a.doSomeStuff();
    c.makeSomeThingsWith(b);
    f.undo(d, e);
    return 9;
}

该代码等同于上述两个模块,但带来了一些挑战。else此处的子句这些if语句连接在一起。在上述保护子句版本中,保护子句彼此独立。添加一个新逻辑意味着只需在逻辑if的最后一个if和其余逻辑之间编写一个新逻辑。在这里,这也可以完成,只需要少量的认知负担即可。但是,不同之处在于何时需要在该新的保护子句之前发生一些附加的逻辑。当语句独立时,我们可以执行以下操作:

...
if(e != f) {
    e++;
    return 3;
}

g.doSomeLogicWith(a);

if(!g.isGood()) {
    return 4;
}

a.doSomeStuff();
...

对于连接的if/else链,这并非易事。事实上,最简单的方法是将链条分解成更类似于警卫条款的形式。

但这确实是有道理的。if/else弱使用意味着零件之间的关系。我们可以推测,a != b在某种程度上与c > dif/else链接的版本。从保护子句版本可以看出,这种假设实际上是不相关的,这会产生误导。这意味着我们可以对独立子句做更多的事情,例如重新排列它们(可能是为了优化查询性能或改善逻辑流程)。一旦经过它们,我们也可以在认知上忽略它们。在一个if/else链中,我不太确定a和之间的等效关系b以后不会弹出。

else在深层嵌套的示例代码中,隐含的关系也很明显,但方式不同。的else语句从它们所连接的条件分离,并且它们在相关的反向顺序的条件句(第一else附连到最后if,最后一个else被附接至第一if)。这造成了认知失调,我们不得不在代码中回溯,试图排挤我们的大脑。有点像试图匹配一堆括号。如果缩进是错误的,那就更糟了。可选的花括号也无济于事。

我忘了提到我同意省略else块的情况,并且是在使用if块应用必须在逻辑上满足所有后续代码(例如,空检查(或任何其他保护措施)的约束)的情况)。

这是一个不错的让步,但实际上并没有使任何答案无效。我可以在您的代码示例中这样说:

bool CanLeaveWithoutUmbrella()
{
    if(sky.Color != Color.Blue)
    {
        return false;
    }
    return true;
}

编写这样的代码的有效解释可能是“如果天空不是蓝色,我们只能带伞离开”。这里有一个约束,天空必须是蓝色,才能使下面的代码有效运行。我已经达到了您让步的定义。

如果我说如果天空是黑色,那是一个晴朗的夜晚,那么就没有伞了怎么办。(有更简单的方法可以编写此示例,但是有更简单的方法可以编写整个示例。)

bool CanLeaveWithoutUmbrella()
{
    if(sky.Color == Color.Blue)
    {
        return true;
    }

    if(sky.Color == Color.Black)
    {
        return true;
    }

    return false;
}

就像上面的较大示例一样,我可以简单地添加new子句,而无需考虑太多必要。在中if/else,我不能确定自己不会弄乱什么,尤其是在逻辑更复杂的情况下。

我还将指出,您的简单示例也不是那么简单。非二进制值的测试与具有反转结果的测试的反向测试不同。


1
我对问题的编辑解决了这种情况。当逻辑上必须满足所有后续代码的约束(例如,空检查(或任何其他保护措施))时,我同意省略else块对于可读性至关重要。
Selali Adob​​or 2014年

1
您的第一个版本很难掌握,但是我认为使用if / else并不需要。看看我怎么写:gist.github.com/winstonewert/c9e0955bc22ce44930fb
Winston Ewert 2014年

@WinstonEwert查看编辑以回复您的评论。
cbojar 2014年

您的编辑提出了一些有效的观点。而且我当然认为保护条款很有用。但是在一般情况下,我赞成else子句。
Winston Ewert 2014年

0

您已经简化了太多示例。 取决于较大的情况,这两种选择中的任何一种都可能更具可读性:具体而言,这取决于条件的两个分支之一是否异常。让我告诉你:在任何一个分支都有相同可能性的情况下,...

void DoOneOfTwoThings(bool which)
{
    if (which)
        DoThingOne();
    else
        DoThingTwo();
}

...比...更具可读性

void DoOneOfTwoThings(bool which)
{
    if (which) {
        DoThingOne();
        return;
    }
    DoThingTwo();
}

...因为第一个具有平行结构,而第二个则没有。但是,当功能的大部分专用于一个任务,而您只需要首先检查一些前提条件时,...

void ProcessInputOfTypeOne(String input)
{
    if (InputIsReallyTypeTwo(input)) {
        ProcessInputOfTypeTwo(input);
        return;
    }
    ProcessInputDefinitelyOfTypeOne(input);

}

...比...更具可读性

void ProcessInputOfTypeOne(String input)
{
    if (InputIsReallyTypeTwo(input))
        ProcessInputOfTypeTwo(input);
    else
        ProcessInputDefinitelyOfTypeOne(input);
}

...因为故意设置并行结构为将来的读者提供了一个线索,即在这里获得“真正的第二类型”输入是异常的。(在现实生活中,它ProcessInputDefinitelyOfTypeOne不是一个单独的功能,因此区别将更加明显。)


对于第二种情况,最好选择一个更具体的示例。我对此的立即反应是,“如果您继续进行处理,那不会是异常。”
Winston Ewert 2014年

@WinstonEwert 异常可能是错误的说法。但是我同意Zack的观点,即如果您有一个默认值(case1)和一个例外,则应将其写为» if->“执行例外情况并离开”作为早期回报策略。考虑一下其中默认包含更多检查的代码,首先处理特殊情况会更快。
Thomas Junk 2014年

这似乎是我正在谈论的情况when using an if block to apply a constraint that must be logically satisfied for all following code, such as a null-check (or any other guards)。从逻辑上讲,任何工作ProcessInputOfTypeOne都取决于!InputIsReallyTypeTwo(input),因此我们需要在常规处理之外进行捕获。通常情况下ProcessInputOfTypeTwo(input);,确实如此throw ICan'tProccessThatException会使相似性更加明显。
Selali Adob​​or 2014年

@WinstonEwert我也许可以说得更好,是的。我想到的是手动编写令牌生成器时会遇到很多情况:例如,在CSS中,字符序列“ u+通常会引入“ unicode-range”令牌,但是如果下一个字符不是十六进制数字,那么需要备份并将“ u”作为标识符进行处理。因此,主扫描程序循环会分派一些不精确的“扫描Unicode范围令牌”,它以“ ...如果我们处于这种特殊情况下,请备份,然后调用scan-an-identifier子例程”。
zwol 2014年

@AssortedTrailmix即使我想到的示例并非来自可能不使用异常的旧式代码库,也不是错误条件,只是调用的代码ProcessInputOfTypeOne使用了不精确但快速的检查,有时捕获类型二。
zwol 2014年

0

是的,因为else子句是多余的,所以复杂度有所增加。因此最好保留一些代码以保持精简和合理。与省掉多余的东西在同一个弦上this限定符(Java,C ++,C#)以及在return语句中省略括号。

一个好的程序员认为“我可以添加什么?” 而一位优秀的程序员则认为“我可以删除什么?”。


1
当我看到该else块的遗漏时,如果以单线形式进行解释(这种情况并不常见),则通常是采用这种推理,因此+1表示了我所看到的“默认”参数,但我没有发现这是一个足够有力的推理。A good programmer things "what can I add?" while a great programmer things "what can I remove?".似乎是很多人在没有仔细考虑的情况下过度扩张的普遍说法,我个人认为这就是这种情况。
Selali Adob​​or 2014年

是的,阅读您的问题后,我立即感到您已经下定决心,但想要确认。答案显然不是您想要的,但有时考虑您不同意的观点很重要。也许也有东西吗?
vidstige 2014年

-1

我注意到,在这种情况下我改变了主意。

当大约一年前被问到这个问题时,我会回答类似:

“ 一种方法应该只有一个entry和一个exit”«考虑到这一点,我建议将代码重构为:

bool CanLeaveWithoutUmbrella()
{
    bool result

    if(sky.Color == Color.Blue)
    {
        result = true;
    }
    else
    {
        result = false;
    }

    return result;
}

从沟通的角度来看,很清楚应该做什么。If事情是这样的,操控结果的一种方式else 操纵它以另一种方式

但这涉及不必要的 else。在else表达了默认情况下。所以在第二步中,我可以将其重构为

bool CanLeaveWithoutUmbrella()
{
    bool result=false

    if(sky.Color == Color.Blue)
    {
        result = true;
    }
    return result;
}

然后问题提出了:为什么根本要使用一个临时变量?重构的下一步将导致:

bool CanLeaveWithoutUmbrella()
{

    if(sky.Color == Color.Blue)
    {
        return true;
    }
    return false;
}

所以这很清楚。我什至更进一步:

bool CanLeaveWithoutUmbrella()
{

    if(sky.Color == Color.Blue) return true;
    return false;
}

还是对于胆小的人

bool CanLeaveWithoutUmbrella()
{

    if(sky.Color == Color.Blue) { return true; }
    return false;
}

您可以这样阅读:»CanLeaveWithoutUmbrella返回bool。如果没有什么特别的情况,则返回false «这是从上到下的第一次读取。

然后,您“解析”条件»如果满足条件,则返回true «您可以完全跳过条件,并且已经了解了该函数在默认情况下的作用。您的眼睛和心灵只需要浏览左侧的一些字母,而无需阅读右侧的部分。

我给人的印象是,其他人通过陈述两个块中的代码直接相关的事实来更直接地说明意图。

是的,这是正确的。但是这些信息是多余的。您有一个默认案例和一个if-exception 涵盖的“例外” 。

当处理更复杂的结构时,我会选择另一种策略,根本不考虑这个if或丑陋的兄弟switch


+1冗余无疑显示不仅复杂性增加,而且混乱也增加。我知道我总是因为冗余而不得不仔细检查像这样编写的代码。
Dietbuddha 2014年

1
您可以在案件始于So this is clear in what it does...并展开后再移除案件吗?(之前的案例也具有可疑的意义)。我之所以这样说,是因为这是我们要研究的问题。您已经声明了自己的立场,省略了else块,实际上这是一种立场,需要更多的解释(这是让我问这个问题的一种立场)。来自问题的I ask that you ignore the many other ways the function can be written, and changes that can be made to it. (I know most people are itching to write return sky.Color == Color.Blue.)
答案

@AssortedTrailmix当然可以!我应该详细说明什么?由于您最初的问题是“其他代码块还会增加代码的复杂性吗?”,而我的答案是:是的。在这种情况下,可以省略。
Thomas Junk 2014年

和不!我不明白那票选票。
Thomas Junk

-1

简而言之,在这种情况下,摆脱else关键字是一件好事。这在这里是完全多余的,并且根据您格式化代码的方式,它将在代码中添加一到三行完全无用的行。考虑一下if块中的内容:

return true;

因此,如果if要输入该块,则根据定义,该函数的其余部分不会执行。但是,如果没有输入,则由于没有其他if语句,因此将执行该函数的其余部分。如果return语句不在if块中,那将完全不同,在这种情况下,else块的存在或不存在都会产生影响,但是在这里,它不会产生影响。

在您的问题中,反转if条件并省略之后else,您陈述了以下内容:

现在,有人可以在第一个示例之后基于条件添加新的if块,而无需立即正确地认识到第一个条件正在对自己的条件施加约束。

然后,他们需要更仔细地阅读代码。我们使用高级语言和清晰的代码组织来缓解这些问题,但是添加完全冗余的else块会在代码中添加“噪声”和“杂波”,我们也不必颠倒或重新颠倒我们的if条件。如果他们不能分辨出差异,那么他们将不知道自己在做什么。如果他们能分辨出差异,那么他们总是可以if自己反转原始条件。

我忘了提到我同意省略else块的情况,并且是在使用if块应用必须在逻辑上满足所有后续代码(例如,空检查(或任何其他保护措施)的约束)的情况)。

基本上,这就是这里正在发生的事情。您将从该if块中返回,因此if条件评估到false 一个逻辑上必须满足的所有后续代码约束。

不包含else块会导致复杂性增加吗?

不,这将降低代码的复杂度,甚至可能降低编译后的代码,具体取决于某些超级琐碎的优化是否会发生。


2
“然后他们需要更加仔细地阅读代码。” 存在编码风格,因此程序员可以减少对挑剔细节的关注,而将更多的注意力放在实际的工作上。如果您的风格使您不必要地注意那些细节,那就不好了。“ ...完全没用的行...”它们不是没用的-它们使您一目了然地看到结构是什么,而不必浪费时间去思考如果执行此部分或该部分会发生什么。
迈克尔·肖

@MichaelShaw我认为Aviv Cohn的答案就在我正在谈论的内容中。
Panzercrisis

-2

在您提供的特定示例中,它确实可以。

  • 它强制审阅者再次阅读该代码,以确保它不是错误。
  • 它增加了(不必要的)循环复杂性,因为它在流程图中增加了一个节点。

我认为这再简单不过了:

bool CanLeaveWithoutUmbrella()
{
    return (sky.Color == Color.Blue);
}

6
我专门解决了这个非常简化的示例可以重写以删除该if语句的事实。实际上,我明确建议不要使用您使用的确切代码进一步简化。另外,该else模块不会增加循环复杂性。
Selali Adob​​or 2014年

-4

我总是尝试编写我的代码,以使意图尽可能清晰。您的示例缺少上下文,因此我将对其进行一些修改:

class Sky {
    Sky(Colour c)
    {
        this.color = c;
    }
    bool CanLeaveWithoutUmbrella()
    {
        if(this.color == Color.Blue)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

我们的意图似乎是,当天空是蓝色时,我们可以没有伞就离开。但是,这种简单的意图在大量的代码中丢失了。我们有几种方法可以使它更容易理解。

首先,您对else分支有疑问。我不介意任何一种方式,但倾向于忽略else。我理解有关显式的论点,但我的观点是if语句应像“分支”一样包裹“可选”分支return true。我不会将return false分支称为“可选”,因为它是我们的默认行为。无论哪种方式,我都认为这是一个非常小的问题,并且远没有以下问题重要:

class Sky {
    Sky(Colour c)
    {
        this.color = c;
    }
    bool CanLeaveWithoutUmbrella()
    {
        if(this.color == Color.Blue)
        {
            return true;
        }
        return false;
    }
}

一个更重要的问题是,你的分支正在做的太过分了!像所有事物一样,分支应该“尽可能简单,但没有更多”。在这种情况下,您使用了一条if语句,当您真正需要在其间分支的全部是表达式(值)时,该分支便会在代码块(分支)之间分支。这很明显,因为a)您的代码块仅包含单个语句,并且b)它们是相同的语句()。我们可以使用三元运算符将分支范围缩小到不同的部分:return?:

class Sky {
    Sky(Colour c)
    {
        this.color = c;
    }
    bool CanLeaveWithoutUmbrella()
    {
        return (this.color == Color.Blue)
            ? true
            : false;
    }
}

现在我们达到明显的冗余,我们的结果与相同this.color == Color.Blue。我知道您的问题要求我们忽略这一点,但是我认为指出这是一种非常一级的,程序化的思维方式非常重要:您正在分解输入值并构建一些输出值。很好,但是如果可能的话,考虑输入和输出之间的关系转换会更有效。在这种情况下,它们之间的关系显然是相同的,但是我经常看到像这样复杂的程序代码,它掩盖了一些简单的关系。让我们继续进行以下简化:

class Sky {
    Sky(Colour c)
    {
        this.color = c;
    }
    bool CanLeaveWithoutUmbrella()
    {
        return (this.color == Color.Blue);
    }
}

我要指出的第二件事是您的API包含双负数:可能CanLeaveWithoutUmbrellafalse。这,当然,简化了NeedUmbrella幸福true,让我们这样做:

class Sky {
    Sky(Colour c)
    {
        this.color = c;
    }
    bool NeedUmbrella()
    {
        return (this.color != Color.Blue);
    }
}

现在,我们可以开始考虑您的编码风格。看起来您正在使用面向对象的语言,但是通过使用if语句在代码块之间分支,您最终编写了程序代码。

在面向对象的代码中,所有代码块都是方法,并且所有分支都是通过例如动态分配进行的。使用类或原型。是否使事情变得更简单是有争议的,但是它应该使您的代码与语言的其余部分更加一致:

class Sky {
    bool NeedUmbrella()
    {
        return true;
    }
}

class BlueSky extends Sky {
    bool NeedUmbrella()
    {
        return false;
    }
}

注意,在函数式编程中,使用三元运算符在表达式之间进行分支是很常见的。


答案的第一段似乎正好回答该问题,但是它立即变成了确切的主题,对于这个非常简单的示例,我明确要求一个忽略的主题。来自问题:I ask that you ignore the many other ways the function can be written, and changes that can be made to it. (I know most people are itching to write return sky.Color == Color.Blue.)。实际上,您使用的是我提到的确切代码行。另外,尽管问题的这一部分是题外话,但我想说您的推断太过分了。您已从根本上更改了代码。
Selali Adob​​or 2014年

@AssortedTrailmix这纯粹是样式问题。关心样式是有意义的,而忽略样式是有意义的(仅关注结果)。我认为关心return false;vs else { return false; }不必关心分支极性,动态调度,三元体系等是没有道理的。如果您使用OO语言编写过程代码,则有必要进行重大更改;)
Warbo

通常,您可以说类似的话,但是当您使用定义明确的参数回答问题时(尤其是在不遵守这些参数的情况下,您可以“抽象”整个问题),该方法不适用。我还要说的最后一句话是完全错误的。OOP是关于“过程抽象”的,而不是“过程消除”的。我要说的是,您从一个班轮排到无限多个班级(每种颜色一个班级),这一事实说明了原因。另外,我仍然想知道动态分配应该与一条if/else语句做什么,以及什么是分支极性。
Selali Adob​​or 2014年

我从来没有说过OO解决方案更好(实际上,我更喜欢函数式编程),我只是说它与语言更加一致。“极性”是指哪个东西是“ true”,哪个是“ false”(我用“ NeedUmbrella”进行了切换)。在OO设计中,所有控制流都是由动态调度确定的。例如,Smalltalk中不允许别的en.wikipedia.org/wiki/Smalltalk#Control_structures(见c2.com/cgi/wiki?HeInventedTheTerm
Warbo

2
在我到达最后一段(关于OO)之前,我会对此进行投票,相反,这使它赢得了反对票!这正是产生馄饨代码的OO构造的意想不到的过度使用,就像意大利面条一样难以理解。
zwol 2014年
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.