何时应该使用Debug.Assert()?

258

我已经成为一名专业的软件工程师已有一年时间了,并持有计算机科学学位。我知道在C++和C中有关于断言的知识,但直到最近才意识到它们也存在于C#和.NET中。

我们的生产代码完全没有包含任何断言,我的问题是...

我应该开始在我们的生产代码中使用断言吗?如果是这样,在什么情况下使用最合适?或者更明智的做法是?

    Debug.Assert(val != null, "message");
或者
    if ( val == null )
        throw new exception("message");

4
您设定的二分法是关键。对于异常和断言而言,并不是选其一,而是在编写防御性代码时需要同时考虑两者。关键在于理解何时使用哪种方法。 - Casper Leon Nielsen
7
我曾经读到有人建议,在“我无法合理恢复”的情况下,异常或其他崩溃方法是适当的,而断言则适用于“这永远不应该发生”的情况。但是,有哪些现实情况能够满足后者而不满足前者呢?我的编程背景是Python,断言在生产中一直处于开启状态,因此我从未理解Java/C#的做法,即在生产中关闭某些验证。我真正能看到它的唯一情况是验证很昂贵。 - Mark Amery
3
我个人在公共方法中使用异常,而在私有方法中使用断言。 - Fred
20个回答

272
调试Microsoft .NET 2.0应用程序中,John Robbins有一个重要的小节是关于断言的。他的主要观点是:
  1. 大胆使用Assert。你永远不可能有太多的断言。
  2. 断言不能取代异常。异常覆盖了代码所需的内容;断言则覆盖了它所假定的内容。
  3. 一个写得好的断言可以告诉你不仅发生了什么和在哪里(像异常一样),而且为什么会这样。
  4. 异常消息通常很难理解,需要你通过代码向后追溯以重新创建导致错误的上下文。断言可以保留程序在发生错误时的状态。
  5. 断言可以兼作文档,告诉其他开发人员你的代码依赖于哪些暗示性假设。
  6. 当断言失败时出现的对话框可以让你附加调试器到进程中,这样你就可以像在那里设置了断点一样浏览堆栈。

PS:如果你喜欢《代码大全》,我建议你接着阅读这本书。我买这本书是为了学习如何使用WinDBG和转储文件,但前半部分充满了帮助避免错误的提示。


4
+1 对于简洁而有用的摘要。非常直接应用。但是对我来说,主要缺失的是何时使用Trace.Assert和Debug.Assert。即什么情况下您希望/不希望在生产代码中使用它们。对于Debug.Assert和Trace.Assert的区别,通常情况下,您可以在开发期间使用Debug.Assert进行调试,并在测试完成之后删除它们。 Trace.Assert可以在生产代码中使用,但仅在需要调试时才启用跟踪日志记录。 在生产环境中,Trace.Assert语句不会抛出异常,而是记录错误并继续执行代码。 - Jon Coombs
3
JonCoombs是“Trace.Assert vs. Trace.Assert”打字错误吗? - thelem
3
@thelem 或许Jon指的是Debug.AssertTrace.Assert之间的区别。后者在Release版本和Debug版本中都会被执行。 - DavidRR

100

在代码中想要进行合理性检查以确保不变量的地方,应该随处使用Debug.Assert()。当编译发布版本(即没有DEBUG编译器常量)时,调用Debug.Assert()将被删除,因此它们不会影响性能。

在调用Debug.Assert()之前仍应抛出异常。断言只是确保在开发过程中一切都如预期。


40
如果在调用断言之前仍然会抛出异常,你为什么要加入一个断言呢?或者我误解了你的回答? - Roman Starkov
2
@romkyns,你仍然必须包含它们,因为如果不包含,在发布模式下构建项目时,所有验证/错误检查都将消失。 - Oscar Mederos
32
@Oscar,我认为使用断言的整个目的就是这个吧...好的,那么你把异常放在它们之前 - 那么为什么要把断言放在后面呢? - Roman Starkov
Rory MacLeod的回答中的第二点就是需要断言和异常处理。 - superjos
4
我不得不持有异议:MacLeod的回答中第二点陈述确实需要断言和异常,但不是在同一个地方。在变量上抛出NullRefEx然后立即在其后进行Assert是没有意义的(在这种情况下,Assert方法永远不会显示对话框,这就是Assert的全部意义所在)。MacLeod的意思是,在某些地方你需要一个异常,在其他地方,Assert就足够了。 - David
1
可能会变得混乱,解释别人答案的解释 :) 无论如何,在这些方面我和你意见一致:你需要它们两个,并且不应该在断言之前放置异常。 我不确定“不在同一个地方”的含义。再次拒绝解释,我只会陈述我的想法/偏好:在某些操作开始前,放置一个或多个断言来检查前提条件,或在操作后检查后置条件。 除了断言之外,在它们之后,检查是否出现问题并需要抛出异常。 - superjos

65

就我个人而言,我发现我的公共方法往往使用 if () { throw; } 模式来确保方法的正确调用。而我的私有方法则倾向于使用 Debug.Assert()

这种做法的想法是,在我的私有方法中,我自己掌控着情况,因此如果我开始使用不正确的参数调用自己的一个私有方法,那么我在某个地方打破了自己的假设——我本不应该处于那种状态。在生产环境中,这些私有断言理想情况下应该是不必要的工作,因为我应该在保持内部状态有效和一致的同时操作对象。与被任何人在运行时调用的公共方法所传递的参数相比,我仍然需要通过抛出异常来强制执行参数约束。

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


4
这是一个很清晰的好实践描述,并且对所提出的问题给出了一个非常合理的答案。 - Casper Leon Nielsen

55

来自编码大全维基百科):

8. 防御式编程

8.2 断言

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

断言在大型复杂程序和高可靠性程序中特别有用。它们使程序员能够更快地找出接口假设不匹配、代码修改时产生的错误等问题。

断言通常需要两个参数:一个描述应该为真的假设的布尔表达式和一个消息,用于在假设不为真时显示。

(…)

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


10
如果在生产中遇到包含超过50,000条记录的客户信息文件,会发生什么情况?如果断言被编译出生产代码并且这种情况没有其他处理方式,那么这是否意味着麻烦? - DavidRR
2
@DavidRR 是的,确实如此。但是一旦生产出现问题信号,并且一些不太了解这段代码的开发人员对问题进行调试,断言将会失败,开发人员将立即知道系统未按预期使用。 - Marc
1
链接失效 - "无法访问此网站|cc2e.com响应时间过长。" - Pang

48

使用asserts检查开发人员的假设和exceptions检查环境的假设。


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
这怎么解决问题?有了这个,debug.assert就变得毫无意义了。 - Quibblesome
46
不,它不会在异常抛出的前一刻崩溃。相反,它会在那个时候进入代码。如果你在代码的其他地方有 try/catch,你可能都不会注意到异常! - Mark Ingram
2
+1 我遇到了很多问题,人们只是尝试/捕获异常而没有做任何事情,因此跟踪错误成为了一个问题。 - dance2die
5
我想你可能会有这样的情况,但千万不要捕获通用异常! - Casebash
8
-1 给你的回答,+1 给你为其辩护的评论。这是在特定情况下使用的一个好技巧,但一般情况下似乎不应该用于所有验证。 - Mark Amery
我认为这是对问题的最佳答案之一。我发布了一个答案,基本上做同样的事情,但语法更短,并且具有忽略的能力。 - AlexDev

26
如果您想在生产代码中使用Asserts(即发布版本),可以使用Trace.Assert代替Debug.Assert。但这会增加生产可执行文件的开销。另外,如果您的应用程序在用户界面模式下运行,默认情况下将显示Assertion对话框,这可能会让您的用户感到困惑。您可以通过删除DefaultTraceListener来覆盖此行为:请参阅MSDN中的Trace.Listeners文档。简而言之,
  • 大量使用Debug.Assert来帮助在调试版本中捕获错误。

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

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


2
+1 是为了指出 Debug.Assert 和 Trace.Assert 之间的关键区别,因为 OP 特别询问了生产代码。 - Jon Coombs

25

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

通常当我不确定时,我更喜欢使用异常(exception)而不是断言。


17

简而言之

Asserts(断言)用于保护和检查设计契约约束条件,即确保您的代码、对象、变量和参数状态在您预期的设计边界和限制内操作。

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

断言具有巨大的好处:

为了协助找到用户输入缺失的验证或高级代码中的上游错误。
代码库中的Assert清楚地传达了代码中所做的假设给读者。
在Debug版本中,Assert将在运行时进行检查。
一旦代码经过全面测试,重新构建Release版本的代码将消除验证假设的性能开销(但如果需要,稍后的Debug版本始终会恢复检查) 。

Debug.Assert表达了程序控制范围内其余代码块对状态所做的假设条件。这可能包括提供参数的状态、类实例成员的状态,或者方法调用的返回值是否在其合同/设计范围内。

通常,断言应该使用所有必要信息(堆栈跟踪、崩溃转储等)使线程/进程/程序崩溃,因为它们表示存在错误或未考虑的条件,这些条件没有被设计好(即不要尝试捕获或处理断言失败),唯一可能的例外是当一个断言本身可能比错误更严重时(例如,在飞行管制员需要处理飞机潜水时,他们不希望看到YSOD,尽管是否应该部署调试版本到生产中还有争议...)

何时应该使用Asserts?

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

.Net Code Contracts
在 .Net Stack 中,Code Contracts 可以被用来 作为 Debug.Assert 的补充或替代品。Code Contracts 可以进一步规范状态检查,并且可以帮助检测假设的违规情况,在编译时(或者如果在 IDE 中作为后台检查运行)之后不久就能发现。

Design by Contract (DBC) 检查包括:

  • Contract.Requires - 前置条件
  • Contract.Ensures - 后置条件
  • Invariant - 表达对象在其整个生命周期内状态的假设。
  • Contract.Assumes - 当调用未装饰为 Contract 的方法时,安抚静态检查器。

1
不幸的是,由于微软已停止开发,Code Contracts 已经几乎被淘汰了。 - Mike Lowery
1
我忍不住觉得很有趣,以“简而言之”开头的答案竟然成为了回答问题中最长的答案之一的竞争者。(公平地说,这让我想起了自己倾向于保持冗长模式的习惯。) - David Thompson

10

在我的看法中,大部分情况下都不需要使用Debug.Asserts。

我不喜欢的是它会让调试版本和发布版本功能有所不同。如果一个调试断言失败,但在发布版本中功能正常,这怎么说得过去呢?尤其是当负责该代码的人已经离开公司,没有人知道这部分代码时,你就必须花费时间探究问题是否真的存在。如果确实存在问题,那么为什么一开始不直接“抛出异常”呢?

对我而言,这意味着通过使用Debug.Asserts,你正在将问题推给别人去解决,而应该自己处理问题。如果某个条件应该成立,但实际上不成立,那就“抛出异常”吧。

我猜可能有一些对性能要求非常高的场景,需要优化掉Asserts,它们在那里可能会有用,但是我还没有遇到过这样的场景。


5
您的回答虽然提出了一些常见的问题,例如中断调试会话和可能出现虚假警报等,但它还是有一定价值的。但是您缺少一些细节,并且写下了“优化断言”,这只能基于将抛出异常和使用debug.assert视为相同的思考方式。它们并不相同,它们具有不同的目的和特点,正如您可以在某些得到赞同的答案中看到的那样。 Dw - Casper Leon Nielsen
我不喜欢的是,调试构建与发布构建在功能上有所不同。如果调试断言失败,但在发布中功能正常,这有什么意义呢?在 .NET 中,System.Diagnostics.Trace.Assert() 在发布构建和调试构建中都会执行。 - DavidRR

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接