使用大括号(即{})来包含单行if或循环的目的是什么?

341

我正在阅读一些C ++讲师的讲义,他写了以下内容:

  1. 使用缩进 // 好的
  2. 永远不要依赖于运算符优先级 - 始终使用括号 // 好的
  3. 即使只有一行也要使用 { } 块 // 不好,为什么?
  4. 比较中将 const 对象放在左侧 // 好的
  5. 对于变量大于或等于0,请使用unsigned // 很好的技巧
  6. 在删除后将指针设置为NULL - 双重删除保护 // 还不错

第三种技术对我来说不太清楚:将一行代码放在{ ... }中会给我带来什么好处?

例如,看这个奇怪的代码:

int j = 0;
for (int i = 0 ; i < 100 ; ++i)
{
    if (i % 2 == 0)
    {
        j++;
    }
}

并将其替换为:

int j = 0;
for (int i = 0 ; i < 100 ; ++i)
    if (i % 2 == 0)
        j++;

使用第一版本的好处是什么?


265
易读性和可维护性。不够明显哪个语句块属于“j++”,并且在它之后添加代码不会与if语句相关联。 - The Forest And The Trees
30
我一直被告知要在这些行中使用大括号{},有几个原因。它使代码更易于阅读。此外,也许六个月后的别人需要编辑你的代码,所以清晰度很重要,并且有了括号,出错的可能性较小。从技术上讲,并没有更正确的做法,这只是一个良好实践的问题。请记住,项目可能有成千上万行的代码需要新手去处理! - RossC
30
我不同意6,因为它会隐藏双重删除并可能隐藏逻辑错误。 - Luchian Grigore
29
第五个可能有些棘手 - 考虑这个循环:for (unsigned i = 100; i >= 0; --i) - Archie
38
顺便提一下,(i % 2 == 0)与(2)相矛盾。你在依赖运算符优先级,当然意思应该是((i % 2) == 0)而不是(i % (2 == 0))。我会将规则2分类为“有效的观点,但‘总是’是错误的”。 - Steve Jessop
显示剩余33条评论
24个回答

524

让我们尝试在增加j时也修改i

int j = 0;
for (int i = 0 ; i < 100 ; ++i)
    if (i % 2 == 0)
        j++;
        i++;

哦,不好了!从Python的角度来看,这似乎是可以的,但实际上并不是,因为它相当于:

int j = 0;
for (int i = 0 ; i < 100 ; ++i)
    if (i % 2 == 0)
        j++;
i++;

当然,这是一个愚蠢的错误,但即使是有经验的程序员也可能犯这种错误。

另一个非常好的原因ta.speot.is's answer中指出。

第三个我能想到的原因是嵌套的if语句:

if (cond1)
   if (cond2) 
      doSomething();

现在假设你想要在cond1未达到时执行doSomethingElse()(新功能)。那么:

if (cond1)
   if (cond2) 
      doSomething();
else
   doSomethingElse();

很明显这是错误的,因为else与内部的if相关联。


编辑:由于这引起了一些关注,我将澄清我的观点。我回答的问题是:

使用第一个版本有什么好处?

我已经描述了一些好处,但在我看来,“总是”规则并不总是适用。所以我不完全支持

始终使用{ }块——即使只有一行//不行的原因是什么?

我并不是说总是使用{}块。如果它是一个足够简单的条件和行为,就不需要使用。如果你怀疑有人后来会更改你的代码以添加功能,请使用{}块。


5
确实如此,但如果在 j++ 之前加上 i++,那么这两个变量在使用时仍然在作用域内。 - Mike Seymour
26
这听起来很合理,但忽略了一个事实,即编辑器会对缩进进行处理,而不是你自己,它会以一种立即显示 i++; 不是循环部分的方式进行缩进。(在过去,这可能是一个合理的论点,我曾经遇到过这样的问题。大约20年前。从那以后就没有了。) - James Kanze
44
@James:虽然这不是“事实”,而是你的工作流程,也是许多人的工作流程,但并非所有人都是如此。我认为将C++源代码视为纯文本文件而不是WYSIWYG编辑器(vi/emacs/Visual Studio)的输出(该编辑器执行格式规则)不一定是错误的。因此,这个规则超出了你所需的编辑器无关性,但并未超出人们实际用于编辑C++的范围。因此,“防御性”的。 - Steve Jessop
31
你真的指望每个人都使用功能强大的集成开发环境吗?我最近写的一段C代码是在Nano中完成的。即使这样,在IDE中,我往往会关闭自动缩进功能,因为它常常妨碍我非线性的工作流程,试图基于不完整的信息纠正我的“错误”。IDE并不能很好地自动缩进每个程序员的自然编码风格。那些使用此类功能的程序员往往会将他们的编码风格与IDE融合在一起,如果你在多个IDE之间切换,则会有问题。 - Rushyo
10
这是一个愚蠢的错误,但即使是经验丰富的程序员也可能犯这种错误。- 就像我在我的答案中所说的那样,我不相信这一点。我认为这是一个完全虚构的情况,在现实中并不构成问题。 - Konrad Rudolph
显示剩余19条评论

332

如果您不使用大括号{},很容易在注释中意外改变控制流程。例如:

if (condition)
  do_something();
else
  do_something_else();

must_always_do_this();
如果你使用单行注释将do_something_else()注释掉,你会得到这样的结果:
if (condition)
  do_something();
else
  //do_something_else();

must_always_do_this();

代码可以编译,但并非总是调用 must_always_do_this()

我们在代码库中遇到过这个问题,有人在发布之前迅速禁用了某些功能。幸运的是,我们在代码审查中发现了这个问题。


3
哦,天哪!如果你注释掉 //do_something_else(); ,must_always_do_this();会被执行,这是一种规定行为。 - Mr.Anubis
1
@Supr 最初的写法是,他说如果你使用大括号,就很难破坏正确的流程,然后举了一个没有正确地用括号包围代码的例子来说明有多容易出错。 - SeanC
21
我就在前几天遇到了这个问题。if(debug) \n //print(info);。基本上把整个库都删掉了。 - Kevin
5
幸运的是,我们在代码审查中发现了它。哎呀!听起来太不对了。"幸运的是,我们在单元测试中发现了它。"会更好! - BЈовић
5
但如果这段代码在单元测试中呢?想想就让人脑袋发懵。(开个玩笑,这是一款遗留应用程序,没有单元测试。) - ta.speot.is
显示剩余2条评论

60
会引起不必要的混乱。
  • NO. This is a terrible idea. It makes the code harder to read and understand. Always use explicit comparisons.
  • Sort of. I agree that using the ternary operator can sometimes make code less readable, especially when it's nested. However, in simple cases, it can make code more concise and easier to follow. It's a tradeoff; use your judgment. (Note that the same argument could be made for any language feature, including conditionals and loops.)
  • 发送错误的信号:位运算也没有意义。基本规则是整数类型是int,除非有明显的原因使用其他类型。
  • 不要这样系统地做,这会产生误导,并且实际上并不能防止任何事情发生。在严格的面向对象的代码中,delete this;经常是最常见的情况(您不能将this设置为NULL),否则,大多数delete都在析构函数中,所以您无法访问指针。稍后无论如何,将其设置为NULL并不能解决浮动的任何其他指针问题。将指针系统地设置为NULL会给人一种虚假的安全感,并且实际上并不能为您带来任何好处。
  • 查看任何典型参考资料中的代码。例如,Stroustrup违反了您给出的每条规则,除了第一条。

    我建议您找另一个讲师。 找一个真正知道自己在说什么的人。


    14
    数字4可能看起来很丑,但它有其用途。它试图防止if(ptr = NULL)的出现。我不认为我曾经使用过delete this,它比我所看到的更常见吗?我认为将指针设置为NULL以后并没有什么坏处,但因人而异。也许只是我的感觉,但他的大多数建议似乎并不那么糟糕。 - Firedragon
    17
    @Firedragon:大多数编译器会对if (ptr = NULL)发出警告,除非你将其写成if ((ptr = NULL))。我同意James Kanze的观点,将NULL放在首位很难看,因此我坚决反对这种写法。 - Leo
    35
    我不太同意你在这里所说的大多数内容,但我欣赏并尊重你得出这些观点的论据。对于有经验的C和C++程序员来说,使用无符号位运算符并不意味着使用了 signals(信号)。对我来说,使用 bit operators 意味着使用了位运算符。使用 unsigned 则表示程序员有一个愿望,即该变量应该只表示正数。将其与有符号数混合使用通常会引起编译器警告,这可能是讲师的本意。 - Component 10
    19
    熟练掌握C和C++的程序员是否使用无符号信号位运算符?size_t类型呢? - ta.speot.is
    17
    考虑它的目的。你正在比较一个经验丰富的程序员编写的代码和教学示例。这些规则由讲师提供,因为这是他看到学生犯的错误类型。随着经验的积累,学生可以放松或忽略这些绝对规则。 - Joshua Shane Liberman
    显示剩余19条评论

    49
    所有其他答案都支持你的讲师规则3。
    让我说,我同意你:规则是多余的,我不建议使用。确实,从理论上讲,如果您始终添加花括号,它可以防止错误。另一方面,与其他答案暗示的相反,我在现实生活中从未遇到过这个问题:一旦需要添加花括号,我从未忘记添加花括号。如果使用适当的缩进,一旦有多个语句缩进,需要再次添加花括号就变得显而易见了。 组件10的答案实际上强调了唯一可能导致错误的情况。但另一方面,通过正则表达式替换代码总是需要极大的小心。
    现在让我们看看奖牌的另一面:总是使用花括号有什么缺点吗?其他答案简单地忽略了这一点。但是确实存在一个缺点:它占用了很多垂直屏幕空间,这反过来可能会使您的代码难以阅读,因为这意味着您必须比必要的更多地滚动。

    考虑一个函数,在开头有很多警卫条款(是的,在其他语言中,以下代码是糟糕的C++代码,但这种情况相当普遍):

    void some_method(obj* a, obj* b)
    {
        if (a == nullptr)
        {
            throw null_ptr_error("a");
        }
        if (b == nullptr)
        {
            throw null_ptr_error("b");
        }
        if (a == b)
        {
            throw logic_error("Cannot do method on identical objects");
        }
        if (not a->precondition_met())
        {
            throw logic_error("Precondition for a not met");
        }
    
        a->do_something_with(b);
    }
    

    这是糟糕的代码,我强烈认为以下代码更易读:
    void some_method(obj* a, obj* b)
    {
        if (a == nullptr)
            throw null_ptr_error("a");
        if (b == nullptr)
            throw null_ptr_error("b");
        if (a == b)
            throw logic_error("Cannot do method on identical objects");
        if (not a->precondition_met())
            throw logic_error("Precondition for a not met");
    
        a->do_something_with(b);
    }
    

    同样地,简短的嵌套循环受益于省略花括号:
    matrix operator +(matrix const& a, matrix const& b) {
        matrix c(a.w(), a.h());
    
        for (auto i = 0; i < a.w(); ++i)
            for (auto j = 0; j < a.h(); ++j)
                c(i, j) = a(i, j) + b(i, j);
    
        return c;
    }
    

    与之比较:

    matrix operator +(matrix const& a, matrix const& b) {
        matrix c(a.w(), a.h());
    
        for (auto i = 0; i < a.w(); ++i)
        {
            for (auto j = 0; j < a.h(); ++j)
            {
                c(i, j) = a(i, j) + b(i, j);
            }
        }
    
        return c;
    }
    

    第一段代码简洁;第二段代码臃肿。
    是的,可以通过将左括号放在上一行来在某种程度上减轻这种情况。因此:如果您坚持使用花括号,请至少将左括号放在上一行。
    简而言之:不要编写占用屏幕空间的不必要的代码。

    自从我最初写下这个答案以来,我大多数时间都接受了普遍的代码风格,并使用花括号,除非我可以将整个单语句放在前一行。我仍然认为不使用冗余的花括号通常更易读,并且我仍然从未遇到由此引起的错误。


    29
    如果你不相信写代码时无谓地占用屏幕空间,那么你就没有理由把左大括号放在它自己的一行上。我可能现在得躲避GNU组织的神圣复仇,但说真的——要么你希望你的代码垂直紧凑,要么你不希望。如果你希望,就不要做那些只是为了使代码变得不那么垂直紧凑的事情。但正如你所说,即使你已经解决了这个问题,你仍然也要删除多余的大括号。或者只需将if (a == nullptr) { throw null_ptr_error("a"); }写成一行。 - Steve Jessop
    6
    实际上,就像你所说的那样,我把左大括号放在上一行。我在这里使用了另一种风格,是为了更明显地突出差异有多大。 - Konrad Rudolph
    9
    我完全同意你的第一个例子没有用括号更容易阅读。在第二个例子中,我个人的编码风格是在外部for循环上使用括号,而不在内部使用。我不同意@SteveJessop关于代码垂直紧凑必须要么极端化为一种,要么另一种的观点。对于单行代码,我会省略额外的括号以减少垂直空间,但我会将我的开放性括号放在新行上,因为我发现当括号排列整齐时更容易看到范围。目标是可读性,有时这意味着使用更多的垂直空间,而其他时候则意味着使用较少。 - Travesty3
    23
    "我从未在现实生活中遇到过这个问题。": 你真幸运。像这样的事情不仅会让你受伤,而且会导致你90%的三度烧伤(而这只是晚上管理层突然要求解决问题时的情况)。 - Richard
    9
    @Richard 我完全不相信这一点。正如我在聊天中解释的那样,即使这种错误真的发生了(我认为这很不可能),只要看一下堆栈跟踪,修复它就是微不足道的,因为通过查看代码,显然知道错误出现在哪里。你夸大其词的说法完全没有根据。 - Konrad Rudolph
    显示剩余16条评论

    41

    我所工作的代码库中,有些人过分厌恶大括号,这让后来的人来维护这个代码库时会感到困难。

    我遇到的最常见的问题是这样的:

    if (really incredibly stupidly massively long statement that exceeds the width of the editor) do_foo;
        this_looks_like_a_then-statement_but_isn't;
    

    如果我不小心添加一个 then 语句,代码可能会变成这样:

    if (really incredibly stupidly massively long statement that exceeds the width of the editor) do_foo;
    {
        this_looks_like_a_then-statement_but_isn't;
        i_want_this_to_be_a_then-statement_but_it's_not;
    }
    

    考虑到添加大括号只需要约1秒钟,并且可以节省您至少几分钟的混淆调试时间,为什么您不选择减少歧义的选项?对我来说,这似乎是虚假经济。


    19
    这个例子中的问题不在于花括号,而是缩进不当和行过长,你同意吗? - Honza Brabec
    8
    是的,但是遵循设计/编码指南的前提是假定人们也遵循其他指南(如不要有过长的代码行),这看起来有点冒险。如果一开始就加上了大括号,在这种情况下就不可能出现错误的if块。 - jam
    17
    加上大括号(变成 if(really long...editor){ do_foo;})会如何帮助你避免这种情况?看起来问题仍然是一样的。个人而言,我倾向于在不必要的情况下避免使用大括号,但这与编写大括号所需的时间无关,而是由于代码中增加了两行降低了可读性。 - Grizzly
    1
    很好的观点 - 我一直假设强制使用花括号也会导致它们被放在一个合理的位置,但当然,有些人决心让事情变得困难,并且像你的例子中一样将它们放在线内。不过我想大多数人不会这么做。 - jam
    2
    当我接触一个文件时,我做的第一件和最后一件事情就是点击自动格式化按钮。它可以消除大部分这些问题。 - Jonathan Allen
    显示剩余2条评论

    20

    我的建议:

    使用缩进

    显然。

    永远不要依赖运算符优先级-始终使用括号

    我不会使用“永远”和“始终”这些词,但通常情况下,我认为这个规则很有用。在某些语言(如Lisp、Smalltalk)中,这不是一个问题。

    即使只有一行代码也要使用{ }代码块

    我从来没有这样做过,也从来没有遇到过任何问题,但我可以看出这对学生可能很有好处,特别是如果他们之前学过Python。

    比较时,将const对象放在左侧

    尤达条件?不,谢谢。它会影响可读性。只需在编译代码时使用最高警告级别。

    对于值大于等于0的变量,请使用无符号类型

    好的。有趣的是,我听说Stroustrup并不赞同。

    在删除后将指针设置为NULL-双重删除保护

    不好的建议!永远不要有一个指向已删除或不存在对象的指针。


    5
    仅就最后一点而言,我给它点赞。一个裸指针本来就不应该拥有内存。 - Konrad Rudolph
    2
    关于使用unsigned:不仅是Stroustrup,而且包括K&R(在C中),Herb Sutter和(我认为)Scott Meyers。事实上,我从未听说过任何真正理解C ++规则的人主张使用unsigned。 - James Kanze
    2
    实际上,在同一场合(2008年的波士顿会议)我听到了Stroustrup的意见,Herb Sutter也在场,并当场不同意Bjarne的看法。 - Nemanja Trifunovic
    3
    为了完善“unsigned存在问题”的说法,其中一个问题是当C++比较大小相似的有符号和无符号类型时,在进行比较之前会将其转换为无符号类型,这导致数值发生变化。转换为有符号类型并不一定更好;实际上应该将比较看作是“仿佛”将两个值转换为能够表示任一类型中所有值的较大类型进行比较。 - James Kanze
    2
    @SteveJessop 我认为你必须将其放在返回“unsigned”的函数的上下文中考虑。我相信他不会对exp(double)返回一个超过MAX_INT的值有任何问题:-)。但再次,真正的问题是隐式转换。 int i = exp(1e6);是完全有效的C ++。 Stroustrup实际上曾经提议废弃有损隐式转换,但委员会并不感兴趣。(一个有趣的问题:unsigned->int是否被认为是有损的。我认为unsigned->intint->unsigned都是有损的。这将大大使unsigned可以接受。 - James Kanze
    显示剩余5条评论

    19

    它更加直观易懂,使意图清晰。

    并确保在新用户无意中遗漏添加新的代码语句时,代码不会出现错误,因为使用它可以保证代码的完整性。


    表达意图清晰 +1,这可能是最简洁和准确的原因。 - Qix - MONICA WAS MISTREATED

    14

    除了之前的回答中提出的非常明智的建议,我想补充一个例子,说明这个问题为什么很关键。在重构一些代码时,我遇到一个例子,需要将一个非常大的代码库从一个 API 切换到另一个 API。第一个 API 有一个设置公司 ID 的调用,如下所示:

    setCompIds( const std::string& compId, const std::string& compSubId );
    

    相比之下,替换需要两个调用:

    setCompId( const std::string& compId );
    setCompSubId( const std::string& compSubId );
    

    我使用正则表达式进行更改,这非常成功。我们还通过astyle对代码进行了修改,使其更易读。但在审核过程中,我发现在某些条件下会发生以下更改:

    if ( condition )
       setCompIds( compId, compSubId );
    

    翻译为:

    变成这样:

    if ( condition )
       setCompId( compId );
    setCompSubId( compSubId );
    

    这显然不是所需的结果。我不得不回到一开始,将替换视为完全在一个块内,并手动修改任何看起来有问题的地方(至少它不会是不正确的)。

    我注意到astyle现在有一个选项--add-brackets,可以在没有括号的情况下添加括号,如果你发现自己处于与我相同的位置,我强烈推荐使用此选项。


    6
    我曾经看到一些文档,其中出现了一个精妙的新词“Microsoftligent”。是的,使用全局搜索和替换可能会导致重大错误。这意味着必须要聪明地使用全局搜索和替换,而不是“Microsoftligently”(笨拙地)。 - Pete Becker
    4
    虽然这不是我该做的任务,但如果你要对源代码进行文本替换,那么你应该按照在此语言中广泛使用的文本替换规则(宏)来执行。例如,你不应该编写一个带有换行符的宏#define FOO() func1(); \ func2();,同样适用于搜索和替换。话虽如此,我曾见过“始终使用大括号”被提升为一种风格规则,正是因为它可以让你避免将所有多语句宏包装在do...while(0)中。但我持不同意见。 - Steve Jessop
    3
    顺便说一下,这里的“well-established”与日本葛的“well-established”意思相似:我并不是在说我们应该费尽心思地使用宏和文本替换,但我的意思是,当我们这样做时,我们应该以实用为出发点,而不是仅在成功强制执行整个代码库的特定样式规则时才起作用的方式。 - Steve Jessop
    1
    @SteveJessop 一个人也可以为大括号和腰带辩护。如果你必须使用这样的宏(在C++和inline之前我们确实需要),那么你应该尽可能让它们像函数一样工作,必要时使用do { ... } while(0)技巧(以及大量额外的括号)。但如果这是公司的风格,仍然无法阻止你到处使用大括号。(值得一提的是:我曾在各种不同的公司工作过,涵盖了所有在此讨论的风格。我从未发现任何一个严重的问题。) - James Kanze
    2
    我认为,你接触的样式越多,你就会更加仔细地阅读和编辑代码。因此,即使你有偏好,喜欢易于阅读的样式,你仍然可以成功地阅读其他样式。我曾在一家公司工作,不同的团队使用不同的“内部样式”编写不同的组件,正确的解决方案是在午餐时间抱怨一下,而不是试图创建全局样式 :-) - Steve Jessop
    显示剩余3条评论

    8

    我在所有地方都使用 {} ,除了一些明显的情况。单行代码就是其中之一:

    if(condition) return; // OK
    
    if(condition) // 
       return;    // and this is not a one-liner 
    

    当你在返回语句之前添加某些方法时,这可能会对你造成影响。缩进表示当条件满足时,返回语句正在执行,但它始终会返回。

    C#中另一个例子是使用语句。

    using (D d = new D())  // OK
    using (C c = new C(d))
    {
        c.UseLimitedResource();
    }
    

    这相当于

    using (D d = new D())
    {
        using (C c = new C(d))
        {
            c.UseLimitedResource();
        }
    }
    

    1
    只需在“using”语句中使用逗号,就不必 :) - Ry-
    1
    @minitech 在这里是不起作用的 - 只有在类型相等时才能使用逗号,而不能用于不相等的类型。Lukas的做法是规范的方式,即使IDE也以不同的方式格式化此代码(请注意第二个using缺乏自动缩进)。 - Konrad Rudolph

    8

    The most pertinent example I can think of:

    if(someCondition)
       if(someOtherCondition)
          DoSomething();
    else
       DoSomethingElse();
    

    哪个ifelse配对?缩进暗示外部的if会得到else,但编译器实际上不���这样看待它的;内部if将得到else,而外部if不会得到。你必须知道这一点(或在调试模式下看到它以这种方式运行),才能通过检查弄清楚为什么该代码可能未达预期。如果你了解Python,则会更加困惑;在这种情况下,你会知道缩进定义了代码块,因此你会根据缩进来评估代码。然而,在C#中,空格并不起任何作用。
    话虽如此,我并不完全同意这个“总是使用括号”的规则。它使代码变得非常嘈杂,减少了快速阅读的能力。如果语句是:
    if(someCondition)
       DoSomething();
    

    如果语句 "总是使用括号" 被写成像这样。它听起来像是 "总是用括号包围数学运算"。这将把非常简单的语句 a * b + c / d 变成 ((a * b) + (c / d)),引入了错失闭合括号(许多码农的噩梦)的可能性,而这样做的目的是什么呢?操作顺序是众所周知且被严格执行的,因此括号是多余的。只有在需要强制执行与通常应用的不同操作顺序时,才会使用括号:例如 a * (b+c) / d。块级花括号也是如此;在需要定义与默认情况不同且不 "显而易见"(主观的,但通常很常识性)的内容时,请使用它们。


    3
    @AlexBrown…这正是我的观点。原帖中所述的规则是“即使只有一行也始终使用括号”,但我因为我提到的理由而不同意这个规则。对于第一个代码示例,使用括号确实会有所帮助,因为代码不会按照指定的缩进方式运行;您需要使用括号将else与第一个if配对,而不是第二个。请取消您的反对票。 - KeithS

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