我什么时候应该使用Debug.Assert()?

时间:2008-09-24 18:57:38

标签: language-agnostic exception testing assertions defensive-programming

我已经是一名专业的软件工程师,已经有一年的时间了,毕业于CS学位。我已经知道C ++和C中有一段时间断言,但直到最近才知道它们在C#和.NET中存在。

我们的生产代码不包含任何断言,我的问题是这个......

我应该在生产代码中开始使用Asserts吗?如果是这样,它的使用何时最合适?

更有意义吗?
Debug.Assert(val != null);

if ( val == null )
    throw new exception();

20 个答案:

答案 0 :(得分:221)

Debugging Microsoft .NET 2.0 Applications约翰罗宾斯有一个关于断言的重要部分。他的要点是:

  1. 自由地断言。你永远不会有太多的断言。
  2. 断言不会替换异常。例外涵盖了您的代码所要求的内容;断言涵盖了它所假设的事物。
  3. 一个写得很好的断言不仅可以告诉你发生了什么以及在哪里(例如异常),而是为什么。
  4. 异常消息通常很神秘,要求您通过代码向后工作以重新创建导致错误的上下文。断言可以在错误发生时保留程序的状态。
  5. 断言加倍作为文档,告诉其他开发人员你的代码所依赖的隐含假设。
  6. 断言失败时出现的对话框允许您将调试器附加到进程,因此您可以在堆栈周围查看,就像在那里放置断点一样。
  7. PS:如果您喜欢Code Complete,我建议您按照本书进行操作。我买它是为了学习使用WinDBG和转储文件,但是上半部分包含了一些提示,以帮助避免错误。

答案 1 :(得分:84)

Debug.Assert()放在您想要进行健全性检查的代码中的任何位置以确保不变量。当您编译发布版本(即,没有 DEBUG 编译器常量)时,对Debug.Assert()的调用将被删除,因此它们不会影响性能。

在调用Debug.Assert()之前,您仍应抛出异常。断言只是确保在你还在开发的时候,一切都如预期的那样。

答案 2 :(得分:50)

来自Code Complete

  

8防御性编程

     

8.2断言

     

断言是在开发过程中使用的代码 - 通常是例程   或宏 - 允许程序在运行时自行检查。当一个   断言是真的,这意味着一切都按预期运作。   当它为假时,这意味着它已检测到意外错误   码。例如,如果系统假定客户信息   该程序可能永远不会有超过50,000条记录   包含一个断言记录数量少于或等于的断言   到50,000。只要记录数小于或等于   50,000,断言将是沉默。如果遇到的话多于   但是,它会大声“断言”有一条记录   程序中的错误。

     

断言在大而复杂的程序中特别有用   在高可靠性计划中。它们使程序员能够更快地完成任务   清除不匹配的接口假设,错误在何时蔓延   代码被修改,等等。

     

断言通常需要两个参数:布尔表达式   描述了应该是真实的假设和消息   如果不是则显示。

     

(...)

     

通常,您不希望用户看到断言消息   生产代码;断言主要用于开发期间   和维护。断言通常编译到代码中   开发时间并编译出生产代码。中   发展,断言消除了矛盾的假设,   意外情况,传递给例程的错误值,等等。   在生产过程中,它们是从代码中编译出来的   断言不会降低系统性能。

答案 3 :(得分:45)

FWIW ...我发现我的公共方法倾向于使用if () { throw; }模式来确保正确调用该方法。我的私有方法倾向于使用Debug.Assert()

我的想法是,使用我的私有方法,我是受控制的,所以如果我开始使用不正确的参数调用我自己的私有方法之一,那么我已经在某个地方打破了我自己的假设 - 我应该从来没有进入那个州。在生产中,这些私有断言理想上应该是不必要的工作,因为我应该保持我的内部状态有效和一致。与公共方法的参数对比,任何人都可以在运行时调用这些参数:我仍然需要通过抛出异常来强制执行参数约束。

此外,如果某些内容在运行时不起作用(网络错误,数据访问错误,从第三方服务检索到的错误数据等),我的私有方法仍然可以抛出异常。我的断言只是为了确保我没有打破我自己关于对象状态的内部假设。

答案 4 :(得分:40)

使用断言检查开发人员的假设和例外,以检查环境假设。

答案 5 :(得分:32)

如果我是你,我会这样做:

Debug.Assert(val != null);
if ( val == null )
    throw new exception();

或者避免重复检查条件

if ( val == null )
{
    Debug.Assert(false,"breakpoint if val== null");
    throw new exception();
}

答案 6 :(得分:23)

如果您希望在生产代码中使用Asserts(即Release版本),则可以使用Trace.Assert而不是Debug.Assert。

这当然会增加生产可执行文件的开销。

此外,如果您的应用程序在用户界面模式下运行,默认情况下将显示“断言”对话框,这可能会让您的用户感到有些不安。

您可以通过删除DefaultTraceListener来覆盖此行为:请查看MSDN中Trace.Listeners的文档。

总之,

  • 自由地使用Debug.Assert来帮助捕获Debug版本中的错误。

  • 如果在用户界面模式下使用Trace.Assert,您可能希望删除DefaultTraceListener以避免让用户感到不安。

  • 如果您正在测试的条件是您的应用无法处理的情况,那么最好不要抛出异常,以确保执行不会继续。请注意,用户可以选择忽略断言。

答案 7 :(得分:21)

断言用于捕获程序员(您的)错误,而不是用户错误。只有在用户无法触发断言时才应使用它们。例如,如果您正在编写API,则不应使用断言来检查API用户可以调用的任何方法中的参数是否为空。但它可以在一个私有方法中使用,而不是作为API的一部分公开,以断言你的代码在它不应该传递时永远不会传递null参数。

当我不确定时,我通常偏爱断言而不是断言。

答案 8 :(得分:10)

简而言之

Asserts用于警卫和检查按合同约束的设计,即:

  • Asserts应仅适用于Debug和非生产版本。在版本构建中,编译器通常会忽略断言。
  • Asserts可以检查系统控制中的错误/意外情况
  • Asserts不是用户输入或业务规则的第一行验证机制
  • Asserts应该用于检测意外的环境条件(不受代码控制),例如内存不足,网络故障,数据库故障等。虽然很少见,但这些条件是可以预期的(并且您的应用程序代码无法解决硬件故障或资源耗尽等问题)。通常,会抛出异常 - 您的应用程序可以采取纠正措施(例如,重试数据库或网络操作,尝试释放缓存的内存),或者如果无法处理异常,则优先中止。
  • 失败的断言对您的系统来说应该是致命的 - 即与异常不同,不要尝试捕获或处理失败的Asserts - 您的代码在意外的区域内运行。堆栈跟踪和崩溃转储可用于确定出错的地方。

断言有很大的好处:

  • 帮助查找缺少用户输入的验证或更高级别代码的上游错误。
  • 代码库中的断言清楚地将代码中的假设传达给读者
  • 将在运行时在Debug版本中检查断言。
  • 一旦对代码进行了详尽的测试,将代码重建为Release将消除验证假设的性能开销(但是如果需要的话,以后的Debug构建将始终恢复检查的好处)。

...更多详情

Debug.Assert表示在程序控制范围内由代码块的其余部分假定状态的条件。这可以包括所提供参数的状态,类实例的成员状态,或者方法调用的返回处于其缩减/设计范围内。 通常,断言应该使线程/进程/程序崩溃所有必要的信息(堆栈跟踪,崩溃转储等),因为它们表明存在未设计的错误或未考虑的情况(即不要尝试捕获或处理断言失败),有一个可能的例外,当断言本身可能造成比bug更多的损害时(例如,当飞机进入潜艇时,空中交通管制员不会想要YSOD,尽管调试构建是否应该部署到生产...)

什么时候应该使用Asserts?   - 在系统或库API或服务中的任何位置,其中假定函数或类的状态的输入有效(例如,当已经对系统的表示层中的用户输入进行验证时,业务和数据层类通常假设已经完成了对输入的空检查,范围检查,字符串长度检查等)。   - 公共Assert检查包括无效假设将导致空对象解除引用,零除数,数值或日期算术溢出以及一般带外/未针对行为设计的位置(例如,如果使用32位int)为了模拟人类的年龄,Assert谨慎的是,年龄实际上介于0到125左右 - -100和10 ^ 10的值不是为了设计的。)

.Net Code Contracts
在.Net堆栈中,Code Contracts可以in addition to, or as an alternative to使用Debug.Assert。代码约定可以进一步形式化状态检查,并且可以在编译时(或之后不久,如果在IDE中作为后台检查运行)帮助检测违反假设的情况。

按合同设计(DBC)检查包括:

  • Contract.Requires - 合同规定的前提条件
  • Contract.Ensures - 签约PostConditions
  • Invariant - 表达关于对象在其生命周期中所有点的状态的假设。
  • Contract.Assumes - 在调用非合约修饰方法时安抚静态检查程序。

答案 9 :(得分:10)

我的书中大部分都没有。 在绝大多数情况下,如果你想检查一切是否理智,那么如果不是,就扔掉。

我不喜欢的是它使调试版本在功能上与发布版本不同。如果调试断言失败但功能在发布中有效,那么这有什么意义呢?当断言器长期离开公司并且没有人知道代码的那部分时,它会更好。然后,您必须花费一些时间来探索问题,看看它是否真的是一个问题。如果这是一个问题那么为什么不是第一个投掷的人呢?

对我来说,这表明使用Debug.Asserts你将问题推迟给别人,自己处理问题。如果事情应该是这样的话,那就不会抛出。

我想有可能是性能关键的情况,你想要优化你的断言,它们在那里很有用,但是我还没有遇到这种情况。

答案 10 :(得分:7)

根据IDesign Standard,你应该

  

断言每一个假设。平均而言,每五行就是一个断言。

using System.Diagnostics;

object GetObject()
{...}

object someObject = GetObject();
Debug.Assert(someObject != null);

作为免责声明,我应该提一下,我发现实施这个IRL并不实用。但这是他们的标准。

答案 11 :(得分:6)

所有断言都应该是可以优化的代码:

Debug.Assert(true);

因为它正在检查你已经假设的东西是真的。 E.g:

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());
}

在上文中,null参数有三种不同的方法。第一个接受它是允许的(它什么都不做)。第二个抛出调用代码处理的异常(或不抛出,导致错误消息)。第三种假设它不可能发生,并声称它是如此。

在第一种情况下,没有问题。

在第二种情况下,调用代码存在问题 - 它不应该使用null调用GetFirstAndConsume,因此它会返回异常。

在第三种情况下,这段代码存在问题,因为它应该在调用en != null之前已经被检查过,所以它不是真的是一个错误。换句话说,它应该是理论上可以优化为Debug.Assert(true)的代码,sicne en != null应始终为true

答案 12 :(得分:6)

仅在您希望为发布版本删除检查的情况下使用断言。请记住,如果不在调试模式下编译,则不会触发断言。

给出check-for-null示例,如果这是在仅内部API中,我可能会使用断言。如果它在公共API中,我肯定会使用显式检查并抛出。

答案 13 :(得分:4)

引自The Pragmatic Programmer: From Journeyman to Master

  

保持断言

     

关于断言存在一个常见的误解,由...颁布   编写编译器和语言环境的人。它去了   像这样的东西:

     

断言会给代码增加一些开销。因为他们检查的东西   这应该永远不会发生,它们只会被一个错误触发   码。代码经过测试和发货后,就不再存在了   需要,应该关闭,以使代码运行得更快。   断言是一个调试工具。

     

这里有两个明显错误的假设。首先,他们认为   测试找到所有的错误。实际上,对于任何复杂的程序你   甚至不太可能测试排列的微小百分比   你的代码将被接通(参见无情测试)。

     

其次,乐观主义者忘记了你的程序运行在   危险的世界。在测试过程中,老鼠可能不会啃咬   通信电缆,有人玩游戏不会耗尽内存,而且   日志文件不会填满硬盘。这些事情可能发生在   您的程序在生产环境中运行。你的第一行   防御正在检查任何可能的错误,而你的第二个正在使用   断言试图发现你错过的那些。

     

在将程序投放到生产环境时关闭断言   就像你曾经做过的那样,在没有网的情况下穿过高线   在实践中。有巨大的价值,但很难获得生命   险。

     

即使您确实遇到了性能问题,也请仅关闭这些问题   真正打击你的断言

答案 14 :(得分:4)

我想我会再增加四个案例,其中Debug.Assert可能是正确的选择。

1)我在这里没有提到的是自动化测试期间Asserts可以提供的额外概念性覆盖。举个简单的例子:

如果某个更高级别的调用者被认为已扩展代码范围以处理其他方案的作者修改,理想情况下(!)他们将编写单元测试来覆盖这个新条件。这可能是完全集成的代码似乎工作正常。

然而,实际上已经引入了一个微妙的缺陷,但在测试结果中没有检测到。在这种情况下,被调用者变得不确定,只有发生才能提供预期结果。或许它已经产生了一个未被注意到的舍入错误。或导致错误在其他地方平均抵消。或者不仅授予所请求的访问权限,还授予不应授予的其他权限。等

此时,被调用者中包含的Debug.Assert()语句与单元测试驱动的新案例(或边缘案例)一起可以在测试期间提供宝贵的通知,原始作者的假设已经失效,并且如果没有额外的审核,则不应发布代码。单元测试的断言是完美的合作伙伴。

2)此外,某些测试编写起来很简单,但考虑到最初的假设,则成本高且不必要。例如:

如果只能从某个安全入口点访问对象,是否应该从每个对象方法对网络权限数据库进行额外查询以确保调用者具有权限?当然不是。也许理想的解决方案包括缓存或其他一些功能扩展,但设计并不需要它。当对象被附加到不安全的入口点时,Debug.Assert()将立即显示。

3)接下来,在某些情况下,产品在发布模式下部署时,可能无法对其全部或部分操作进行有用的诊断交互。例如:

假设它是嵌入式实时设备。抛出异常并在遇到格式错误的数据包时重新启动会适得其反。相反,该设备可以受益于尽力而为的操作,甚至可以在其输出中产生噪声。它也可能没有人机界面,记录设备,甚至在发布模式下部署时甚至可以被人物物理访问,并且通过评估相同的输出最好地提供对错误的认识。在这种情况下,自由断言和彻底的预释放测试比例外更有价值。

4)最后,某些测试是不必要的,因为被调用者被认为非常可靠。在大多数情况下,代码越多,可重复使用的代码越多,就越努力使其可靠。因此,来自调用者的意外参数的异常是常见的,但是来自被调用者的意外结果是Assert。例如:

如果核心String.Find操作声明它将在找不到搜索条件时返回-1,则您可以安全地执行一个操作而不是三个操作。但是,如果它实际返回-2,您可能没有合理的行动方案。将单纯的计算替换为单独测试-1值的计算是没有用的,而在大多数发布环境中使用测试来确保核心库按预期运行时不合理。在这种情况下,断言是理想的。

答案 15 :(得分:2)

你应该总是使用第二种方法(抛出异常)。

此外,如果你正在进行生产(并且有一个发布版本),最好抛出一个异常(让应用程序在最坏的情况下崩溃),而不是处理无效值,并可能破坏客户的数据(可能要花费数千美元。)

答案 16 :(得分:0)

您应该使用Debug.Assert来测试程序中的逻辑错误。编译器只能告诉您语法错误。因此,您应该定义使用Assert语句来测试逻辑错误。就像测试销售汽车的程序一样,只有蓝色的宝马应该获得15%的折扣。编译器可以告诉你关于你的程序在执行此操作时是否在逻辑上是正确的,但是断言语句可以。

答案 17 :(得分:0)

我已经在这里阅读了答案,我认为我应该添加一个重要的区别。使用断言的方式有两种截然不同的方式。一个是作为&#34的临时开发人员快捷方式;这不应该发生,所以如果它确实让我知道,那么我可以决定做什么&#34;有点像条件断点,对于你的情况程序能够继续。另一种方法是在代码中对有效程序状态进行假设。

在第一种情况下,断言甚至不需要在最终代码中。您应该在开发期间使用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>

答案 18 :(得分:-1)

我不会在生产代码中使用它们。抛出异常,捕获并记录。

在asp.net中也需要小心,因为断言可以显示在控制台上并冻结请求。

答案 19 :(得分:-1)

我不知道它在C#和.NET中是怎么回事,但是在C中,assert()只有在使用-DDEBUG编译时才能工作 - 如果没有编译,enduser将永远不会看到assert()。它仅适用于开发人员。我经常使用它,有时更容易跟踪错误。