什么时候该用ASSERT?


—————————————————-
Michael to pongba

有下面2种方法:

方法一:Section *pImageSection = new Section(pImage);

assert(pImageSection);

方法二:略

ps:现在项目组代码用第一方法,并且也不写日志,每一次客户端down了,定位问题都要很久,让人很崩溃。并且到处都是assert。

我个人认为,用assert的地方,是比较严重的错误,甚至不能够让程序再运行下去。

如果到处用asset也太残忍了,有的时候应该温柔的跳过,然后写日志,返回。告诉我哪里运行失败了。

—————————————————-

下面是我对这个问题的回答:

首先需要指出一个非常重要、讨论前必须明确的问题,assert一般不应该直接用,而应该包装一下,比如微软就有ASSERT宏,可以跟踪line file这些有用信息,另外可以通过条件编译取消ASSERT。每次定位很久,只能说这种编程common sense还没有建立起来。

原文虽然说得很简单,其实涉及了几个方面,一个是版本管理发布,一个是错误处理机制,一个是如何写日志,我们一个个讨论。

首先是什么情况下用ASSERT,什么情况下应该处理wrong case?

一个成熟的代码里面,大概有百分之三十到七十的代码都是为了查错,fault tolerance。比如你写一个API或者对外使用的函数,别人要调用,你要确保传入的指针不为空,传入的参数合法,这时候都不应该,注意是不应该使用ASSERT,因为这些情况out of your control。

什么情况可以使用ASSERT,应该是那些绝少发生的例子,而且你可以控制的那些代码段落,比如Object* p = new Object,返回空指针,这种情况下多半是内存申请出错,即使你容错了,后面的逻辑也不会正常,这时候就应该停止运行报错了。还有就是你的内部函数(注意是你程序的内部哦),因为内部函数的代码责任其实是你自己的,你传错参数只能说水平太差怨不得别人。也许有人会说,同组里别的程序员万一也调用了这个函数怎么办?这种情况当然是会有的,其实也没法避免,但可以用ASSERT以及Code review这些手段来规范,同一个程序组里面的问题属于内部矛盾好解决多了。

所以,大的原则可以是这样:内部函数尽量简化容错机制,稍微苛刻一些,而对外接口则尽量容忍,出错处理也要温柔。

第二个问题,ASSERT该怎么写?

我已经提到,不要直接使用assert,最好包装一下,加入#ifdef _DEBUG这种条件编译选项,保证Release版本不弹出ASSERT。这里其实也涉及到版本管理问题,一个商业产品发布一般包含两个版本(Release版以及Debug checked版本),即使是Windows操作系统也是如此。在Release版本要Disasble assert,这时候如果出错可以用后面的Crash Dump机制捕捉跟踪,而且要说明的是Release版本一样要附带PDB文件(或者Linux同类文件),保证Crash Dump可以定位。Debug版本也要发给用户,用户可以通过它返回更详细信息。每个Build出来的Release、Debug都应该妥善备份,将来客户提交Bug,对应版本以及PDB,就可以准确定位,不会出现很难差错的现象。

另外也可以加入日志log选项。在这里跑一下题,日志记录也是个大问题,要考虑多线程以及性能问题,其实不是加入日志系统就万事大吉了,说句难听的,对于那些2B Number One的人来讲,任何一个好东西都可能被他们用到烂。一般ASSERT跑出来,对于客户来说就等同于Crash了,所以可以参考一下Google的Crash Report开源项目,比如Windows下可使用Crash Dump机制来捕捉崩溃时调用栈,这样即使Fail Soon Fail Fast也不会很难看。这些手段都是common sense的东西,一个正常的软件开发公司应该积累下这些代码财富才对。


《“什么时候该用ASSERT?”》 有 7 条评论

  1. 内部和外部的划分错误。不是按自己的代码和别人的代码来分,而是应该按进程内外来分。API接口规范中要求传入指针非空,就应该用断言表达出来。

  2. ps:现在项目组代码用第一方法,并且也不写日志,每一次客户端down了,定位问题都要很久,让人很崩溃。并且到处都是assert。

    我也是这么用的,定位很方便。都有行数和条件显示的。assert拉停系统很容易引起争议,最后发布release版本要关掉。但关掉的目的是提高性能,不要指望关了出错就不死机了。

    assert用法百度上应该有说明吧,查下 “检查型异常与非检查型异常”,哪些错误该用assert捕获,哪些要返回失败,都很清楚的,不要用混就行了。

  3. 明确一个问题, 什么时候必须用assert:
    1. 程序内部逻辑不一致
    例子1:发现自己写的list的计数器cnt<0
    原因分析:
    1. 内存乱掉了,可能是被某个野指针搅乱了list的内存状态
    2. 逻辑错误,代码在操作cnt的过程中有bug
    2. 内部的precondition失败
    例子1:在同一公司,或者同一部门中,被调用函数int func(char* ibuf, size_t in_len, char* obuf, size_t max_len, size_t* out_len),检测发现ibuf=NULL;或者发现ibuf=NULL, 但in_len!=0。
    原因分析:调用者没有理解该函数的主义;调用者粗心大意,没有校验传进来的参数;调用者的状态不一致

    以上两种情况, 对于第一种, 必须fast fail,无论debug还是release版本,都应保留assert。原因是从发现状态不一致这一刻起,程序的行为已经完全无法预测,如果继续运行,可能会造成灾难性的后果(试想银行转账),没有任何益处。
    对于第二种,本人主张fast fail,无论debug还是release都应保留,因为这是属于团队内部的交流,直面错误,遇到问题大声叫出来,对整个团队的生产力提高是有好处的。

    其它任何情况,都不应该使用assert。包括malloc返回NULL,因为不属于以上两种情况的任何一种,程序内部的逻辑状态仍然一致,如果牺牲掉当前feature,现有的用户可以正常运行。
    详细情况,见我的博客http://blog.hhyue.com/when_to_use_assert.html

  4. 嗨,这篇文章本人认为异常有意思,请问博主能够让我转到吗?我会保存原文出处的链接以及你的姓名!

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注