断言是邪恶的吗?[关闭]


199

Go语言的创造者写的

Go不提供断言。它们无疑是很方便的,但是我们的经验是程序员将它们用作拐杖,以避免考虑适当的错误处理和报告。正确的错误处理意味着服务器在发生非严重错误后仍可继续运行,而不会崩溃。正确的错误报告意味着错误是直接的并指向重点,从而使程序员不必解释大型崩溃跟踪。当程序员看到的错误不熟悉代码时,精确的错误尤为重要。

您对此有何看法?


4
切线:Go是一种非常规的语言。这不一定是一件坏事。但是,这确实意味着您应该以更大的食盐采纳它的意见。这也意味着,如果您不同意,您将在使用该语言时咬牙切齿。作为Go在现实中如何坚持其观点的证据,请考虑您需要借助反思的魔力来确定两个集合是否相等。
allyourcode

@allyourcode如果您指的是reflect.DeepEqual,那么您当然不需要它。这很方便,但是却以性能为代价(单元测试是一个很好的用例)。否则,您可以进行任何适合于“集合”的相等性检查,而不会带来太多麻烦。
伊戈尔·杜宾斯基

1
不,那不是我要说的。没有反射就没有sl​​ice1 == slice2这样的东西。所有其他语言都具有与此超级基本操作等效的功能。Go没有的唯一原因是偏见。
allyourcode '16

您可以使用forGo中的循环比较两个切片而无需反射(就像C一样)。这是非常好的,有通用切片操作,虽然当指针和结构都参与的比较变得复杂。
kbolino '18

Answers:


321

不,assert只要您按预期使用它,就没有错。

也就是说,这是为了在调试过程中捕获“不可能发生”的情况,而不是正常的错误处理。

  • 断言:程序逻辑本身失败。
  • 错误处理:不是由于程序错误引起的错误输入或系统状态。

109

不,goto也不assert邪恶是。但是两者都可能被滥用。

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


如何goto明智地使用?
ar2015 2016年

1
@ ar2015找到一些goto纯粹出于宗教原因goto而建议避免的荒唐人为模式,然后仅使用而不是混淆您的工作。换句话说:如果您可以证明自己确实需要goto,并且唯一的选择是制定无意义的脚手架,最终完成相同的工作,而无需更改Goto Police ...,然后使用即可goto。当然,这的前提是“如果可以证明确实需要goto”。人们常常不这样做。那仍然并不意味着它本质上是一件坏事。
underscore_d

2
goto在Linux内核中用于代码清理
Malat

61

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

断言应该用作调试辅助,别无其他。当您尝试使用它们而不是错误处理时,是“邪恶的” 。

断言可以帮助程序员(程序员)发现并解决可能不存在的问题,并验证您的假设是否成立。

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


40

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

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

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


31

作为附加信息,go提供了一个内置功能panic。可以代替assert。例如

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

panic将打印堆栈跟踪,因此以某种方式具有目的assert



13

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

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

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

您仍然必须对生产代码进行单元测试,但这是确保代码正确的另一种补充方法。单元测试可确保您的例程符合其接口,而断言是一种更细粒度的方法,可确保您的实现完全按照您的预期进行。


8

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

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

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

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


5

我承认使用断言时没有考虑适当的错误报告。但是,这并不能说明正确使用它们会很有帮助。

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


5

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

但是,在某种情况下闯入调试器并获取所有文件/行信息非常有用,确切的表达式和确切的值也很有用。

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

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

我们已经开发了一组断言宏,它们会引发异常,并在调试版本和发行版中均会发生。例如,THROW_UNLESS_EQ(a, 20);将对同时具有文件,行和a的实际值等的what()消息引发异常。只有宏才有力量。调试器可以配置为在特定异常类型的“抛出”时中断。


4
参数中使用的统计信息的90%为假。
若昂里斯本

5

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

断言基本上与未检查的异常会做相同的事情,唯一的例外是断言(通常)不应保留给最终产品。

如果您在调试和构建系统时为自己构建了一个安全网,那么为什么要为您的客户,支持服务台或将要使用当前正在构建的软件的任何人拒绝该安全网。仅在断言和例外情况下使用异常。通过创建适当的异常层次结构,您将可以非常快速地彼此区分。除这次外,assert保留在原位,并且可以在失败的情况下提供有价值的信息,否则可能会丢失。

因此,我通过完全删除断言并强迫程序员使用异常处理情况来完全理解Go的创建者。对此有一个简单的解释,例外只是工作的一种更好的机制,为什么坚持使用过时的断言?


Go没有例外。使用断言而不是异常的通常原因是因为出于性能原因您希望在部署中将其删除。断言不是过时的。听起来很刺耳,我很抱歉,但是这个答案对原始问题没有多大帮助,也没有正确。
尼尔·弗里德曼


3

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

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

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

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);
}

这给了我两层检查。在运行时确定数据的任何事物都会得到异常或立即的错误处理。但是,Class::f使用该assert语句进行额外的检查意味着,如果某些内部代码曾经调用过Class::f,我仍然可以进行健全性检查。我的内部代码可能没有传递有效的参数(因为我可能是根据value一系列复杂的函数计算得出的),因此我喜欢在set函数中声明该值,以证明无论谁调用该函数,该值都value不得大于或等于end

这似乎与我在某些地方阅读的内容相吻合,在一个运行良好的程序中,断言应该是不可能违反的,而对于仍然可能出现的异常情况和错误情况,应该有例外。因为理论上我正在验证所有输入,所以不可能触发我的断言。如果是这样,我的程序是错误的。


2

如果您在说的断言意味着该程序呕吐然后存在,则断言可能非常糟糕。这并不是说它们总是使用错误的东西,它们是很容易被滥用的结构。他们还有许多更好的选择。这样的事情是被称为邪恶的良好候选者。

例如,第三方模块(或实际上的任何模块)几乎永远都不应退出调用程序。这并不能使调用程序的程序员可以控制程序在那时应该承担的风险。在许多情况下,数据非常重要,因此即使保存损坏的数据也比丢失数据更好。断言可能会迫使您丢失数据。

断言的一些替代方法:

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

一些参考:

即使是主张主张的人也认为,它们仅应在开发中使用,而不能在生产中使用:

此人说,当模块具有引发异常后仍然存在的可能损坏的数据时,应该使用asserts:http : //www.advogato.org/article/949.html。这当然是合理的一点,但是,外部模块绝对不能规定损坏的数据对调用程序有多重要(通过退出“ for”它们)。解决此问题的正确方法是抛出一个异常,该异常使程序现在可能处于不一致状态。而且由于好的程序主要由模块组成(在主要的可执行文件中带有少量粘合代码),所以断言几乎总是错误的事情。


1

assert 是非常有用的工具,可以在出现问题的最初迹象时暂停程序,以防在发生意外错误时为您节省大量回溯时间。

另一方面,很容易滥用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可以滥用。我一直都在做。


1

assert 被滥用进行错误处理,因为它的键入较少。

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


1
还算不错:-)异常与否,断言与否,围棋迷还在谈论代码有多短。
Moshe Revah'1

1

看到那一刻,我感觉就像把作者踢在了脑海。

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

我不喜欢将异常也更容易地与生产代码融合。断言比起更容易注意到throw new Exception("Some generic msg or 'pretend i am an assert'");


1

我用这些答案来捍卫断言的问题是,没有人明确指出是什么使它与常规的致命错误不同,以及为什么一个断言不能成为异常的子集。现在,这样说,如果从未捕获到异常怎么办?这是否通过命名法将其确定?而且,您为什么要在语言中施加限制,使得可以引发/ nothing /无法处理的异常?


如果你看看我的回答。我的用途是区分要摆脱的“异常”(断言)和我保留的异常。我为什么要摆脱它们?因为如果没有他们的努力,那将是不完整的。例如,如果我处理了3种情况,而第四项是待办事项。我可以轻松地搜索断言以在代码中找到它们并知道其不完整,而后使用一个可能偶然(被另一个程序员捕获)的异常,或者很难告诉我它是否是异常或应该在代码中解决的逻辑检查。

在我看来,这是一个糟糕的主意,出于相同的原因,它与“密封”班级处于同一水平。您假设要保留的异常对于尚不了解的代码的使用是可以接受的。所有异常都通过相同的渠道,如果用户不想捕获它们,则可以选择不捕获。如果他这样做了,他也应该有能力。无论哪种方式,您都只是在进行假设或通过断言之类的概念来进行练习。
埃文·卡罗尔

我认为示例场景是最好的。这是一个简单的。int func(int i){if(i> = 0){console.write(“数字为正数{0}”,i); } else {assert(false); //懒于做底片ATM}返回i * 2; <-在没有断言的情况下我将如何做到这一点,并且异常确实更好吗?请记住,此代码将在发布之前实现。

当然,例外情况更好,可以说我接受用户输入并func()以负数致电。现在,突然有了您的主张,您就把地毯从我下面拉了下来,没有给我恢复的机会,而是有礼貌地告诉我我所要求的无法完成。指责该程序行为不当并予以引用是没有错的:问题是您在模糊执行法律和判处重罪的行为。
埃文·卡洛尔

就是这一点,您应该说用户想做的事情不能完成。该应用程序应以错误msg终止,并且正在调试的任何人都将记住它应该做但不是的事情。您不希望它得到处理。您想终止合同,并提醒您您未处理该案件。它非常容易看到还剩下什么情况,因为您要做的就是在代码中搜索断言。处理错误将是错误的,因为代码一旦准备好投入生产就应该能够执行。允许程序员抓住它并做其他事情是错误的。

1

是的,断言是邪恶的。

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

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

通常,它们是从发行版本中编译出来的,这意味着当您运行实际发行的代码时,没有任何“测试”可用。鉴于在多线程情况下,最糟糕的问题通常只在发行代码中显示,所以可能很糟糕。

有时,它们对于其他情况下的破烂设计是不利的。也就是说,代码的设计允许用户以不应调用的方式调用它,而断言“阻止”此行为。修正设计!

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


0

邪恶程度不如一般适得其反。永久错误检查和调试之间是有区别的。断言使人们认为所有调试都应该是永久的,并且在大量使用时会引起大量的可读性问题。永久错误处理应该比需要的地方更好,并且由于assert会导致自身错误,因此这是一个非常可疑的做法。


5
assert非常适合在函数顶部声明前提条件,并且如果写得清楚,则可以作为函数文档的一部分。
彼得·科德斯

0

我从不使用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.
}

14
new永不返回nullptr,它抛出。
大卫·斯通

注意,您可以使用std :: nothrow
markand 2015年
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.