我已经做了大约一年的专业软件工程师,毕业于 CS 学位。我知道 C++ 和 C 中的断言有一段时间了,但直到最近才知道它们存在于 C# 和 .NET 中。
我们的生产代码不包含任何断言,我的问题是......
我应该开始在我们的生产代码中使用断言吗?如果是这样,什么时候使用最合适?这样做会更有意义吗
Debug.Assert(val != null);
或者
if ( val == null )
throw new exception();
我已经做了大约一年的专业软件工程师,毕业于 CS 学位。我知道 C++ 和 C 中的断言有一段时间了,但直到最近才知道它们存在于 C# 和 .NET 中。
我们的生产代码不包含任何断言,我的问题是......
我应该开始在我们的生产代码中使用断言吗?如果是这样,什么时候使用最合适?这样做会更有意义吗
Debug.Assert(val != null);
或者
if ( val == null )
throw new exception();
在调试 Microsoft .NET 2.0 应用程序中,John Robbins 有一个关于断言的大节。他的主要观点是:
PS:如果您喜欢 Code Complete,我建议您继续阅读本书。我买它是为了了解如何使用 WinDBG 和转储文件,但前半部分包含了一些技巧,可以帮助避免首先出现错误。
将Debug.Assert()
代码中您希望进行健全性检查以确保不变量的任何位置放置。当您编译发布版本(即,没有DEBUG
编译器常量)时,对 的调用Debug.Assert()
将被删除,因此它们不会影响性能。
在调用Debug.Assert()
. 断言只是确保在您仍在开发时一切都按预期进行。
FWIW ...我发现我的公共方法倾向于使用该if () { throw; }
模式来确保正确调用该方法。我的私人方法倾向于使用Debug.Assert()
.
这个想法是,使用我的私有方法,我是受控制的,所以如果我开始使用不正确的参数调用我自己的私有方法之一,那么我在某处打破了我自己的假设——我不应该得到进入那个状态。在生产中,理想情况下,这些私有断言应该是不必要的工作,因为我应该保持我的内部状态有效和一致。与提供给公共方法的参数相比,任何人都可以在运行时调用它:我仍然需要通过抛出异常来强制执行参数约束。
此外,如果某些东西在运行时不起作用(网络错误、数据访问错误、从第三方服务检索到的错误数据等),我的私有方法仍然会引发异常。我的断言只是为了确保我没有打破我自己对对象状态的内部假设。
从代码完成
8 防御性编程
8.2 断言
断言是在开发过程中使用的代码(通常是例程或宏),它允许程序在运行时检查自身。当断言为真时,这意味着一切都按预期运行。当它为假时,这意味着它在代码中检测到意外错误。例如,如果系统假设客户信息文件的记录永远不会超过 50,000 条,则程序可能包含记录数小于或等于 50,000 条的断言。只要记录数小于或等于 50,000,断言就会保持沉默。但是,如果它遇到超过 50,000 条记录,它会大声“断言”程序中存在错误。
断言在大型复杂程序和高可靠性程序中特别有用。它们使程序员能够更快地清除不匹配的接口假设、修改代码时出现的错误等等。
一个断言通常需要两个参数:一个布尔表达式,描述假设为真,如果不是,则显示消息。
(……)
通常,您不希望用户在生产代码中看到断言消息;断言主要用于开发和维护期间。断言通常在开发时编译到代码中,并从代码中编译出来用于生产。在开发过程中,断言会清除矛盾的假设、意外情况、传递给例程的错误值等等。在生产过程中,它们是从代码中编译出来的,因此断言不会降低系统性能。
使用断言检查开发人员假设和异常检查环境假设。
如果我是你,我会这样做:
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 以避免让用户感到不安。
如果您正在测试的条件是您的应用程序无法处理的情况,您最好抛出异常,以确保执行不会继续。请注意,用户可以选择忽略断言。
断言用于捕获程序员(您的)错误,而不是用户错误。仅当用户不可能触发断言时才应使用它们。例如,如果您正在编写 API,则不应使用断言来检查 API 用户可以调用的任何方法中的参数是否不为空。但它可以用于不作为 API 的一部分公开的私有方法中,以断言您的代码在不应该传递空参数时永远不会传递。
当我不确定时,我通常更喜欢异常而不是断言。
简而言之
Asserts
用于保护和检查按合同设计的约束,即确保代码、对象、变量和参数的状态在预期设计的边界和限制内运行。
Asserts
应该仅用于调试和非生产版本。在发布版本中,编译器通常会忽略断言。Asserts
可以检查系统控制的错误/意外情况Asserts
不是用于第一线验证用户输入或业务规则的机制Asserts
不应用于检测意外的环境条件(超出代码的控制范围),例如内存不足、网络故障、数据库故障等。虽然很少见,但这些情况是可以预料的(并且您的应用程序代码无法修复诸如硬件故障或资源耗尽)。通常,异常会被抛出——您的应用程序可以采取纠正措施(例如重试数据库或网络操作,尝试释放缓存的内存),或者如果无法处理异常则优雅地中止。Asserts
——你的代码在意想不到的领域运行。堆栈跟踪和故障转储可用于确定出了什么问题。断言有很大的好处:
Debug
构建中进行检查。... 更多详情
Debug.Assert
表示在程序控制范围内由代码块的其余部分假定的关于状态的条件。这可以包括提供的参数的状态、类实例成员的状态,或者方法调用的返回在其约定/设计范围内。通常,断言应该使用所有必要的信息(堆栈跟踪、崩溃转储等)使线程/进程/程序崩溃,因为它们表明存在未设计用于的错误或未考虑的条件(即不要尝试捕获或处理断言失败),一个可能的例外是断言本身可能比错误造成更大的损害(例如,当飞机进入潜艇时,空中交通管制员不想要 YSOD,尽管是否应该部署调试版本是没有实际意义的生产 ...)
你应该什么时候使用Asserts?
Assert
检查包括无效假设将导致空对象取消引用、零除数、数字或日期算术溢出以及一般带外/不是为行为而设计的(例如,如果使用 32 位 int 来模拟人类的年龄,谨慎Assert
的做法是,年龄实际上在 0 到 125 左右之间——-100 和 10^10 的值不是为之设计的)。.Net 代码契约
在 .Net 堆栈中,代码契约可以作为.Net 协议的补充或替代使用Debug.Assert
。代码契约可以进一步形式化状态检查,并且可以帮助在编译时(或之后不久,如果在 IDE 中作为背景检查运行)检测违反假设的情况。
可用的合同设计 (DBC) 检查包括:
Contract.Requires
- 约定的先决条件Contract.Ensures
- 签约后置条件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<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)
,sicneen != null
应该总是true
!
我想我会再添加四个案例,其中 Debug.Assert 可能是正确的选择。
1)我在这里没有看到的是Asserts 在自动化测试期间可以提供的额外概念覆盖。举个简单的例子:
当作者修改了一些更高级别的调用者时,作者认为他们已经扩展了代码的范围以处理其他场景,理想情况下(!)他们将编写单元测试来覆盖这个新条件。然后可能是完全集成的代码似乎可以正常工作。
然而,实际上已经引入了一个细微的缺陷,但在测试结果中没有发现。在这种情况下,被调用者变得不确定,只是碰巧提供了预期的结果。或者它可能产生了一个未被注意到的舍入错误。或者导致在其他地方同样抵消的错误。或者不仅授予请求的访问权限,还授予不应授予的其他权限。等等。
此时,被调用方中包含的 Debug.Assert() 语句与单元测试驱动的新案例(或边缘案例)相结合,可以在测试期间提供宝贵的通知,即原作者的假设已失效,代码不应无需额外审查即可发布。带有单元测试的断言是完美的合作伙伴。
2)此外,一些测试编写起来很简单,但考虑到初始假设,成本高且不必要。例如:
如果一个对象只能从某个安全入口点访问,是否应该从每个对象方法对网络权限数据库进行额外查询以确保调用者具有权限?肯定不是。也许理想的解决方案包括缓存或其他一些功能扩展,但设计不需要它。当对象已附加到不安全的入口点时,Debug.Assert() 将立即显示。
3)接下来,在某些情况下,您的产品在发布模式下部署时可能对其全部或部分操作没有有用的诊断交互。例如:
假设它是一个嵌入式实时设备。当遇到格式错误的数据包时抛出异常并重新启动会适得其反。相反,设备可能会受益于尽力而为的操作,甚至会在其输出中呈现噪声。在发布模式下部署时,它也可能没有人机界面、日志记录设备,甚至根本无法被人类物理访问,并且最好通过评估相同的输出来提供对错误的感知。在这种情况下,自由断言和彻底的预发布测试比异常更有价值。
4)最后,一些测试是不必要的,只是因为被调用者被认为是非常可靠的。在大多数情况下,可重用的代码越多,就越努力使其可靠。因此,对于来自调用者的意外参数,异常是常见的,但对于来自被调用者的意外结果,断言是常见的。例如:
如果核心String.Find
操作声明在未找到搜索条件时将返回 a -1
,则您可以安全地执行一项操作而不是三项操作。但是,如果它真的返回了-2
,您可能没有合理的行动方案。将更简单的计算替换为单独测试一个-1
值的计算是没有帮助的,并且在大多数发布环境中使用测试乱扔代码以确保核心库按预期运行是不合理的。在这种情况下,断言是理想的。
摘自The Pragmatic Programmer:从熟练工到大师
打开断言
编写编译器和语言环境的人发布了关于断言的常见误解。它是这样的:
断言给代码增加了一些开销。因为它们会检查不应该发生的事情,所以它们只会被代码中的错误触发。一旦代码经过测试和交付,就不再需要它们,应该关闭它们以使代码运行得更快。断言是一种调试工具。
这里有两个明显错误的假设。首先,他们假设测试发现了所有的错误。实际上,对于任何复杂的程序,您都不太可能测试您的代码将通过的排列的一小部分(请参阅无情测试)。
其次,乐观主义者忘记了你的程序运行在一个危险的世界中。在测试期间,老鼠可能不会啃通讯线,玩游戏的人不会耗尽内存,日志文件也不会填满硬盘。当您的程序在生产环境中运行时,这些事情可能会发生。您的第一道防线是检查任何可能的错误,第二道防线是使用断言来尝试检测您遗漏的那些。
将程序交付到生产环境时关闭断言就像在没有网络的情况下穿越高线,因为您曾经在实践中穿越过。有巨大的价值,但很难获得人寿保险。
即使你确实有性能问题,也只关闭那些真正打击你的断言。
您应该始终使用第二种方法(抛出异常)。
此外,如果您正在生产中(并且有发布版本),最好抛出异常(并让应用程序在最坏的情况下崩溃)而不是使用无效值并可能破坏您客户的数据(这可能会花费数千美元)。
您应该使用 Debug.Assert 来测试程序中的逻辑错误。编译器只能通知您语法错误。所以你应该明确地使用 Assert 语句来测试逻辑错误。比如说测试一个销售汽车的程序,只有蓝色的宝马才能获得 15% 的折扣。编译器无法告诉您您的程序在执行此操作时是否在逻辑上正确,但断言语句可以。
我在这里阅读了答案,我认为我应该添加一个重要的区别。有两种非常不同的使用断言的方式。一个是作为“这不应该真的发生,所以如果它确实让我知道所以我可以决定做什么”的临时开发人员快捷方式,有点像条件断点,用于您的程序能够继续的情况。另一种方法是在代码中对有效程序状态进行假设。
在第一种情况下,断言甚至不需要在最终代码中。您应该Debug.Assert
在开发期间使用,如果/不再需要,您可以删除它们。如果您想留下它们或忘记删除它们也没问题,因为它们不会在发布编译中产生任何影响。
但在第二种情况下,断言是代码的一部分。他们,好吧,断言,你的假设是真实的,并且记录它们。在这种情况下,您真的想将它们留在代码中。如果程序处于无效状态,则不应允许其继续。如果您无法承受性能损失,您就不会使用 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>
我不知道它在 C# 和 .NET 中的情况如何,但在 C 中 assert() 仅在使用 -DDEBUG 编译时才有效 - 如果没有使用 -DDEBUG 编译,最终用户将永远不会看到 assert()。它仅供开发人员使用。我经常使用它,有时更容易跟踪错误。
我不会在生产代码中使用它们。抛出异常,捕获并记录。
在 asp.net 中也需要小心,因为断言可能会出现在控制台上并冻结请求。