我已经是一名专业软件工程师,已经有一年的时间了,已经获得了CS学位.我已经知道C++和C中有一段时间的断言,但直到最近才知道它们在C#和.NET中存在.
我们的生产代码不包含任何断言,我的问题是......
我应该在生产代码中开始使用Asserts吗?如果是这样,它的使用何时最合适?这样做会更有意义吗?
Debug.Assert(val != null);
要么
if ( val == null ) throw new exception();
Rory MacLeod.. 224
在调试Microsoft .NET 2.0应用程序中, John Robbins有一个关于断言的重要部分.他的要点是:
自由地断言.你永远不会有太多的断言.
断言不会取代异常.例外涵盖了您的代码所要求的内容; 断言涵盖了它所假设的事物.
一个写得很好的断言不仅可以告诉你发生了什么以及在哪里(例如异常),而是为什么.
异常消息通常很神秘,要求您通过代码向后工作以重新创建导致错误的上下文.断言可以在错误发生时保留程序的状态.
断言加倍作为文档,告诉其他开发人员你的代码所依赖的隐含假设.
断言失败时出现的对话框允许您将调试器附加到进程,因此您可以在堆栈周围查看,就像在那里放置断点一样.
PS:如果您喜欢Code Complete,我建议您继续阅读本书.我买它是为了学习使用WinDBG和转储文件,但上半部分包含了一些提示,以帮助避免错误.
在调试Microsoft .NET 2.0应用程序中, John Robbins有一个关于断言的重要部分.他的要点是:
自由地断言.你永远不会有太多的断言.
断言不会取代异常.例外涵盖了您的代码所要求的内容; 断言涵盖了它所假设的事物.
一个写得很好的断言不仅可以告诉你发生了什么以及在哪里(例如异常),而是为什么.
异常消息通常很神秘,要求您通过代码向后工作以重新创建导致错误的上下文.断言可以在错误发生时保留程序的状态.
断言加倍作为文档,告诉其他开发人员你的代码所依赖的隐含假设.
断言失败时出现的对话框允许您将调试器附加到进程,因此您可以在堆栈周围查看,就像在那里放置断点一样.
PS:如果您喜欢Code Complete,我建议您继续阅读本书.我买它是为了学习使用WinDBG和转储文件,但上半部分包含了一些提示,以帮助避免错误.
将Debug.Assert()
所有代码放在您想要的代码中进行健全性检查以确保不变量.编译Release版本(即没有DEBUG
编译器常量)时,Debug.Assert()
将删除调用,因此它们不会影响性能.
你应该在调用之前抛出异常Debug.Assert()
.断言只是确保在你还在开发的时候,一切都如预期的那样.
从代码完成
8防御性编程
8.2断言
断言是在开发期间使用的代码 - 通常是例程或宏 - 允许程序在运行时检查自身.当断言为真时,这意味着一切都按预期运行.当它为假时,这意味着它已在代码中检测到意外错误.例如,如果系统假定客户信息文件永远不会有超过50,000条记录,则程序可能包含一条断言,即记录数量小于或等于50,000.只要记录数小于或等于50,000,断言就会保持沉默.但是,如果它遇到超过50,000条记录,它将大声"断言"程序中存在错误.
断言在大型复杂程序和高可靠性程序中特别有用.它们使程序员能够更快地清除不匹配的接口假设,修改代码时出现的错误,等等.
断言通常需要两个参数:一个布尔表达式,用于描述假定应该为true的假设,以及一个要显示的消息(如果不是).
(......)
通常,您不希望用户在生产代码中看到断言消息; 断言主要用于开发和维护期间.断言通常在开发时编译到代码中,并从代码中编译生产.在开发过程中,断言会清除相互矛盾的假设,意外情况,传递给例程的错误值等等.在生产过程中,它们是从代码中编译出来的,因此断言不会降低系统性能.
FWIW ...我发现我的公共方法倾向于使用if () { throw; }
模式来确保正确调用该方法.我的私人方法倾向于使用Debug.Assert()
.
我的想法是,使用我的私有方法,我是一个受控制的人,所以如果我开始使用不正确的参数调用我自己的私有方法之一,那么我已经在某个地方打破了我自己的假设 - 我应该从未得到过进入那个州.在生产中,这些私有断言理想上应该是不必要的工作,因为我应该保持我的内部状态有效和一致.与给予公共方法的参数对比,任何人都可以在运行时调用这些参数:我仍然需要通过抛出异常来强制执行参数约束.
此外,如果某些内容在运行时不起作用(网络错误,数据访问错误,从第三方服务检索到的错误数据等),我的私有方法仍然可以抛出异常.我的断言只是为了确保我没有打破我自己关于对象状态的内部假设.
使用断言来检查开发人员的假设和异常,以检查环境假设.
如果我是你,我会这样做:
Debug.Assert(val != null); if ( val == null ) throw new exception();
或者避免重复检查
if ( val == null ) { Debug.Assert(false,"breakpoint if val== null"); throw new exception(); }
如果您希望在生产代码中使用Asserts(即Release版本),则可以使用Trace.Assert而不是Debug.Assert.
这当然会增加生产可执行文件的开销.
此外,如果您的应用程序在用户界面模式下运行,默认情况下将显示"断言"对话框,这可能会让您的用户感到有些不安.
您可以通过删除DefaultTraceListener来覆盖此行为:请查看MSDN中Trace.Listeners的文档.
综上所述,
使用Debug.Assert可以帮助捕获Debug构建中的错误.
如果在用户界面模式下使用Trace.Assert,则可能需要删除DefaultTraceListener以避免使用户感到不安.
如果您正在测试的条件是您的应用无法处理的情况,那么最好不要抛出异常,以确保执行不会继续.请注意,用户可以选择忽略断言.
断言用于捕获程序员(您的)错误,而不是用户错误.只有在用户无法触发断言时才应使用它们.例如,如果您正在编写API,则不应使用断言来检查API用户可以调用的任何方法中的参数是否为空.但是它可以在一个私有方法中使用,而不是作为API的一部分公开,以断言你的代码在它不应该传递时永远不会传递null参数.
当我不确定时,我通常偏爱断言而不是断言.
简而言之
Asserts
用于保护和检查设计合同约束,即:
Asserts
应仅适用于Debug和非生产版本.在版本构建中,编译器通常会忽略断言.
Asserts
可以检查系统控制中的错误/意外情况
Asserts
不是用于用户输入或业务规则的第一行验证的机制
Asserts
应该不会被用来检测突发环境条件(这是代码的控制之外),如超出内存,网络故障,数据库故障等.虽然罕见,这些条件是可以预期的(和你的应用程序代码无法修复像问题硬件故障或资源耗尽).通常,会抛出异常 - 您的应用程序可以采取纠正措施(例如,重试数据库或网络操作,尝试释放缓存的内存),或者如果无法处理异常,则优先中止.
失败的断言对您的系统来说应该是致命的 - 即与异常不同,不要尝试捕获或处理失败Asserts
- 您的代码在意外的区域中运行.堆栈跟踪和故障转储可用于确定出错的地方.
断言有很多好处:
帮助查找缺少用户输入的验证,或更高级代码中的上游错误.
代码库中的断言清楚地将代码中的假设传达给读者
将在运行时在Debug
构建中检查断言.
一旦代码已经被彻底测试,重建代码版本将删除验证假设的性能开销(但与后面的调试版本总是会恢复的检查,如果需要的利益).
... 更多详情
Debug.Assert
表示在程序控制下由代码块的其余部分假定的关于状态的条件.这可以包括所提供参数的状态,类实例的成员状态,或者方法调用的返回处于其合同/设计范围内.通常,断言应该使线程/进程/程序崩溃所有必要的信息(堆栈跟踪,崩溃转储等),因为它们表明存在未设计的错误或未考虑的情况(即不要尝试捕获或处理断言失败),有一个可能的例外,当断言本身可能造成比bug更多的损害时(例如,当飞机进入潜艇时,空中交通管制员不会想要YSOD,尽管调试构建是否应该部署到生产 ...)
您应该何时使用Asserts?
- 在系统,库API或服务中的任何位置,其中假定函数或类状态的输入有效(例如,在系统的表示层中已对用户输入进行验证时) ,业务和数据层类通常假设已经完成了对输入的空检查,范围检查,字符串长度检查等).- 常见Assert
检查包括无效假设会导致空对象解除引用,零除数,数值或日期算术溢出,以及通用带外/非行为设计(例如,如果使用32位int来模拟人类年龄) ,谨慎的是Assert
,年龄实际上在0到125左右 - -100和10 ^ 10的值不是为了设计的).
.Net代码合同
在.Net堆栈中,代码合同可以作为使用的补充或替代使用Debug.Assert
.代码约定可以进一步形式化状态检查,并且可以在编译时(或之后不久,如果在IDE中作为后台检查运行)帮助检测违反假设的情况.
合同设计(DBC)检查包括:
Contract.Requires
- 合同规定的前提条件
Contract.Ensures
- 签约Postconditions
Invariant
- 表达关于对象在其生命周期中的所有点的状态的假设.
Contract.Assumes
- 在调用非合约修饰方法时,静默静态检查程序.
绝大多数都不在我的书中.在绝大多数情况下,如果你想检查一切是否理智,那么扔掉,如果不是.
我不喜欢的是它使调试版本在功能上与发布版本不同.如果调试断言失败但功能在发布中有效,那么这有什么意义呢?当断言器长期离开公司并且没有人知道代码的那部分时,它会更好.然后,您必须花费一些时间来探索问题,看看它是否真的是一个问题.如果这是一个问题那么为什么不是第一个投掷的人呢?
对我来说,这表明使用Debug.Asserts你将问题推迟给别人,自己处理问题.如果事情应该是这样的话,那就不会抛出.
我想有可能是性能关键的场景,你想要优化你的断言,它们在那里很有用,但是我还没有遇到这样的场景.
根据IDesign标准,你应该
断言每一个假设.平均而言,每五行就是一个断言.
using System.Diagnostics; object GetObject() {...} object someObject = GetObject(); Debug.Assert(someObject != null);
作为免责声明,我应该提到我没有发现实施这个IRL是否切合实际.但这是他们的标准.
仅在需要为发布版本删除检查的情况下使用断言.请记住,如果不在调试模式下编译,则不会触发断言.
给出check-for-null示例,如果这是在仅内部API中,我可能会使用断言.如果它在公共API中,我肯定会使用显式检查和抛出.
所有断言都应该是可以优化的代码:
Debug.Assert(true);
因为它正在检查你已经假设的东西是真的.例如:
public static void ConsumeEnumeration(this IEnumerable source) { if(source != null) using(var en = source.GetEnumerator()) RunThroughEnumerator(en); } public static T GetFirstAndConsume (this IEnumerable source) { if(source == null) throw new ArgumentNullException("source"); using(var en = source.GetEnumerator()) { if(!en.MoveNext()) throw new InvalidOperationException("Empty sequence"); T ret = en.Current; RunThroughEnumerator(en); return ret; } } private static void RunThroughEnumerator (IEnumerator en) { Debug.Assert(en != null); while(en.MoveNext()); }
在上文中,null参数有三种不同的方法.第一个接受它是允许的(它什么都不做).第二个抛出调用代码处理的异常(或不抛出,导致错误消息).第三个假设它不可能发生,并断言它是如此.
在第一种情况下,没有问题.
在第二种情况下,调用代码存在问题 - 它不应该GetFirstAndConsume
使用null 调用,因此它会返回异常.
在第三种情况下,这个代码存在一个问题,因为en != null
在它被调用之前它应该已经被检查过,所以它不是真的是一个错误.或者换句话说,它应该是理论上可以优化的代码Debug.Assert(true)
,sicne en != null
应该永远是true
!