断言是邪恶的吗?

Go语言创建者:

它们是不可否认的方便,但我们的经验是,程序员使用它们作为拐杖,以避免考虑正确的错误处理和报告。正确的错误处理意味着服务器在发生非致命错误后继续运行,而不是崩溃。正确的错误报告意味着错误是直接的和切中要点的,从而使程序员不必解释大量的崩溃跟踪。当看到错误的程序员并不熟悉代码时,精确的错误就显得尤为重要。

你对此有什么看法?

31879 次浏览

简短的回答:不,我相信断言可能有用

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

Assert用于完整性检查。如果它们不正确,就会杀死程序。不是为了验证或替换错误处理。

我很喜欢用断言。我发现当我第一次构建应用程序(可能是一个新的领域)时,它非常有用。我没有做非常花哨的错误检查(我认为是不成熟的优化),而是快速编码,并添加了大量断言。在我了解了更多事情的工作原理后,我重新编写并删除了一些断言并更改它们以更好地处理错误。

因为断言,我花更少的时间编码/调试程序。

我还注意到,断言可以帮助我想到许多可能破坏程序的东西。

不,assert没有任何问题,只要你按预期使用它。

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

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

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

如果使用正确,他们是邪恶。

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

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

断言的存在是为了帮助程序员检测和修复不可能存在的问题,并验证您的假设是否正确。

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

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

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

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

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

与其说是坏事,不如说是适得其反。永久性错误检查和调试是分开的。Assert让人们认为所有的调试都应该是永久性的,使用过多会导致大量的可读性问题。在需要时,永久错误处理应该比这种方法更好,由于assert会导致自己的错误,因此这是一种非常值得怀疑的实践。

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

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

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

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可以被滥用。我一直都这么做。

我承认使用了断言,但没有考虑适当的错误报告。然而,这并不意味着它们在正确使用时是非常有用的。

如果你想要遵循“尽早崩溃”原则,它们尤其有用。例如,假设您正在实现一个引用计数机制。在代码中的某些位置,你知道refcount应该是0或1。并且还假设如果refcount是错误的,程序不会立即崩溃,但在下一个消息循环中,在这一点上,将很难找到事情出错的原因。断言将有助于更接近错误的起源检测错误。

是的,断言是邪恶的。

它们通常用于应该使用正确错误处理的地方。从一开始就要习惯编写正确的产品质量错误处理程序!

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

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

有时,他们是一个拐杖,否则破碎的设计;也就是说,代码的设计允许用户以一种不应该被调用的方式调用它,而断言“阻止”了这一点。修改设计!

早在2005年,我就在我的博客上写过:http://www.lenholgate.com/blog/2005/09/assert-is-evil.html

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

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

如果您在调试和构建系统时为自己构建了一个安全网,那么为什么要拒绝为您的客户、您的支持帮助台或任何将使用您正在构建的软件的人提供这个安全网呢?在断言和异常情况下都使用异常。通过创建一个适当的异常层次结构,您将能够很快地从其他异常中辨别出来。除了这次,断言仍然存在,并且可以在失败的情况下提供有价值的信息,否则这些信息就会丢失。

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

我更倾向于避免在调试和发布中做不同事情的代码。

但是,在一个条件下中断调试器并获得所有文件/行信息,以及确切的表达式和确切的值是有用的。

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

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

我们开发了一组assert宏,可以在调试版和发布版中抛出异常。例如,THROW_UNLESS_EQ(a, 20);会抛出一个异常,其中what()消息同时包含文件、行和a的实际值,等等。只有宏才有这个功能。调试器可以配置为在特定异常类型的'throw'时中断。

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

因此,作为语言设计者,他们应该看到,适当的错误处理可以用更少的输入完成。因为异常机制太冗长而排除assert并不是解决方案。哦,等等,Go也没有异常。太糟糕了:)

这种情况经常出现,我认为让断言辩护令人困惑的一个问题是,它们通常基于参数检查。所以考虑一下这个不同的例子,当你使用断言时:

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

您对输入使用异常是因为您预计有时会得到错误的输入。您断言对列表进行排序是为了帮助您找到算法中的bug,根据定义,这是您所不期望的。断言只存在于调试版本中,因此即使检查的开销很大,您也不介意对例程的每次调用都进行检查。

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

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

我在代码中一直使用断言,并最终在编写更多代码时将它们全部替换掉。当我还没有编写所需的逻辑时,我就会使用它们,当我运行到代码时,我想要得到警告,而不是编写一个将在项目接近完成时删除的异常。

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

我对这些捍卫assert的答案的问题是没有人清楚地指定它与常规致命错误的不同之处,以及为什么断言不能是异常的子集。现在,如果异常从未被捕获,该怎么办?从命名法上看,这是一种断言吗?而且,为什么要在语言中强加一个可以引发/nothing/可以处理的异常的限制呢?

如果您所谈论的断言意味着程序抛出然后存在,那么断言可能非常糟糕。这并不是说它们是错误的东西,它们是一个很容易被误用的结构。他们也有很多更好的选择。这样的事情很容易被称为邪恶。

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

断言的一些替代方法:

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

参考:

即使主张assert的人也认为它们应该只在开发中使用,而在生产中使用:

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

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

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

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

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

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),所以我喜欢在设置函数中使用断言来记录无论谁调用函数,value都不能大于或等于end

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