是断言邪恶?

时间:2009-12-06 04:04:19

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

Go语言创建者write

  

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

您对此有何看法?

21 个答案:

答案 0 :(得分:317)

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

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

  • 断言:程序逻辑本身失败。
  • 错误处理:错误的输入或系统状态,而不是程序中的错误。

答案 1 :(得分:106)

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

断言是为了进行健全性检查。如果不正确,应该杀死程序的东西。不用于验证或替代错误处理。

答案 2 :(得分:61)

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

断言应该用作调试辅助工具,而不是其他任何东西。 “邪恶”是指您尝试使用代替进行错误处理。

断言可以帮助您,程序员,检测和修复不存在的问题,并验证您的假设是否成立。

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

答案 3 :(得分:39)

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

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

我也注意到这些断言帮助我想到许多可能破坏我程序的事情。

答案 4 :(得分:30)

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

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

答案 5 :(得分:30)

作为附加信息,go提供了内置函数panic。这可以用来代替assert。 E.g。

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

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

答案 6 :(得分: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

您为输入使用了一个例外,因为您希望有时会收到错误的输入。您声明列表已排序,以帮助您找到算法中的错误,根据定义,您不会发现该错误。断言只在调试版本中,所以即使检查很昂贵,你也不介意在每次调用例程时都这样做。

您仍然需要对生产代码进行单元测试,但这是确保代码正确的另一种补充方式。单元测试确保您的例程符合其界面,而断言是一种更精细的方式,可确保您的实现完全符合您的预期。

答案 7 :(得分:8)

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

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

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

总之,我遵循的经验法则是:不要在生成版本中使用断言,如果可能的话,只对编译时无法验证的东西使用断言(即必须在运行时检查) )。

答案 8 :(得分:5)

我承认在没有考虑正确的错误报告的情况下使用了断言。但是,如果使用得当,它们并没有带走它们非常有用。

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

答案 9 :(得分:5)

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

在一个条件下打破调试器,并且所有文件/行信息都很有用,也是确切的表达式和确切的值。

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

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

我们开发了一组断言宏,它会引发异常,并在调试版和发行版中都这样做。例如,THROW_UNLESS_EQ(a, 20);会抛出一个异常,其中what()消息同时包含文件,行和实际值,依此类推。只有宏才能拥有这种力量。调试器可以配置为在特定异常类型的'throw'处中断。

答案 10 :(得分:5)

我不喜欢断言。我不会说他们是邪恶的。

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

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

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

答案 11 :(得分:3)

简短回答:不,我认为断言很有用

答案 12 :(得分: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),所以我喜欢在设置函数中使用断言来记录,无论谁调用函数,{ {1}}不得大于或等于value

这似乎适合我在一些地方阅读的内容,在一个功能完善的程序中断言应该是不可能的,而异常应该是仍然可能的例外和错误案例。因为理论上我验证了所有输入,所以我的断言不可能被触发。如果是,我的程序是错误的。

答案 13 :(得分:1)

是的,断言是邪恶的。

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

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

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

有时它们会成为设计破损的拐杖;即,代码的设计允许用户以不应该被调用的方式调用它,并且断言“阻止”这个。修复设计!

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

答案 14 :(得分: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可以被滥用。我一直这样做。

答案 15 :(得分:1)

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

因此,作为语言设计者,他们应该看到即使较少的打字也可以进行正确的错误处理。排除断言,因为您的异常机制是冗长的不是解决方案。哦等等,Go也没有例外。太糟糕了:))

答案 16 :(得分:1)

当我看到这一点时,我觉得自己喜欢踢作者。

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

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

更容易被注意到

答案 17 :(得分:1)

我对这些答案的保护断言的问题是没有人清楚地指明是什么使它与常规致命错误不同,以及为什么断言不能是异常的子集。现在,有了这个说,如果异常从未被捕获怎么办?这是否通过命名法使其成为断言?而且,为什么你会想要在语言中施加限制,以便可以提出/无需/可以处理异常?

答案 18 :(得分:1)

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

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

断言的一些替代方案:

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

一些参考文献:

即使是那些主张断言的人也认为他们只应该在开发中使用而不是在生产中:

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

答案 19 :(得分:0)

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

答案 20 :(得分: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.
}