【问题标题】:Is assert evil? [closed]断言是邪恶的吗? [关闭]
【发布时间】:2010-12-23 16:22:28
【问题描述】:

Go 语言创造者write

Go 不提供断言。 不可否认,它们很方便,但我们的经验是程序员将它们用作拐杖,以避免考虑正确的错误处理和报告。正确的错误处理意味着服务器在非致命错误后继续运行而不是崩溃。正确的错误报告意味着错误是直接的、切中要害的,从而使程序员免于解释大量的崩溃跟踪。当看到错误的程序员不熟悉代码时,准确的错误尤为重要。

您对此有何看法?

【问题讨论】:

  • 切题:Go 是一种非同寻常的固执己见的语言。这不一定是坏事。但是,这确实意味着您应该对它的意见持保留态度。这也意味着,如果您不同意,您将在使用该语言时咬牙切齿。作为 Go 如何在现实中坚持其观点的证据,请考虑您需要借助反射的魔力来确定两个集合是否相等。
  • @allyourcode 如果你指的是reflect.DeepEqual,你当然不需要需要它。这很方便,但以性能为代价(单元测试是一个很好的用例)。否则,您可以毫不费力地实施适合您的“集合”的任何相等性检查。
  • 不,我说的不是这个。没有反射就没有 slice1 == slice2 这样的东西。所有其他语言都具有与此超级基本操作等效的功能。 Go 没有的唯一原因是偏见。
  • 您可以在 Go 中使用 for 循环来比较两个切片而无需反射(就像 C 一样)。拥有通用切片操作非常好,尽管当涉及指针和结构时比较会变得复杂。

标签: c++ c error-handling go assert


【解决方案1】:

简短回答:不,我相信断言很有用

【讨论】:

    【解决方案2】:

    不,gotoassert 都不是邪恶的。但两者都可能被滥用。

    Assert 用于完整性检查。如果不正确,应该杀死程序的事情。不用于验证或替代错误处理。

    【讨论】:

    • 如何明智地使用goto
    • @ar2015 找到一些人出于纯粹的宗教原因建议避免使用goto 的荒谬设计模式之一,然后只使用goto 而不是混淆你正在做的事情。换句话说:如果你能证明你真的需要goto,那么唯一的选择是制定一堆毫无意义的脚手架,最终做同样的事情,但不改变Goto Police……那么只需使用goto。当然,前提是“如果你能证明你真的需要goto”。通常,人们不会。但这并不意味着它本质上是一件坏事。
    • goto在linux内核中用于代码清理
    • 到目前为止,我发现 goto 绝对优越的唯一地方是创建一个带有用户确认的 abort-retry-ignore 逻辑。在这种情况下,它使代码更容易阅读和编写(这只是一种意见)。另一种不太优越的情况是将外部 switch 语句拆分为另一个 switch 语句,例如 C# 语言,不允许失败。
    【解决方案3】:

    我很喜欢使用断言。当我第一次构建应用程序时(也许对于一个新域),我发现它非常有用。我没有进行非常花哨的错误检查(我会考虑过早优化),而是快速编码并添加了很多断言。在我对事情的工作原理有了更多了解后,我重写并删除了一些断言并更改它们以更好地处理错误。

    由于断言,我在编码/调试程序上花费的时间减少了很多。

    我还注意到断言可以帮助我思考许多可能会破坏我的程序的事情。

    【讨论】:

      【解决方案4】:

      不,assert 没有任何问题,只要您按预期使用即可。

      也就是说,它应该用于在调试期间捕获“不可能发生”的情况,而不是正常的错误处理。

      • 断言:程序逻辑本身出现故障。
      • 错误处理:错误的输入或系统状态不是由程序中的错误引起的。

      【讨论】:

        【解决方案5】:

        它们应该用于检测程序中的错误。不错的用户输入。

        如果使用得当,它们不是邪恶的。

        【讨论】:

          【解决方案6】:

          按照这个逻辑,断点也是邪恶的。

          断言应该用作调试辅助,仅此而已。 “邪恶”是指您尝试使用它们而不是错误处理。

          断言可以帮助您(程序员)检测和修复不应该存在的问题,并验证您的假设是否正确。

          它们与错误处理无关,但不幸的是,一些程序员滥用它们,然后将它们声明为“邪恶”。

          【讨论】:

            【解决方案7】:

            断言并不邪恶,但它们很容易被滥用。我确实同意“断言经常被用作拐杖以避免考虑正确的错误处理和报告”的说法。我经常看到这种情况。

            就个人而言,我喜欢使用断言,因为它们记录了我在编写代码时可能做出的假设。如果在维护代码时打破了这些假设,则可以在测试期间检测到问题。但是,在进行生产构建(即使用#ifdefs)时,我确实会从我的代码中删除每个断言。通过剥离生产构建中的断言,我消除了任何人滥用它们作为拐杖的风险。

            断言还有另一个问题。断言仅在运行时检查。但通常情况下,您想要执行的检查可能已在编译时执行。最好在编译时检测问题。对于 C++ 程序员,boost 提供了 BOOST_STATIC_ASSERT 允许您执行此操作。对于 C 程序员,本文 (link text) 描述了一种可用于在编译时执行断言的技术。

            总而言之,我遵循的经验法则是:不要在生产构建中使用断言,并且如果可能,仅将断言用于无法在编译时验证的事物(即必须在运行时检查) )。

            【讨论】:

              【解决方案8】:

              与其说是邪恶,不如说是适得其反。永久错误检查和调试是分开的。 Assert 使人们认为所有调试都应该是永久性的,并且在大量使用时会导致大量的可读性问题。永久错误处理应该比需要的更好,因为 assert 会导致自己的错误,这是一个非常值得怀疑的做法。

              【讨论】:

              • assert 适合在函数顶部声明前置条件,如果写得清楚,作为函数文档的一部分。
              【解决方案9】:

              作为附加信息,go 提供了一个内置函数panic。这可以用来代替assert。例如

              if x < 0 {
                  panic("x is less than 0");
              }
              

              panic 将打印堆栈跟踪,因此在某种程度上它具有assert 的目的。

              【讨论】:

                【解决方案10】:

                另一方面,很容易滥用assert

                int quotient(int a, int b){
                    assert(b != 0);
                    return a / b;
                }
                

                正确的版本应该是这样的:

                bool quotient(int a, int b, int &result){
                    if(b == 0)
                        return false;
                
                    result = a / b;
                    return true;
                }
                

                所以...从长远来看...从大局来看...我必须同意assert 可以被滥用。我一直这样做。

                【讨论】:

                  【解决方案11】:

                  我承认在没有考虑正确的错误报告时使用了断言。但是,这并不能说明它们在正确使用时非常有用。

                  如果您想遵循“早期崩溃”原则,它们特别有用。例如,假设您正在实现一个引用计数机制。在代码中的某些位置,您知道引用计数应该为零或一。并且还假设如果引用计数错误,程序不会立即崩溃,但在下一个消息循环期间,很难找出出错的原因。断言有助于检测更接近其起源的错误。

                  【讨论】:

                    【解决方案12】:

                    是的,断言是邪恶的。

                    它们通常用于应该使用适当错误处理的地方。从一开始就习惯于编写适当的生产质量错误处理!

                    通常它们会妨碍编写单元测试(除非您编写与测试工具交互的自定义​​断言)。这通常是因为它们被用于应该使用正确错误处理的地方。

                    大多数情况下,它们是在发布版本中编译出来的,这意味着当您运行实际发布的代码时,它们的任何“测试”都不可用;考虑到在多线程情况下,最糟糕的问题通常只出现在发布代码中,这可能很糟糕。

                    有时它们是其他损坏设计的拐杖;即代码的设计允许用户以不应该调用的方式调用它,并且断言“阻止”了这种情况。修复设计!

                    我早在 2005 年就在我的博客上写了更多关于此的内容:http://www.lenholgate.com/blog/2005/09/assert-is-evil.html

                    【讨论】:

                      【解决方案13】:

                      我非常不喜欢断言。不过,我不会说他们是邪恶的。

                      基本上,断言将与未经检查的异常执行相同的操作,唯一的例外是断言(通常)不应保留用于最终产品。

                      如果您在调试和构建系统时为自己构建安全网,为什么您会拒绝为您的客户、支持服务台或任何将使用您当前构建的软件的人提供此安全网。专门为断言和异常情况使用异常。通过创建适当的异常层次结构,您将能够非常快速地辨别一个与另一个。除了这一次,断言仍然存在,并且可以在失败的情况下提供有价值的信息,否则会丢失。

                      所以我完全理解 Go 的创建者,他们完全删除了断言并强制程序员使用异常来处理这种情况。对此有一个简单的解释,异常只是一种更好的工作机制,为什么要坚持古老的断言?

                      【讨论】:

                      • Go 没有异常。使用断言而不是异常的通常原因是,出于性能原因,您希望在部署中省略它。断言并不陈旧。很抱歉听起来很苛刻,但这个答案对原始问题没有多大帮助,也不正确。
                      【解决方案14】:

                      我更喜欢避免在调试和发布中执行不同操作的代码。

                      在某个条件下中断调试器并获得所有文件/行信息是有用的,但准确的表达式和准确的值也是如此。

                      拥有一个“仅在调试时评估条件”的断言可能是一种性能优化,因此,仅在 0.0001% 的程序中有用——人们知道他们在做什么。在所有其他情况下,这是有害的,因为表达式实际上可能会改变程序的状态:

                      assert(2 == ShroedingersCat.GetNumEars()); 会使程序在调试和发布时做不同的事情。

                      我们开发了一组断言宏,它们会抛出异常,并在调试版和发布版中都执行此操作。例如,THROW_UNLESS_EQ(a, 20); 会抛出带有文件、行和 a 的 实际值 的 what() 消息的异常,依此类推。只有宏才能做到这一点。调试器可以配置为在特定异常类型的“抛出”处中断。

                      【讨论】:

                      • 90% 的参数中使用的统计数据是错误的。
                      【解决方案15】:

                      assert 被滥用于错误处理,因为它的输入更少。

                      因此,作为语言设计者,他们宁愿看到正确的错误处理可以通过更少的输入来完成。因为您的异常机制很冗长而排除断言不是解决方案。哦等等,Go 也没有例外。太糟糕了:)

                      【讨论】:

                      • 还不错 :-) 例外与否,断言与否,Go 粉丝仍在谈论代码有多短。
                      【解决方案16】:

                      这出现了很多,我认为使断言的辩护令人困惑的一个问题是它们通常基于参数检查。因此,请考虑一下何时可以使用断言的不同示例:

                      build-sorted-list-from-user-input(input)
                      
                          throw-exception-if-bad-input(input)
                      
                          ...
                      
                          //build list using algorithm that you expect to give a sorted list
                      
                          ...
                      
                          assert(is-sorted(list))
                      
                      end
                      

                      您对输入使用异常,因为您预计有时会收到错误的输入。您断言该列表已排序以帮助您找到算法中的错误,根据定义,这是您不期望的。断言仅在调试版本中,因此即使检查代价高昂,您也不介意在例程的每次调用时都进行此检查。

                      您仍然需要对您的生产代码进行单元测试,但这是确保您的代码正确的一种不同且互补的方式。单元测试可确保您的例程符合其接口,而断言是一种更细粒度的方式,可确保您的实现完全符合您的预期。

                      【讨论】:

                        【解决方案17】:

                        当我看到这个时,我想踢作者的头。

                        我一直在代码中使用断言,并最终在我编写更多代码时全部替换它们。当我没有编写所需的逻辑并希望在遇到代码时收到警报而不是编写将在项目接近完成时将被删除的异常时,我会使用它们。

                        异常也更容易融入生产代码,这是我不喜欢的。断言比throw new Exception("Some generic msg or 'pretend i am an assert'"); 更容易注意到

                        【讨论】:

                          【解决方案18】:

                          我对这些为断言辩护的答案的问题是没有人明确说明它与常规致命错误的不同之处,以及为什么断言不能是异常的子集时间>。现在,话虽如此,如果从未捕获到异常怎么办?这是否使它成为命名法的断言?而且,您为什么要对可以引发 /nothing/ 可以处理的异常的语言施加限制?

                          【讨论】:

                          • 如果你看看我的回答。我的用途是区分我想要摆脱的用于调试的“异常”(断言)与我保留的异常。我为什么要摆脱它们?因为没有他们的工作,它是不完整的。例如,如果我处理 3 个案例,第 4 个是待办事项。我可以轻松地搜索 assert 以在代码中找到它们并知道它的不完整,而不是使用可能意外捕获的异常(被另一个程序员)或难以判断我是否应该在代码中解决它的异常或逻辑检查。
                          • 在我看来,这是一个糟糕的主意,与“密封”类处于同一水平并且出于相同的原因。您假设您想要保留的异常对于您还不知道的代码的使用是可以接受的。所有异常都通过相同的渠道,如果用户不想捕捉它们,他可以选择不捕捉。如果他有,他也应该有这个能力。不管怎样,你只是在做假设,或者用断言之类的概念来推进你的实践。
                          • 我认为示例场景是最好的。这是一个简单的。 int func(int i) { if(i>=0) { console.write("数字为正{0}", i); } else { assert(false);//懒得做否定 ATM } return i*2;
                          • 当然例外更好,假设我接受用户输入并用负数调用func()。现在,突然你的断言从我身下拉开了地毯,没有给我恢复的机会,而不是礼貌地告诉我我所要求的不能完成。指责该程序行为不端并对其发出传票并没有错:问题是您正在模糊执法行为和对重罪犯进行判决。
                          • 这就是重点,你不应该说用户想做的事情不能做。该应用程序应该以错误消息终止,并且正在调试的任何人都会记住它应该完成但不应该完成的事情。你不希望它被处理。你想要终止并被提醒你没有处理那个案子。并且非常容易查看剩下的情况,因为您需要做的就是在代码中搜索断言。处理错误是错误的,因为代码一旦准备好投入生产就应该能够做到这一点。允许程序员抓住它并做其他事情是错误的。
                          【解决方案19】:

                          如果您正在谈论的断言意味着程序呕吐然后存在,那么断言可能非常糟糕。这并不是说它们总是使用错误,它们是一种很容易被滥用的结构。他们也有很多更好的选择。这样的事情很适合被称为邪恶。

                          例如,第 3 方模块(或任何模块)几乎不应该退出调用程序。这并没有给调用程序员任何控制程序在那一刻应该承担的风险。在许多情况下,数据非常重要,即使保存损坏的数据也比丢失数据要好。断言可能会迫使您丢失数据。

                          断言的一些替代方法:

                          • 使用调试器,
                          • 控制台/数据库/其他日志记录
                          • 例外情况
                          • 其他类型的错误处理

                          一些参考资料:

                          即使是主张 assert 的人也认为它们应该只用于开发而不是用于生产:

                          这个人说,当模块有可能损坏的数据在抛出异常后仍然存在时,应该使用断言:http://www.advogato.org/article/949.html。这当然是一个合理的观点,但是,外部模块应该永远规定损坏的数据对调用程序有多重要(通过“为”它们退出)。处理这个问题的正确方法是抛出一个异常,明确表明程序现在可能处于不一致的状态。而且由于好的程序大多由模块组成(在主可执行文件中有一些胶水代码),因此断言几乎总是错误的做法。

                          【讨论】:

                            【解决方案20】:

                            我从不使用 assert(),示例通常是这样的:

                            int* ptr = new int[10];
                            assert(ptr);
                            

                            这很糟糕,我从不这样做,如果我的游戏分配了一堆怪物怎么办?我为什么要让游戏崩溃,而不是你应该优雅地处理错误,所以这样做:

                            CMonster* ptrMonsters = new CMonster[10];
                            if(ptrMonsters == NULL) // or u could just write if(!ptrMonsters)
                            {
                                // we failed allocating monsters. log the error e.g. "Failed spawning 10 monsters".
                            }
                            else
                            {
                                // initialize monsters.
                            }
                            

                            【讨论】:

                            • new 永远不会返回 nullptr,它会抛出。
                            • 请注意,您可以使用std::nothrow
                            【解决方案21】:

                            我最近开始在我的代码中添加一些断言,这就是我一直在做的事情:

                            我在心理上将我的代码分为边界代码和内部代码。边界代码是处理用户输入、读取文件和从网络获取数据的代码。在这段代码中,我在一个循环中请求输入,该循环仅在输入有效时退出(在交互式用户输入的情况下),或者在不可恢复的文件/网络损坏数据的情况下抛出异常。

                            内部代码就是一切。例如,在我的类中设置变量的函数可能被定义为

                            void Class::f (int value) {
                                assert (value < end);
                                member = value;
                            }
                            

                            从网络获取输入的函数可能会这样读:

                            void Class::g (InMessage & msg) {
                                int const value = msg.read_int();
                                if (value >= end)
                                    throw InvalidServerData();
                                f (value);
                            }
                            

                            这给了我两层检查。任何在运行时确定数据的东西总是会得到异常或立即错误处理。但是,使用assert 语句对Class::f 进行的额外检查意味着如果某些内部代码曾经调用Class::f,我仍然需要进行完整性检查。我的内部代码可能没有传递有效的参数(因为我可能已经从一些复杂的函数系列中计算出value),所以我喜欢在设置函数中使用断言来记录无论是谁调用该函数,value不得大于或等于end

                            这似乎符合我在几个地方所读到的内容,即在一个运行良好的程序中应该不可能违反断言,而异常应该是针对仍然可能发生的异常和错误情况。因为理论上我正在验证所有输入,所以我的断言应该不可能被触发。如果是,我的程序有问题。

                            【讨论】:

                              猜你喜欢
                              • 1970-01-01
                              • 2010-11-26
                              • 1970-01-01
                              • 2014-03-28
                              • 2011-03-21
                              • 1970-01-01
                              • 1970-01-01
                              • 2011-01-02
                              • 2010-10-02
                              相关资源
                              最近更新 更多