繁体   English   中英

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

[英]When should I use Debug.Assert()?

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

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

我应该在我们的生产代码中开始使用断言吗? 如果是这样,它什么时候使用最合适? 这样做会更有意义吗

Debug.Assert(val != null);

要么

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

Debugging Microsoft .NET 2.0 Applications 中, John Robbins 有很大一部分是关于断言的。 他的主要观点是:

  1. 自由地断言。 你永远不能有太多的断言。
  2. 断言不能取代异常。 例外涵盖了您的代码要求的内容; 断言涵盖了它假设的事情。
  3. 一个写得很好的断言不仅可以告诉你发生了什么和在哪里(就像一个例外),而且可以告诉你为什么。
  4. 异常消息通常很神秘,需要您通过代码向后工作以重新创建导致错误的上下文。 断言可以保留发生错误时程序的状态。
  5. 断言兼作文档,告诉其他开发人员您的代码所依赖的隐含假设。
  6. 断言失败时出现的对话框让您可以将调试器附加到进程中,这样您就可以在堆栈中进行检查,就像在那里放置了一个断点一样。

PS:如果你喜欢 Code Complete,我建议你跟进这本书。 我购买它是为了了解如何使用 WinDBG 和转储文件,但前半部分包含了帮助首先避免错误的技巧。

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

在调用Debug.Assert()之前,您仍然应该抛出异常。 assert 只是确保在您仍在开发时一切都如预期。

FWIW ...我发现我的公共方法倾向于使用if () { throw; } if () { throw; }图案,以确保该方法的调用是否正确。 我的私有方法倾向于使用Debug.Assert()

这个想法是,对于我的私有方法,我是受控制的那个,所以如果我开始使用不正确的参数调用我自己的私有方法之一,那么我已经在某处打破了我自己的假设——我应该永远不会得到进入那个状态。 在生产中,理想情况下,这些私有断言应该是不必要的工作,因为我应该保持我的内部状态有效和一致。 与提供给公共方法的参数相反,公共方法可以在运行时被任何人调用:我仍然需要通过抛出异常来强制执行参数约束。

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

代码完成

8 防御性编程

8.2 断言

断言是在开发过程中使用的代码——通常是一个例程或宏——它允许程序在运行时自我检查。 当断言为真时,这意味着一切都按预期运行。 当它为 false 时,这意味着它在代码中检测到了意外错误。 例如,如果系统假定客户信息文件的记录永远不会超过 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();
}

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

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

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

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

总之,

  • 大量使用 Debug.Assert 来帮助捕获调试版本中的错误。

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

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

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

当我不确定时,我通常更喜欢异常而不是断言。

简而言之

Asserts用于保护和检查契约式设计约束,即确保代码、对象、变量和参数的状态在预期设计的边界和限制内运行。

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

断言有巨大的好处:

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

... 更多详情

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

什么时候应该使用Asserts?

  • 在系统、库 API 或服务中的任何一点,其中函数或类的状态的输入被认为是有效的(例如,当在系统的表示层中对用户输入进行了验证时,业务和数据层类通常假设已经完成了对输入的空检查、范围检查、字符串长度检查等)。
  • 常见的Assert检查包括无效假设会导致空对象取消引用、零除数、数字或日期算术溢出以及一般带外/不是为行为而设计的(例如,如果使用 32 位 int 来模拟人类的年龄, Assert年龄实际上在 0 到 125 左右之间是明智的 - 值 -100 和 10^10 不是设计的)。

.Net 代码契约
在 .Net Stack 中,除了使用Debug.Assert之外,还可以使用代码契约,或者作为使用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());
}

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

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

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

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

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

1)我在这里没有提到的是断言在自动化测试期间可以提供额外概念覆盖 作为一个简单的例子:

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

然而,实际上已经引入了一个微妙的缺陷,但在测试结果中没有发现。 被叫方已成为不确定性在这种情况下,只有恰好提供预期的结果。 或者它可能产生了一个未被注意到的舍入误差。 或者导致在其他地方同样抵消的错误。 或者不仅授予所请求的访问权限,还授予不应授予的其他权限。 等等。

此时,被调用者中包含的 Debug.Assert() 语句加上单元测试驱动的新案例(或边缘案例)可以在测试期间提供非常宝贵的通知,即原作者的假设已失效,代码不应无需额外审查即可发布。 带有单元测试的断言是完美的搭档。

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

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

3)其次,在某些情况下,在发布模式下部署时,您的产品可能对其全部或部分操作没有有用的诊断交互 例如:

假设它是一个嵌入式实时设备。 在遇到格式错误的数据包时抛出异常并重新启动会适得其反。 相反,设备可能会从尽力而为的操作中受益,甚至会在其输出中呈现噪声。 它也可能没有人机界面、日志记录设备,甚至在发布模式下部署时根本无法由人进行物理访问,并且最好通过评估相同的输出来提供错误意识。 在这种情况下,自由的断言和彻底的预发布测试比例外更有价值。

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

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

引用自务实的程序员:从熟练工到大师

保持断言开启

对于由编写编译器和语言环境的人所传播的断言,人们普遍存在误解。 它是这样的:

断言给代码增加了一些开销。 因为它们会检查不应该发生的事情,所以它们只会被代码中的错误触发。 一旦代码经过测试和交付,就不再需要它们,应该关闭它们以使代码运行得更快。 断言是一种调试工具。

这里有两个明显错误的假设。 首先,他们假设测试会发现所有错误。 实际上,对于任何复杂的程序,您都不可能测试您的代码将通过的排列的极小百分比(请参阅无情测试)。

其次,乐观主义者忘记了您的程序运行在一个危险的世界中。 在测试期间,老鼠可能不会啃咬通信电缆,玩游戏的人不会耗尽内存,日志文件不会填满硬盘。 当您的程序在生产环境中运行时,可能会发生这些事情。 您的第一道防线是检查任何可能的错误,第二道防线是使用断言来尝试检测您遗漏的错误。

当您将程序交付到生产环境时关闭断言就像在没有网络的情况下穿越高线,因为您曾经在实践中穿越过它 有巨大的价值,但很难获得人寿保险。

即使您确实有性能问题,也只关闭那些真正打击您的断言

您应该始终使用第二种方法(抛出异常)。

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

您应该使用 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 编译时才有效 - 如果没有编译,最终用户将永远不会看到 assert()。 它仅供开发人员使用。 我经常使用它,有时更容易跟踪错误。

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

在 asp.net 中也需要小心,因为断言可能会出现在控制台上并冻结请求。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM