我已经获得CS学位,现在已经是专业软件工程师大约一年了。我已经在C ++和C中了解断言了一段时间了,但是直到最近才完全不知道它们存在于C#和.NET中。
我们的生产代码不包含任何断言,我的问题是……
我应该在生产代码中开始使用Asserts吗?如果是这样,什么时候最合适使用它?这样做更有意义吗
Debug.Assert(val != null);
要么
if ( val == null )
throw new exception();
我已经获得CS学位,现在已经是专业软件工程师大约一年了。我已经在C ++和C中了解断言了一段时间了,但是直到最近才完全不知道它们存在于C#和.NET中。
我们的生产代码不包含任何断言,我的问题是……
我应该在生产代码中开始使用Asserts吗?如果是这样,什么时候最合适使用它?这样做更有意义吗
Debug.Assert(val != null);
要么
if ( val == null )
throw new exception();
Answers:
在调试Microsoft .NET 2.0应用程序中, John Robbins在断言中有很大一部分。他的主要观点是:
PS:如果您喜欢Code Complete,我建议您在本书中继续阅读。我购买它是为了学习有关使用WinDBG和转储文件的知识,但前半部分包含了许多技巧,可帮助您首先避免出现bug。
Debug.Assert
对Trace.Assert
。后者在Release版本和Debug版本中执行。
将Debug.Assert()
代码放在任何地方,要进行完整性检查以确保不变性。当您编译Release版本(即没有DEBUG
编译器常量)时,对的调用Debug.Assert()
将被删除,因此它们不会影响性能。
您仍应在调用之前引发异常Debug.Assert()
。断言只是确保在您仍在开发时一切都按预期进行。
从代码完成
8防御性编程
8.2断言
断言是在开发过程中使用的代码(通常是例程或宏),它使程序可以在运行时进行自我检查。当一个断言为真时,这意味着一切都按预期运行。如果为假,则表示已在代码中检测到意外错误。例如,如果系统假定客户信息文件的记录永远不会超过50,000,则该程序可能会断言记录数小于或等于50,000。只要记录数小于或等于50,000,该断言将是静默的。但是,如果遇到超过50,000条记录,它将大声“断言”程序中存在错误。
断言在大型,复杂程序和高可靠性程序中特别有用。它们使程序员能够更快地清除不匹配的接口假设,修改代码时产生的错误等等。
断言通常带有两个参数:一个布尔表达式,描述应该为真的假设;否则为要显示的消息。
(...)
通常,您不希望用户在生产代码中看到断言消息。断言主要用于开发和维护期间。断言通常在开发时编译到代码中,并从代码中编译出来用于生产。在开发过程中,断言消除了相互矛盾的假设,意外情况,传递给例程的错误值等。在生产过程中,它们是用代码编译的,因此断言不会降低系统性能。
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(即发布版本),则可以使用Trace.Assert而不是Debug.Assert。
当然,这会增加生产可执行文件的开销。
同样,如果您的应用程序以用户界面模式运行,则默认情况下将显示“断言”对话框,这可能会使您的用户有些不安。
您可以通过删除DefaultTraceListener来覆盖此行为:查看MSDN中Trace.Listeners的文档。
综上所述,
广泛使用Debug.Assert可以帮助捕获Debug版本中的错误。
如果在用户界面模式下使用Trace.Assert,则可能要删除DefaultTraceListener以避免使用户感到困惑。
如果您要测试的条件是您的应用无法处理的,那么最好抛出一个异常,以确保执行不会继续。请注意,用户可以选择忽略断言。
简而言之
Asserts
用于警卫和检查按合同约束的设计,即:
Asserts
应该仅用于调试和非生产版本。声明通常在发行版中被编译器忽略。Asserts
可以检查系统控制中的错误/意外情况Asserts
不是用于用户输入或业务规则的一线验证的机制Asserts
应该不会被用来检测突发环境条件(这是外部的代码的控制),如超出内存,网络故障,数据库故障等。虽然罕见,这些条件是可以预期的(和你的应用程序代码无法修复问题,如硬件故障或资源耗尽)。通常,将引发异常-您的应用程序可以采取纠正措施(例如,重试数据库或网络操作,尝试释放缓存的内存),或者在无法处理异常的情况下优雅地中止。Asserts
-您的代码在意外的区域中运行。堆栈跟踪和崩溃转储可用于确定出了什么问题。断言具有巨大的好处:
Debug
构建中进行检查。... 更多详情
Debug.Assert
表示在程序控制范围内的代码块其余部分假定的状态状态。这可以包括提供的参数的状态,类实例的成员的状态,或者方法调用的返回在其约定的/设计的范围内。通常,断言应使用所有必要的信息(堆栈跟踪,崩溃转储等)使线程/进程/程序崩溃,因为它们表明存在未设计的错误或未考虑的情况(即,不要尝试捕获或捕获)。处理断言失败),但断言本身何时可能造成比错误更大的破坏(例如,飞机降落时空中交通管制员不希望YSOD)是一个可能的例外,尽管是否应将调试版本部署到生产 ...)
您应该何时使用Asserts?
-在系统,库API或服务中的任何时候,假设对某个函数或类状态的输入是有效的(例如,当已经对系统表示层中的用户输入进行了验证时) ,业务和数据层类通常假定已经完成对输入的null检查,范围检查,字符串长度检查等)。-常见Assert
检查包括无效假设会导致空对象解除引用,零除数,数值或日期算术溢出以及常规带外/不适用于行为的情况(例如,如果使用32位int建模人类的年龄,则为常规检查) ,请谨慎考虑Assert
年龄实际上是介于0到125之间-并非专为-100和10 ^ 10的值而设计。
.NET代码合同
在.NET堆栈,代码契约可以用于除了,或作为替代使用Debug.Assert
。代码合同可以进一步形式化状态检查,并可以帮助在编译时(或此后不久,如果在IDE中作为后台检查运行)检测到违反假设的情况。
可用的按合同设计(DBC)检查包括:
Contract.Requires
-约定的前提条件 Contract.Ensures
-合同规定的后期条件 Invariant
-表示关于对象在其生命周期中所有点的状态的假设。Contract.Assumes
-调用非合同修饰方法时,使静态检查器平静。在我的书中几乎从来没有。在大多数情况下,如果您想检查一切是否正常,则抛出异常。
我不喜欢的事实是,它使调试版本与发布版本在功能上有所不同。如果调试断言失败,但是该功能在发行版中有效,那么这有什么意义呢?当断言者离开公司很久并且没人知道那部分代码时,情况会更好。然后,您必须花费一些时间来探索问题,以查看是否确实存在问题。如果有问题,那为什么不把人放在首位呢?
对我来说,这建议您使用Debug.Asserts,将问题推迟到其他人那里,亲自解决。如果应该是这种情况,那么就不会抛出。
我猜可能在一些性能至关重要的场景中,您需要优化断言,并且在这里很有用,但是我还没有遇到这样的场景。
System.Diagnostics.Trace.Assert()
在Release版本和Debug版本中执行。
仅在要为发布版本删除支票的情况下才使用断言。请记住,如果不以调试模式进行编译,则不会触发您的断言。
给定您的null检查示例,如果该示例位于仅限内部使用的API中,则可以使用断言。如果在公共API中,我肯定会使用显式检查并抛出。
System.Diagnostics.Trace.Assert()
在发布(生产)版本中执行断言。
null
:“外部可见方法取消引用其参考参数之一,而不验证该参数是否为 null 。” 在这种情况下,方法或属性应抛出ArgumentNullException
。
所有的断言应该是可以优化为:
Debug.Assert(true);
因为它正在检查您已经假设为真的东西。例如:
public static void ConsumeEnumeration<T>(this IEnumerable<T> source)
{
if(source != null)
using(var en = source.GetEnumerator())
RunThroughEnumerator(en);
}
public static T GetFirstAndConsume<T>(this IEnumerable<T> 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<T>(IEnumerator<T> en)
{
Debug.Assert(en != null);
while(en.MoveNext());
}
在上面,有三种不同的方法来处理空参数。第一个接受它是允许的(它什么也不做)。第二个抛出异常,供调用代码处理(或不生成,导致错误消息)。第三个假设它不可能发生,并断言确实如此。
在第一种情况下,没有问题。
在第二种情况下,调用代码存在问题-不应GetFirstAndConsume
使用null 进行调用,因此它会返回异常。
在第三种情况下,此代码存在问题,因为应该en != null
在调用它之前就已经对其进行了检查,以至于它不是真的是一个错误。换句话说,应该是理论上可以优化为的代码Debug.Assert(true)
,sicne en != null
应该始终是true
!
en == null
在生产中会发生什么呢?你可能说,en == null
可从来没有发生在生产中(因为该方案已被彻底调试)?如果是这样,那么Debug.Assert(en != null)
至少可以替代评论。当然,如果将来进行更改,它也将继续具有检测可能的回归的价值。
Debug.Assert()
在Release版本中删除对的呼叫。所以,如果你是错的,在第三种情况下,你不会知道它在生产中(假设在生产过程中使用一个发布版本的)。但是,第一种情况和第二种情况的行为在Debug和Release版本中是相同的。
我以为我会再添加四种情况,其中Debug.Assert可能是正确的选择。
1)我在这里没有提到的是Asserts在自动测试期间可以提供的其他概念覆盖。作为一个简单的例子:
当作者认为某些更高级别的调用者对其进行了修改后,他认为他们已经扩展了代码范围以处理其他情况,理想情况下(!),他们将编写单元测试以涵盖此新条件。然后,完全集成的代码可能会正常工作。
但是,实际上已经引入了一个细微的缺陷,但是在测试结果中并未发现。在这种情况下,被调用方已变得不确定,只能碰巧提供预期的结果。或者,它产生了未被注意的舍入误差。或导致了在其他位置均被抵消的错误。或不仅授予请求的访问权限,还授予其他不应授予的特权。等等。
在这一点上,被调用方中包含的Debug.Assert()语句以及由单元测试驱动的新案例(或边缘案例)可以在测试期间提供宝贵的通知,证明原始作者的假设已失效,并且代码不应无需额外审核即可发布。带有单元测试的断言是完美的伙伴。
2)此外,有些测试编写起来很简单,但是由于最初的假设,成本很高且不必要。例如:
如果只能从某个安全的入口点访问对象,是否应通过每个对象方法对网络权限数据库进行附加查询,以确保调用者具有权限?当然不是。也许理想的解决方案包括缓存或其他一些功能扩展,但设计不需要。当对象已附加到不安全的入口点时,Debug.Assert()将立即显示。
3)接下来,在某些情况下,以发布模式部署时,您的产品可能无法对其全部或部分操作进行有用的诊断交互。例如:
假设它是嵌入式实时设备。引发异常并在遇到格式错误的数据包时重新启动会适得其反。取而代之的是,该设备可能会从尽力而为的操作中受益,甚至会在其输出中产生噪声。它也可能没有人机界面,日志记录设备,甚至当以发布模式部署时,甚至根本无法被人物理访问,并且最好通过评估相同的输出来提供对错误的认识。在这种情况下,自由声明和全面的预发布测试比例外更有价值。
4)最后,仅由于被调用方被认为是非常可靠的,所以不需要进行某些测试。在大多数情况下,可重用的代码越多,投入更多的精力使其变得可靠。因此,对于来自调用方的意外参数来说,异常是常见的,而对于来自被调用方的意外结果而言,断言是常见的。例如:
如果核心String.Find
操作指出-1
未找到搜索条件时它将返回a ,则您可以安全地执行一项操作,而不是三项。但是,如果它确实返回了-2
,那么您可能没有合理的措施。用单独测试一个-1
值的简单计算代替无用的计算,在大多数发行环境中用确保核心库按预期运行的测试来乱码,这是不合理的。在这种情况下,断言是理想的。
从报价采取实用主义程序员:从熟练工到硕士
保持断言处于打开状态
关于断言的常见误解是由编写编译器和语言环境的人发布的。它是这样的:
断言为代码增加了一些开销。因为它们检查了永远都不会发生的事情,所以它们只会被代码中的错误触发。一旦测试并交付了代码,就不再需要它们,应该将其关闭以使代码运行更快。断言是一种调试工具。
这里有两个明显错误的假设。首先,他们认为测试可以找到所有错误。实际上,对于任何复杂的程序,您甚至都不可能测试将要通过代码的排列的很小一部分(请参阅无情的测试)。
其次,乐观主义者忘记了您的程序在危险的世界中运行。在测试过程中,老鼠可能不会咬通讯电缆,玩游戏的人不会耗尽内存,日志文件也不会填满硬盘。当您的程序在生产环境中运行时,可能会发生这些事情。您的第一道防线是检查任何可能的错误,第二道防线是使用断言尝试检测您遗漏的错误。
在将程序交付到生产环境时关闭断言就像在没有网络的情况下越过高线一样,因为您曾经在实践中将其跨过。有巨大的价值,但很难获得人寿保险。
即使您确实有性能问题,也请仅关闭真正触动您的断言。
您应该始终使用第二种方法(引发异常)。
另外,如果您正在生产中(并具有发布版本),则抛出异常(使应用程序在最坏的情况下崩溃)比使用无效值可能会破坏客户的数据(可能要花费数千美元)更好。美元)。
我已经在这里阅读了答案,并认为应该添加一个重要的区别。使用断言有两种非常不同的方式。一个临时的开发人员快捷方式是“在您的程序能够继续运行的情况下,这应该像有条件的断点”,“这应该不会真的发生,如果它确实让我知道,这样我就可以决定要做什么”。另一种是在代码中放置有关有效程序状态的假设的方法。
在第一种情况下,断言甚至不需要在最终代码中。您应该Debug.Assert
在开发过程中使用它,并且在/不再需要它们时可以将其删除。如果您想离开它们,或者忘记删除它们,则没有问题,因为它们在Release编译中不会有任何后果。
但是在第二种情况下,断言是代码的一部分。他们断言您的假设是正确的,并记录在案。在这种情况下,您真的想将它们留在代码中。如果程序处于无效状态,则不应继续执行该程序。如果您负担不起性能方面的损失,那么您就不会使用C#。一方面,如果发生这种情况,能够附加调试器可能会很有用。另一方面,您不希望在用户上弹出堆栈跟踪,也许更重要的是,您不希望他们能够忽略它。此外,如果它在服务中,它将始终被忽略。因此,在生产中,正确的行为是引发异常,并使用程序的常规异常处理,这可能会向用户显示一个不错的消息并记录详细信息。
Trace.Assert
拥有实现此目标的完美方法。它不会在生产中删除,并且可以使用app.config与其他侦听器一起配置。因此,对于开发而言,默认处理程序很好,对于生产而言,您可以创建如下所示的简单TraceListener,该方法将引发异常并在生产配置文件中将其激活。
using System.Diagnostics;
public class ExceptionTraceListener : DefaultTraceListener
{
[DebuggerStepThrough]
public override void Fail(string message, string detailMessage)
{
throw new AssertException(message);
}
}
public class AssertException : Exception
{
public AssertException(string message) : base(message) { }
}
并在生产配置文件中:
<system.diagnostics>
<trace>
<listeners>
<remove name="Default"/>
<add name="ExceptionListener" type="Namespace.ExceptionTraceListener,AssemblyName"/>
</listeners>
</trace>
</system.diagnostics>