Debug.Assert与异常抛出


92

我已经阅读了很多有关如何以及何时使用断言的文章(以及在StackOverflow上发布的其他一些类似问题),并且我对它们很了解。但是,我仍然不明白应该用哪种动机来促使我使用它,Debug.Assert而不是抛出一个明显的例外。我的意思是,在.NET中,对失败的断言的默认响应是“停止运行”并向用户显示消息框。尽管可以修改这种行为,但我发现这样做非常烦人和多余,而我可以抛出一个适当的异常。这样,在抛出异常之前,我可以轻松地将错误写入应用程序日志,此外,我的应用程序不一定会冻结。

那么,为什么我应该使用Debug.Assert而不是普通的例外呢?将断言放置在不应该出现的位置可能会导致各种“有害行为”,因此,根据我的观点,我真的没有通过使用断言而不是抛出异常来获得任何好处。您是否同意我的意见,或者我在这里错过了什么?

注意:我完全理解“理论上”的区别(调试与发行,使用模式等),但是据我所知,我最好抛出一个异常而不是执行一个断言。由于如果在生产版本中发现了错误,我仍然希望“断言”失败(毕竟,“开销”很小),所以我最好抛出一个异常。


编辑:我认为,如果断言失败,则意味着应用程序进入某种损坏的意外状态。那我为什么要继续执行呢?应用程序是在调试版本还是发行版本上运行都没有关系。两者都一样


1
对于您所说的“如果在生产版本中发现错误,我仍然希望“声明”失败”的事情,您应该使用例外
Tom Neyland

1
性能是唯一的原因。始终空检查所有内容可能会降低速度,尽管这可能完全不明显。这主要是针对不应该发生的情况,例如,您知道已经在上一个函数中对它进行了空检查,没有必要浪费时间再次检查它。debug.assert有效地充当了最后机会单元测试来通知您。
轧辊

Answers:


175

尽管我同意您的推理是合理的 -也就是说,如果断言被意外违反,则通过抛出中断执行是有意义的-我个人不会在断言的位置使用异常。原因如下:

正如其他人所说,主张应记录那些情况是不可能的,以这样的方式,如果涉嫌不可能的情况来传递,开发商通知。相比之下,异常为异常,不太可能或错误的情况提供了控制流机制,但并非并非不可能。对我而言,关键区别在于:

  • 总是有可能产生一个执行给定throw语句的测试用例。如果不可能产生这样的测试用例,则您的程序中有一条代码路径永远不会执行,应将其作为无效代码删除。

  • 绝不可能产生导致断言触发的测试用例。如果断言被触发,则代码是错误的,或者断言是错误的。无论哪种方式,都需要在代码中进行某些更改。

这就是为什么我不会用异常代替断言的原因。如果断言实际上不能触发,则用异常替换它意味着您的程序中具有不可测试的代码路径。我不喜欢无法测试的代码路径。


16
断言的问题在于它们不在生产构建中。如果没有达到假定的条件,则意味着您的程序已进入未定义的行为范围,在这种情况下,负责任的程序必须尽快停止执行(展开堆栈也很危险,具体取决于您希望获得的严格程度)。是的,断言通常应该是不可能的火灾,但你不知道什么是可能的,当事情在野外出门。您认为不可能的事情可能会在生产中发生,负责任的程序应发现违反的假设并立即采取行动。
kizzx2 2010年

2
@ kizzx2:好的,那么您每行生产代码中写了多少个不可能的异常?
埃里克·利珀特

4
@ kixxx2:这是C#,因此您可以使用Trace.Assert在生产代码中保留断言。您甚至可以使用app.config文件将生产断言重定向到文本文件,而不是无视最终用户。
HTTP 410

3
@AnorZaken:您的观点说明了异常中的设计缺陷。正如我在其他地方提到的,异常是(1)致命灾难,(2)永远都不会发生的头脑错误,(3)使用异常来表示非异常情况的设计失败,或者(4)意外的外生条件。为什么这四个完全不同的事物都用异常表示?如果我有我的德鲁特人,那么头脑笨拙的“ null被取消引用”根本不会被捕获。这永远是不对的,它应该在损害更大之前终止程序。他们应该更像断言。
埃里克·利珀特

2
@Backwards_Dave:错误的断言是不好的,不是真正的断言。断言使您可以运行您不想在生产中运行的昂贵的验证检查。如果在生产中违反了断言,最终用户应如何处理?
埃里克·利珀特

17

断言用于检查程序员对世界的理解。仅当程序员做错了什么时,断言才应该失败。例如,切勿使用断言来检查用户输入。

断言测试“不可能发生”的条件。例外情况是“不应发生但会发生”。

断言非常有用,因为在构建时(甚至在运行时),您可以更改其行为。例如,在发行版本中,断言甚至不会被检查,因为它们会引入不必要的开销。这也是要注意的一点:您的测试甚至可能不会执行。

如果使用异常而不是断言,则会失去一些价值:

  1. 代码更加冗长,因为测试和引发异常至少需要两行,而断言仅是一行。

  2. 您的测试和引发代码将始终运行,而断言可以被编译掉。

  3. 您失去了与其他开发人员的交流,因为断言与检查和抛出的产品代码的含义不同。如果您确实在测试编程断言,请使用断言。

此处更多信息:http : //nedbatchelder.com/text/assert.html


如果“不可能发生”,那么为什么要写一个断言。那不是多余的吗?如果它确实可以发生但不应该发生,那么这与例外是不是“不应该发生但应该这样做”一样吗?
David Klempfner '18年

2
用引号引起来“不可能发生”是有原因的:只有当程序员在程序的另一部分做错了什么时,它才会发生。断言是对程序员错误的检查。
Ned Batchelder

@NedBatchelder但是,当您开发库时,术语程序员有点模棱两可。那些“不可能的情况” 应该是图书馆用户不可能的,但是当图书馆作者犯了一个错误时是可能的吗?
布鲁诺·泽尔

12

编辑: 响应您在帖子中所做的编辑/注释:听起来,对于要尝试完成的事情类型,使用异常是优于使用断言的正确选择。我认为您遇到的心理绊脚石是您正在考虑使用异常和断言来实现相同的目的,因此您正在尝试找出使用哪种“正确”方法。尽管断言和异常的使用方式可能有所重叠,但请不要混淆它们对于同一问题的不同解决方案,而不能混淆。断言和异常各有其目的,优点和缺点。

我本来要用自己的话来输入答案,但这确实比我想象的要好得多:

C#站:断言

使用assert语句可以是在运行时捕获程序逻辑错误的有效方法,但是很容易将它们从生产代码中过滤掉。一旦开发完成,只需在编译过程中定义预处理器符号NDEBUG [可禁用所有断言],就可以消除这些冗余的编码错误测试的运行时成本。但是,请务必记住,放置在assert中的代码在生产版本中将被省略。

仅当以下所有条件成立时,才最好使用断言来测试条件:

* the condition should never be false if the code is correct,
* the condition is not so trivial so as to obviously be always true, and
* the condition is in some sense internal to a body of software.

断言几乎绝不应用于检测软件正常运行期间出现的情况。 例如,通常不应使用断言来检查用户输入中的错误。但是,使用断言来验证呼叫者已检查用户输入可能是有意义的。

基本上,将异常用于生产应用程序中需要捕获/处理的事物,使用断言执行逻辑检查,这将对开发有用,但在生产中已关闭。


我意识到了所有这些。但事实是,标记为粗体的同一条语句也适用于例外情况。因此,我看待它的方式,而不是声明,我可以抛出一个异常(因为如果“永远不应该发生的情况”确实发生在已部署的版本上,那么我仍然想知道它[加上应用程序可能会进入损坏状态,所以我适合一个例外,我可能不想继续正常的执行流程)

1
断言应用于不变量;例如,当某些东西不应该为null而是应该为null时(例如方法的参数),应使用异常。
Ed S.

我想这全都归结为您希望如何编码。
Ned Batchelder

我同意,对于您似乎需要的东西,例外是解决之道。您说了自己想要的:生产中检测到的故障,记录有关错误的信息的能力以及执行流控制等。这三件事使我认为您需要做的就是抛出一些异常。
汤姆·内兰德

7

我认为(人为的)实际示例可能有助于阐明差异:

(改编自MoreLinq的Batch扩展

// 'public facing' method
public int DoSomething(List<string> stuff, object doohickey, int limit) {

    // validate user input and report problems externally with exceptions

    if(stuff == null) throw new ArgumentNullException("stuff");
    if(doohickey == null) throw new ArgumentNullException("doohickey");
    if(limit <= 0) throw new ArgumentOutOfRangeException("limit", limit, "Should be > 0");

    return DoSomethingImpl(stuff, doohickey, limit);
}

// 'developer only' method
private static int DoSomethingImpl(List<string> stuff, object doohickey, int limit) {

    // validate input that should only come from other programming methods
    // which we have control over (e.g. we already validated user input in
    // the calling method above), so anything using this method shouldn't
    // need to report problems externally, and compilation mode can remove
    // this "unnecessary" check from production

    Debug.Assert(stuff != null);
    Debug.Assert(doohickey != null);
    Debug.Assert(limit > 0);

    /* now do the actual work... */
}

因此,正如Eric Lippert等人所说的那样,您只能断言您期望正确的内容,以防万一(开发人员)在其他地方不小心使用了它,以便您可以修复代码。当您无法控制或无法预料其中的内容时(例如,对于用户输入),您基本上会抛出异常,从而使任何给它的坏数据都可以适当地响应(例如,用户)。


您的3个断言不是完全多余吗?他们的参数不可能为假。
David Klempfner '18年

1
这就是重点-断言可以记录不可能的内容。为什么要这么做?因为您可能有类似ReSharper的东西,它在DoSomethingImpl方法内部警告您“您可能在此处取消引用null”,并且您想告诉它“我知道我在做什么,所以永远不能为null”。这对于以后的程序员也可能是一个指示,他们可能不会立即意识到DoSomething和DoSomethingImpl之间的联系,特别是如果它们相距数百行。
Marcel Popescu

4

Code Complete的另一个块:

“断言是一个函数或宏,如果假设不成立,则会大声抱怨。使用断言来记录代码中的假设并清除意外情况。...

“在开发过程中,断言消除了矛盾的假设,意想不到的条件,传递给例程的错误值等。”

他接着就应主张和不应主张的内容添加一些准则。

另一方面,例外:

“使用异常处理来引起人们对意外情况的注意。应对异常情况的方式应使其在开发期间显而易见,并在生产代码运行时可恢复。”

如果您没有这本书,则应该购买。


2
我读过这本书,太好了。但是..您没有回答我的问题:)

你是对的,我没有回答。我的回答是不,我不同意你的看法。断言和异常是上文所述的不同动物,此处还列出了其他一些答案。
安德鲁·考文霍芬(09年

0

默认情况下,Debug.Assert仅在调试版本中起作用,因此,如果要在发行版本中捕获任何不良的不良行为,则需要使用异常或在项目属性中打开调试常量(在一般不是一个好主意)。


第一部分是正确的,其余部分通常不是一个好主意:断言是假设,没有验证(如上所述),因此在版本中启用调试实际上是没有选择的。
Marc Wittke

0

使用断言的事情,现在可能的,但不应该发生(如果它是不可能的,为什么你会放一个说法对吗?)。

这听起来不像是使用案例Exception吗?为什么要使用断言而不是Exception

因为应该有在断言之前调用的代码,这将停止断言的参数为false。

通常,在您之前没有代码Exception可以保证不会将其引发。

为什么Debug.Assert()在产品中编译好了?如果您想在调试中了解它,您是否不想在产品中了解它?

您只需要在开发过程中就可以使用它,因为一旦发现Debug.Assert(false)情况,您就可以编写代码以确保Debug.Assert(false)不会再发生这种情况。一旦开发完成,假设您已找到Debug.Assert(false)情况并进行了修复,则Debug.Assert()可以安全地将其编译掉,因为它们现在是多余的。


0

假设您是一个相当大的团队的成员,并且有几个人都在同一通用代码库上工作,包括在类上重叠。您可以创建一个由其他几种方法调用的方法,并且为了避免锁争用,您无需向其添加单独的锁,而是“假定”该方法先前已由具有特定锁的调用方法锁定。例如,Debug.Assert(RepositoryLock.IsReadLockHeld || RepositoryLock.IsWriteLockHeld); 其他开发人员可能会忽略注释,即调用方法必须使用锁,但他们不能忽略此注释。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.