为什么“ ifif else”语句实际上从不采用表格式?


73
if   i>0 : return sqrt(i)  
elif i==0: return 0  
else     : return 1j * sqrt(-i)

VS

if i>0:  
   return sqrt(i)  
elif i==0:  
   return 0  
else:  
   return 1j * sqrt(-i)  

给定以上示例,我不明白为什么我实际上从来没有在代码库中看到过第一种样式。对我来说,您将代码转换为表格格式,可以清楚地显示所需内容。第一列实际上可以忽略。第二列标识条件,第三列为您提供所需的输出。至少在我看来,它直截了当且易于阅读。但是我总是看到这种简单的案例/切换情况是以扩展的,制表符缩进的格式出现的。这是为什么?人们是否发现第二种格式更具可读性?

唯一可能出现问题的情况是代码更改并变得更长。在那种情况下,我认为将代码重构为长缩进格式是完全合理的。每个人都只是因为它总是做第二件事而做第二种吗?作为恶魔的拥护者,我想还有另一个原因可能是因为人们根据if / else语句的复杂性找到了两种不同的格式,这令人困惑?任何见识将不胜感激。


91
因为人们发现第二个选项更具可读性?
GrandmasterB

65
与可能不返回值,可能跨越多行并可能产生副作用的分支相比,使用命令式语言时,互斥的分支都返回相同类型的值的用例通常不会出现。如果您看过函数式编程语言,您会经常看到类似于第一个示例的代码。
2013年

47
@horta“唯一可能出现问题的情况是代码更改并变得更长。” -您永远不要以为不会更改任何一段代码。代码重构占据了软件生命周期的大部分。
查尔斯·亚迪斯

7
@horta:与我无关。这是代码。在我正在阅读的代码库的上下文中,我想查看语句(和其他语言结构)是否一致地格式化,并且没有任何边缘情况。并不是我学会了以特定的方式阅读代码,我可以很好地阅读,但是如果所有内容都相同,则可读性更高。同样,对我来说不一样,其余的代码也一样。
GManNickG '16

44
而且,大多数调试器都是基于行的。if如果断言在同一行中,则不能在断言中放置断点。
isanae '16

Answers:


93

原因之一可能是您没有使用流行的语言。

一些反例:

Haskell的守卫和模式:

sign x |  x >  0        =   1
       |  x == 0        =   0
       |  x <  0        =  -1

take  0     _           =  []
take  _     []          =  []
take  n     (x:xs)      =  x : take (n-1) xs

Erlang模式:

insert(X,Set) ->
    case lists:member(X,Set) of
        true  -> Set;
        false -> [X|Set]
    end.

Emacs Lisp:

(pcase (get-return-code x)
  (`success       (message "Done!"))
  (`would-block   (message "Sorry, can't do it now"))
  (`read-only     (message "The shmliblick is read-only"))
  (`access-denied (message "You do not have the needed rights"))
  (code           (message "Unknown return code %S" code)))

通常,我看到表格式在函数式语言(以及基于通用表达式的语言)中非常流行,而换行在其他语言中最为流行(主要是基于语句的)。


10
我对此表示赞同,但普遍同意,但我有义务指出1.所有这些都是微不足道的回报。2.除语言本身的简洁性外,Haskellers还喜欢可笑的短标识符,以及3.语言倾向于基于表达式,甚至命令式语言的表达标准也不同于陈述式。如果OP将原始示例重写为函数应用程序,他可能会得到不同的建议……
Jared Smith

3
@JaredSmith感谢基于表达式/语句的拆分-我认为它可能比功能/命令更合适。同样,ruby几乎是基于表达式的,并且不经常使用该约定。(所有内容除外)关于第1点和第2点,我发现实际Haskell代码的50%以上是“琐碎的回报”,这只是更大内容的一部分-这就是代码的编写方式-不仅在此处的示例中。例如,这里接近一半的功能只是一个/两个衬垫。(某些行使用表格布局)
维拉托

是。我不是Haskeller,但做了一些ocaml,发现模式匹配往往比逻辑上等效的开关更简洁,无论如何,多态函数涵盖了很多方面。我想Haskell的类型类将进一步扩大覆盖范围。
贾里德·史密斯

我认为是模式案例语法促进了这一点。因为它更简洁并且通常更接近短开关盒,所以更容易表达为单线。出于类似的原因,我经常使用简短的switch-case语句来执行此操作。但是,字面if-else语句在有效地不是简单的三元数时,通常仍会分散在多行中。
伊西亚·梅多斯

@viraptor从技术上讲,haskell代码的其他50%是“非平凡的回报”,因为所有haskell函数在功能上都是纯函数,不会产生副作用。甚至从命令行读取并打印到命令行的函数也只是冗长的return语句。
法拉普

134

更具可读性。原因如下:

  • 几乎每种语言都使用这种语法(不是全部,大多数 -尽管您的示例似乎是Python)
  • isanae在评论指出,大多数调试器是基于行的(而不是基于语句的)
  • 如果必须内联分号或花括号,它看起来会更加难看
  • 从上到下阅读更顺畅
  • 如果您除了琐碎的return语句之外的其他内容, 这看起来非常难以理解
    • 略读代码时,任何缩进的有意义语法都会丢失,因为条件代码不再在视觉上分离(与Dan Neely分离)
    • 如果您继续将项目修补/添加到1行if语句中,这将特别糟糕。
  • 仅当您所有的if支票的长度都相同时才可读
  • 这意味着您不能将复杂的语句格式化为多行语句,它们必须是一体式的
  • 当我逐行垂直阅读而不是试图将多行解析在一起时,我更有可能注意到错误/逻辑流
  • 我们的大脑比水平的长文本更快地阅读窄而高的文本

尝试执行任何操作的那一刻,最终将其重写为多行语句。这意味着您只是浪费时间!

人们也不可避免地添加如下内容:

if i>0:  
   print('foobar')
   return sqrt(i)  
elif i==0:  
   return 0  
else:  
   return 1j * sqrt(-i)  

在确定此格式比其他格式好得多之前,并不需要经常这样做。啊,但是您可以将它们全部内联! 恩德兰在内部死亡

或这个:

if   i>0 : return sqrt(i)  
elif i==0 and bar==0: return 0  
else     : return 1j * sqrt(-i)

真的,真的很烦。没有人喜欢格式化这样的东西。

最后,您将开始“制表符多少个空间”问题的圣战。根据设置,在屏幕上以表格格式完美呈现的内容可能无法在我的屏幕上呈现。

无论如何,可读性不应该取决于IDE设置。


14
@horta,因为如果您首先要格式化它,就必须转换?我全力以赴,以减少对未来enderland的工作。当我可能向if检查中添加一些逻辑一点都不好玩时(忽略可读性的含义),制作漂亮的空格表并计数空格和制表符以更新视觉格式。
enderland '16

14
@horta他并不一定要说自己不会解决它,而是在说自己必须解决它,这花费了大量的时间在乏味的格式化而不是编程上。
Servy '16

11
@horta:恕我直言,您的方式通常不易读懂,而且格式上肯定很烦人。而且,仅当“ ifs”是一个小的衬管时才可以使用,这种情况很少。最后,恕我直言,由于这种原因,要携带两种“ if”格式是不好的。
dagnelies

9
@horta您似乎很幸运,可以在需求,系统,API和用户需求从未改变的系统上工作。你这幸运的灵魂。
enderland '16

11
我要补充一点:在单个条件下的任何细微变化都可能需要重新格式化其他条件以匹配该:位置->在CVS中进行比较,突然变得更难理解实际的变化。条件与身体的关系也是如此。将它们放在单独的行上意味着,如果仅更改其中之一,则差异明显表明仅条件发生了变化,而不是身体发生了变化。
巴库里

55

我坚信“代码被读取很多次,而被写入的次数很少,因此可读性非常重要。”

当我阅读别人的代码时,对我有帮助的关键是遵循我的眼睛经过训练可以识别的“正常”模式。我可以很容易地阅读缩进形式,因为我已经看过很多次了,几乎自动注册了(我的认知工作很少)。这不是因为它“更漂亮”,而是因为它遵循了我惯用的约定。大会胜过“更好” ...



11
这解释了为什么人们很保守。它没有解释为什么人们选择以某种特定的方式编写代码。
约尔根·福格

8
问题是“为什么我经常看到这种情况”,而不是这种风格从何而来。这两个问题都很有趣。我试图回答我以为被问到的那个。
Art Swri '16

16

除了已经提到的其他缺点外,表格布局还增加了版本控制合并冲突的几率,需要人工干预。

当需要重新排列表格格式的代码块时,版本控制系统会将这些行中的每一行都视为已修改:

diff --git a/foo.rb b/foo.rb
index 40f7833..694d8fe 100644
--- a/foo.rb
+++ b/foo.rb
@@ -1,8 +1,8 @@
 class Foo

   def initialize(options)
-    @cached_metadata = options[:metadata]
-    @logger          = options[:logger]
+    @metadata = options[:metadata]
+    @logger   = options[:logger]
   end

 end

现在假设与此同时,在另一个分支中,程序员在对齐的代码块中添加了新行:

diff --git a/foo.rb b/foo.rb
index 40f7833..86648cb 100644
--- a/foo.rb
+++ b/foo.rb
@@ -3,6 +3,7 @@ class Foo
   def initialize(options)
     @cached_metadata = options[:metadata]
     @logger          = options[:logger]
+    @kittens         = options[:kittens]
   end

 end

合并该分支将失败:

wayne@mercury:/tmp/foo$ git merge add_kittens
Auto-merging foo.rb
CONFLICT (content): Merge conflict in foo.rb
Automatic merge failed; fix conflicts and then commit the result.

如果要修改的代码未使用表格对齐方式,则合并将自动成功。

(此答案在我自己的文章中被“ pla窃”,不鼓励代码中的表格对齐方式)。


1
有趣,但这不是合并工具的失败吗?在这种情况下特别是git?这是达成约定的简单方法。对我来说,这是可以改进的(从工具方面)。
Horta

7
@horta对于合并工具以几乎不总是破坏代码的方式修改空白,它必须了解在不更改代码含义的情况下可以以何种方式修改空白。还必须了解所使用的特定表格对齐方式。这不仅取决于语言(Python!),而且可能需要该工具在某种程度上理解代码。另一方面,基于行的合并可以在没有AI的情况下完成,并且通常甚至不会破坏代码。
韦恩·康拉德

知道了 因此,如评论中其他地方所提到的,在我们拥有将表直接合并到该表的IDE或编程输入格式之前,工具问题将始终存在,这对于那些喜欢表的人来说是很难的。
奥尔塔

1
@horta正确。我对代码中表格对齐的大多数反对意见都可以通过足够先进的工具来解决。
韦恩·康拉德

8

如果所有内容始终适合分配的宽度,则表格格式可能会非常不错。但是,如果某些东西超出了分配的宽度,那么通常有必要使表的一部分与其余部分不对齐,或者调整表中其他所有东西的布局以适合长项。

如果源文件是使用旨在处理表格格式数据的程序进行编辑的,并且可以通过使用较小的字体大小来处理过长的项目,并将它们在同一单元格中分成两行,等等,那么使用表格格式可能就有意义格式化的频率更高,但是大多数编译器都希望源文件没有此类编辑器为了保持格式而需要存储的标记。在最佳情况下,使用缩进量可变但没有其他布局的行并不像表格格式那样好,但是在最坏情况下,它不会引起那么多问题。


真正。我注意到我使用的文本编辑器(vim)对表格格式甚至宽文本都提供了可怕的支持。我还没有看到其他文字编辑器做得更好。
Horta

6

有一个'switch'语句为特殊情况提供了这种功能,但是我想这并不是您要问的。

我已经看过if语句是否采用表格式,但是必须有大量条件才能使其值得。3如果以传统格式最好地显示语句,但是如果您有20条,那么将其以大块格式显示起来会更容易,而大块的格式可以使其更加清晰。

重点是:清晰度。如果它更易于查看(并且您的第一个示例不容易看到:分隔符在哪里),则将其格式化以适合这种情况。否则,请坚持人们的期望,因为这总是更容易识别。


1
OP似乎正在使用Python,所以没有switch
贾里德·史密斯

2
“ 3,如果用传统格式最好地显示语句,但是如果您有20,则……”那么您有更大的问题要考虑!
:)

@GrimmTheOpiner如果您正在编写语言解析器或AST字符串化器,那么这很可能要处理。例如,我曾经为JavaScript解析器做过贡献,在其中我将函数拆分成15-20种情况,每种表达式类型一种。我将大多数情况按自己的功能进行了细分(以显着提高性能),但是需要很长的switch时间。
伊西亚·梅多斯

@JaredSmith:显然switch是邪恶的,但是实例化字典然后对它执行查询以进行琐碎的分支并不是邪恶的……
Mark K Cowan

1
@MarkKCowan哦,我抓到了讽刺,但以为你在用它嘲笑我。互联网上缺乏上下文等等。
贾里德·史密斯

1

如果您的表达确实如此简单,那么大多数编程语言都提供?:分支运算符:

return  ( i > 0  ) ? sqrt( i)
      : ( i == 0 ) ? 0
        /* else */ : 1j * sqrt( -i )

这是一种简短的可读表格格式。但是重要的是:我一眼就能看出“主要”行动是什么。这是一个退货声明!该值由某些条件决定。

另一方面,如果您有执行不同代码的分支,那么我发现缩进这些块的可读性更高。因为现在根据if语句有不同的“主要”操作。在一种情况下,我们抛出,在一种情况下,我们记录并返回或仅返回。取决于逻辑,会有不同的程序流,因此代码块封装了不同的分支,并使它们对开发人员更为突出(例如,快速读取函数以掌握程序流)

if ( i > 0 )
{
    throw new InvalidInputException(...);
}
else if ( i == 0 )
{
    return 0;
}
else
{
    log( "Calculating sqrt" );
    return sqrt( -i );
}

7
实际上,我发现您的“短可读表格格式”是一场噩梦,而OP所建议的格式就可以了。
Matteo Italia

@MatteoItalia这个编辑版本怎么样?
法尔科

5
对不起,还更糟;我认为这是因为与?:相比if/ else关键字更难发现和/或由于符号的“噪声”增加而引起的。
Matteo Italia

@MatteoItalia:我有超过一百种不同价值的案例。该表值使检查错误成为可能。使用多行,这是不可能的。
gnasher729

1
@ gnasher729-对于100个不同的“值”,我通常发现声明每个“项目”的数据结构并将其全部列为表格中这些数据结构数组的初始化要好得多。(当然,语言限制可能在此处适用)。如果任何项目需要“计算”方面,则项目结构可以包含指针或对执行所需动作的函数的引用。对于可能的应用程序,这可以大大简化代码并简化维护。
迈克尔·卡拉斯,2016年

1

正如enderland已经说过的那样,您假设您只有一次“返回”作为操作,并且可以将“返回”标记为条件的结尾。我想提供一些额外的细节,说明为什么这将无法成功。

我不知道您首选的语言是什么,但是我已经用C编码很长时间了。围绕着许多编码标准,其目的是通过不允许在初始编码中或在以后的维护期间易于出错的代码构造来避免一些标准编码错误。我对MISRA-C最熟悉,但是还有其他一些,并且通常它们都有相似的规则,因为它们使用相同的语言来解决相同的问题。

编码标准经常解决的一个普遍错误是这个小陷阱:

if (x == 10)
    do_something();
    do_something_else();

这并没有按照您的想法做。就C而言,如果x为10,则您调用do_something(),但是无论x的值如何,do_something_else()都会被调用。仅紧接“ if”语句之后的操作是有条件的。这可能就是编码人员的意图,在这种情况下,维护人员可能会陷入困境。否则可能不是编码人员想要的,在这种情况下会出现错误。这是一个受欢迎的面试问题。

编码标准中的解决方案是对所有条件操作都强制使用大括号,即使它们是单行也是如此。现在我们得到

if (x == 10)
{
    do_something();
    do_something_else();
}

要么

if (x == 10)
{
    do_something();
}
do_something_else();

现在它可以正常工作,并且对于维护人员来说很清楚。

您会注意到这与您的表样式格式完全不兼容。

其他一些语言(例如Python)研究了这个问题,并决定由于编码人员正在使用空格来使布局清晰明了,所以最好使用空格而不是花括号。因此,在Python中,

if x == 10:
    do_something()
    do_something_else()

对x和= 进行调用,do_something()do_something_else()以x == 10为条件,而

if x == 10:
    do_something()
do_something_else()

表示仅以do_something()x为条件,并且do_something_else()始终被调用。

这是一个有效的概念,您会发现几种语言都在使用它。(我是在Occam2中第一次看到它的,时光倒流。)不过,您可以轻松地看到您的表样式格式与该语言不兼容。


1
我想你错过了重点。您提到的问题是导致C出现的奇怪的C特定的非标准噩梦。如果使用C语言进行编码,则绝不建议您使用建议的表格格式的替代简单if方法。相反,由于使用的是C,因此应在一行上全部使用大括号。大括号实际上将使表格格式更加清晰,因为它们充当分隔符。
奥尔塔

同样,在这种情况下的return语句仅是示例。通常,这本身可能就是代码气味。我仅指的是简单语句的格式,而不必完全包含return语句。
Horta

2
我的观点是,这使得表格格式更加笨拙。顺便说一句,它不是特定于C的-它由C派生的所有语言共享,因此C ++,C#,Java和JavaScript都允许相同的陷阱。
格雷厄姆

1
我不介意这是return语句-我明白你的意图是显示简单的语句。但这变得更加麻烦。当然,一旦任何语句变得不简单,您就需要更改格式,因为表格式无法维护。除非您要进行代码混淆,否则长行代码本身就是一种味道。(原始限制为80个字符,如今通常为130个字符左右,但一般原则仍然认为您不必滚动即可看到行尾。)
Graham

1

表格布局在少数情况下可能很有用,但是在if中很少有用。

在简单的情况下,?:可能是一个更好的选择。在中等情况下,通常更适合使用开关(如果您的语言适合的话)。在复杂的情况下,您可能会发现呼叫表更合适。

在重构代码时,有很多次我将其重新排列为表格形式,以使其显得明显。我很少这样说,因为在大多数情况下,一旦您理解了问题,便有了更好的解决方法。有时,编码惯例或布局标准禁止这样做,在这种情况下,注释是有用的。

有关的问题?:。是的,它是三元运算符(或者我想将其视为值)。乍一看,这个示例对于?:有点复杂(并且过度使用?:不利于可读性,但会伤害它),但是经过一番思考,可以将该示例重新排列为以下形式,但是我认为在这种情况下,开关是最易读的解。

if i==0: return 0
return i>0?sqrt(i):(1j*sqrt(-i))

1
您可能必须弄清楚“-:”是什么意思(例如,举个例子,可能与问题有关)。
Peter Mortensen

我假设这是三元运算符。我觉得这是有道理的,因为三元运算符趋向于重新排列标准,例如,要进行日常操作,否则要进行人们日常使用的其他工作格式,因此可以轻松阅读。
Horta

@PeterMortensen:如果新手不知道这意味着什么,他们应该远离代码,直到他们问了明显的问题并学到了。
gnasher729

@horta三元是if ? do stuff : do other stuff。与if / else的顺序相同。
纳文

1
@Navin啊,也许这只是我最常使用的语言(python)的失败。stackoverflow.com/questions/394809/…–
奥尔塔

-3

我看不到表格格式有什么问题。个人喜好,但我会使用这样的三元:

return i>0  ? sqrt(i)       :
       i==0 ? 0             :
              1j * sqrt(-i)

无需return每次重复:)


1
正如一些评论中提到的那样,return语句不是理想的选择,也不是本文的重点,只是我在网上找到的一小段代码并以几种方式格式化。
Horta

Python三元do_something() if condition() else do_something_else()不是condition() ? do_something() : do_something_else()
伊赛亚·梅多斯

@IsiahMeadows OP从未提及Python。
纳文
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.