是断言邪恶?
Go
语言创build者写道 :
Go没有提供断言。 它们不可否认方便,但我们的经验是,程序员使用它们作为拐杖,以避免考虑正确的error handling和报告。 正确的error handling意味着服务器在非致命错误而不是崩溃之后继续运行。 适当的错误报告意味着错误是直接的,并且重点在于,使程序员不必解释大的崩溃痕迹。 当程序员看到错误不熟悉代码时,精确的错误是特别重要的。
你对此有何看法?
不,只要按照预期使用, assert
就没有任何问题。
也就是说,它应该是在debugging期间捕捉“不可能发生”的情况,而不是正常的error handling。
- 断言:程序逻辑本身的失败。
- error handling:错误的input或系统状态不是由于程序中的错误。
不,既不去也不去邪恶。 但两者都可能被滥用。
断言是为了健康检查。 如果程序不正确,应该杀死程序。 不用于validation或作为error handling的替代。
按照这个逻辑,断点也是邪恶的。
断言应该用作debugging帮助,而不是别的。 “邪恶”是当你尝试使用它们而不是error handling。
断言是为了帮助程序员,帮助你发现和解决一些不存在的问题,并validation你的假设是否真实。
他们与error handling无关,但不幸的是,一些程序员滥用它们,然后宣称它们是“邪恶的”。
我喜欢用很多断言。 当我第一次构build应用程序(也许是一个新的域)时,我发现它非常有用。 而不是做非常花哨的错误检查(我会考虑过早优化),我快速编码,并添加了很多断言。 当我知道更多关于事情的工作之后,我做了重写并删除了一些断言,并改变它们以获得更好的error handling。
由于断言我花了很多时间编码/debugging程序。
我也注意到,断言帮助我想到了许多可能会破坏我的程序的事情。
他们应该用来检测程序中的错误。 不错的用户input。
如果使用得当,他们不是邪恶的。
作为附加信息,去提供一个内置的functionpanic
。 这可以用来代替assert
。 例如
if x < 0 { panic("x is less than 0"); }
panic
将打印堆栈跟踪,所以在某种程度上它有assert
的目的。
这个问题出现了很多,我认为一个使得断言的辩护混淆的问题是它们通常基于参数检查。 所以考虑一下你可能使用断言的另一个例子:
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
您使用input的例外,因为您希望有时会得到错误的input。 你声明这个列表已经sorting,以帮助你find你的algorithm中的一个错误,根据定义你是不期望的。 断言只在debugging版本中,所以即使这个检查代价很高,你也不介意每次调用这个例程。
您仍然需要对您的生产代码进行unit testing,但这是确保您的代码正确的另一种方法。 unit testing可以确保你的例程完全符合它的接口,而断言是一种更细致的方式,以确保你的实现正在做你期望的。
断言不是邪恶的,但它们很容易被滥用。 我同意“断言经常被用作避免考虑正确的error handling和报告”的陈述。 我经常看到这个。
就我个人而言,我喜欢使用断言,因为它们logging了我在编写代码时可能做出的假设。 如果这些假设在维护代码的同时被破坏,则在testing期间可以检测到问题。 不过,我在做一个生产构build(即使用#ifdefs)的时候,已经把我的代码中的每个断言都剥离了出来。 通过剥离生产版本中的断言,我消除了任何人误用它们作为拐杖的风险。
断言还有另一个问题。 断言只在运行时被检查。 但通常情况下,您想要执行的检查可能在编译时执行。 在编译时最好检测一个问题。 对于C ++程序员来说,boost提供了BOOST_STATIC_ASSERT,允许你这样做。 对于C程序员来说,这篇文章( 链接文本 )描述了一种可以在编译时用来执行断言的技术。
总之,我遵循的经验法则是:不要在生产版本中使用断言,并且如果可能的话,只对在编译时无法validation的事情使用断言(即必须在运行时检查)。
我承认在不考虑适当的错误报告的情况下使用断言。 但是,这并不能消除在正确使用时它们非常有用。
如果你想遵循“早期碰撞”的原则,它们特别有用。 例如,假设你正在实现一个引用计数机制。 在您的代码中的某些位置,您知道该refcount应该是零或一。 还假设如果引用计数错误,程序不会立即崩溃,而是在下一个消息循环期间很难找出错误的原因。 断言会有助于检测更接近其原点的错误。
我更喜欢避免在debugging和发布时执行不同的代码。
打破debugging器的条件,并具有所有的文件/行信息是有用的,也确切的expression式和确切的价值。
有一个断言“只能在debugging中评估条件”可能是性能优化,因此只有在0.0001%的程序中才有用 – 人们知道他们在做什么。 在所有其他情况下,这是有害的,因为expression可能实际上改变程序的状态:
assert(2 == ShroedingersCat.GetNumEars());
会使程序在debugging和发布中做不同的事情。
我们已经开发了一组assertmacros,这将引发exception,并在debugging版本和发行版本中执行。 例如, THROW_UNLESS_EQ(a, 20);
会抛出一个exception,具有文件,行和a的实际值 ,等等。 只有一个macros才有这个权力。 debugging器可能被configuration为在特定exceptiontypes的“throw”处中断。
我不喜欢强烈地断言。 我不会去说他们是邪恶的。
基本上,一个断言会做一个未经检查的exception相同的事情,唯一的例外是断言(通常)不应该保留为最终产品。
如果在debugging和构build系统时为自己构build安全网,为什么要拒绝为您的客户,支持服务台或将要使用您当前正在构build的软件的任何人安全网。 对于断言和exception情况都使用例外。 通过创build适当的exception层次结构,您将能够很快地辨别出彼此。 除此之外,断言仍然存在,并可以提供有价值的信息,否则将失败的失败。
所以我完全理解了Go的创造者,完全消除了断言,迫使程序员使用exception来处理这种情况。 有一个简单的解释,例外只是一个更好的工作机制,为什么坚持古老的断言?
简短的回答:不,我相信断言是有用的
我最近开始为我的代码添加一些断言,这就是我一直在做的事情:
我从心理上将我的代码分成边界代码和内部代码。 边界代码是处理用户input,读取文件和从networking获取数据的代码。 在这个代码中,我要求input的循环只在input有效(在交互式用户input的情况下)时退出,或者在不可恢复的文件/networking损坏的数据的情况下抛出exception。
内部代码是其他的一切。 例如,在我的类中设置variables的函数可能被定义为
void Class::f (int value) { assert (value < end); member = value; }
而从networking获取input的函数可能会这样读取:
void Class::g (InMessage & msg) { int const value = msg.read_int(); if (value >= end) throw InvalidServerData(); f (value); }
这给了我两层检查。 数据在运行时被确定的任何东西总是会得到一个exception或者直接的error handling。 然而,使用assert
语句的Class::f
中的额外检查意味着如果某些内部代码曾经调用过Class::f
,我仍然有一个完整的检查。 我的内部代码可能不会传递一个有效的参数(因为我可能已经从一些复杂的函数系列中计算出了value
),所以我喜欢在设置函数中声明,不pipe谁在调用函数, value
不能大于大于或等于end
。
这似乎符合我在几个地方正在阅读的内容,认为在一个运转良好的scheme中断言是不可能违反的,而例外应该是对于仍然可能的特殊和错误的情况。 因为在理论上我正在validation所有的input,所以我的断言不应该被触发。 如果是这样,我的程序是错误的。
assert
正在滥用error handling,因为它是lessinput。
所以作为语言devise者,他们应该看到,即使较less的input,也可以进行适当的error handling。 不包括断言,因为你的exception机制是冗长的不是解决scheme。 哦,等等,Go也没有例外。 太糟糕了 :)
当我看到这些时,我感觉就像在踢脑子里的作者。
我在代码中始终使用断言,并最终在我编写更多代码时全部replace它们。 当我没有写出需要的逻辑,并且在运行代码的时候要提醒,而不是写一个在项目接近完成的时候会被删除的exception。
例外情况也更容易与我不喜欢的生产代码混合。 一个断言更容易被注意到比throw new Exception("Some generic msg or 'pretend i am an assert'");
我的问题与这些答案辩护断言是没有人明确指出是什么使它不同于常规的致命错误 ,为什么一个断言不能是一个例外的子集。 现在,有了这个说法,如果这个例外从来没有被捕捉过呢? 这是否通过命名使它成为一个断言? 而且,为什么你要在语言中施加一个限制,就是可以提出一个/什么都不能处理的例外呢?
是的,断言是邪恶的。
通常它们被用在应该使用适当的error handling的地方。 习惯于从一开始就编写适当的生产质量error handling!
通常他们会采取写unit testing的方式(除非你编写一个与你的testing工具交互的自定义断言)。 这通常是因为在正确使用error handling的地方使用它们。
大多数情况下,它们是从发布版本编译而来的,这意味着当您运行实际发布的代码时,没有任何“testing”可用; 鉴于在multithreading情况下,最糟糕的问题通常只在发布代码中出现,这可能是不好的。
有时候他们是破坏devise的拐杖; 即代码的devise允许用户以不应该被调用的方式调用它,并且断言“阻止”这一点。 修复devise!
我在2005年的博客上写了更多这方面的内容: http : //www.lenholgate.com/blog/2005/09/assert-is-evil.html
如果断言你说的意思是程序呕吐,然后存在,断言可能是非常糟糕的。 这并不是说他们总是使用错误的东西,他们是一个非常容易被滥用的构造。 他们也有很多更好的select。 这样的事情是被称为邪恶的好候选人。
例如,第三方模块(或任何真正的模块)几乎不应该退出调用程序。 这并没有给调用程序员以任何控制程序应该采取什么样的风险。 在许多情况下,数据非常重要,即使保存损坏的数据也比丢失数据要好。 断言可能会强制您丢失数据。
一些替代方法断言:
- 使用debugging器,
- 控制台/数据库/其他日志
- 例外
- 其他types的error handling
一些参考:
- http://ftp.gnu.org/old-gnu/Manuals/nana-1.14/html_node/nana_3.html
- http://www.lenholgate.com/blog/2005/09/assert-is-evil.html
- Go没有提供断言,并有很好的理由: http : //golang.org/doc/faq#assertions
- http://c2.com/cgi/wiki?DoNotUseAssertions
即使是主张主张的人也认为只能用于发展而不是用于生产:
- http://codebetter.com/gregyoung/2007/12/12/asserts-are-not-evil/
- http://www.codeproject.com/Articles/6404/Assert-is-your-friend
- http://parabellumgames.wordpress.com/using-asserts-for-debugging/
这个人说,当模块有可能损坏的数据在抛出exception之后仍然存在时应该使用断言: http : //www.advogato.org/article/949.html 。 这当然是一个合理的点,但是,外部模块不应该规定损坏的数据对调用程序有多重要(通过退出“for”)。 处理这个问题的正确方法是抛出一个exception,以明确程序现在可能处于不一致的状态。 而且由于好的程序主要由模块组成(在主要的可执行文件中只有一点胶水代码),断言几乎总是错误的。
没有那么邪恶,一般适得其反。 永久性的错误检查和debugging是分开的。 断言让人们认为所有的debugging应该是永久性的,并且在使用太多时会导致大量的可读性问题。 永久error handling应该比需要的更好,因为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
可以被滥用。 我一直这样做。
我从来没有使用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 eg "Failed spawning 10 monsters". } else { // initialize monsters. }