我已经阅读了大量关于如何以及何时使用断言的articles(以及发布在 StackOverflow 上的其他一些类似问题),并且我非常了解它们。但是,我仍然不明白什么样的动机应该驱使我使用 Debug.Assert
而不是抛出一个普通的异常。我的意思是,在 .NET 中,对失败断言的默认响应是“停止世界”并向用户显示一个消息框。尽管可以修改这种行为,但我发现这样做非常烦人和多余,而我可以改为抛出一个合适的异常。这样,我可以在抛出异常之前轻松地将错误写入应用程序的日志,而且,我的应用程序不一定会冻结。
那么,如果有的话,我为什么要使用 Debug.Assert
而不是普通的异常?将断言放在不应该出现的地方可能只会导致各种“不需要的行为”,所以在我看来,通过使用断言而不是抛出异常,我真的没有任何收获。你同意我的观点,还是我在这里遗漏了什么?
注意:我完全理解“理论上”的区别(调试与发布,使用模式等),但正如我所见,我最好抛出异常而不是执行断言。因为如果在生产版本中发现错误,我仍然希望“断言”失败(毕竟,“开销”非常小),所以我最好还是抛出异常。
编辑:在我看来,如果断言失败,则意味着应用程序进入了某种损坏的、意外的状态。那么我为什么要继续执行呢?应用程序是在调试版本还是发布版本上运行并不重要。两者都一样
虽然我同意你的推理是合理的——也就是说,如果一个断言被意外违反,通过抛出来停止执行是有意义的——我个人不会使用异常来代替断言。原因如下:
正如其他人所说,断言应该记录不可能的情况,如果据称不可能的情况发生,则通知开发人员。相比之下,异常为异常、不太可能或错误的情况提供了一种控制流机制,但不是不可能的情况。对我来说,主要区别在于:
应该总是可以生成一个执行给定 throw 语句的测试用例。如果无法生成这样的测试用例,那么您的程序中有一个永远不会执行的代码路径,应该将其作为死代码删除。
永远不可能产生导致断言触发的测试用例。如果一个断言触发,要么代码错误,要么断言错误;无论哪种方式,都需要在代码中进行一些更改。
这就是为什么我不会用异常替换断言。如果断言实际上无法触发,那么用异常替换它意味着您的程序中有不可测试的代码路径。我不喜欢不可测试的代码路径。
断言用于检查程序员对世界的理解。只有当程序员做错了什么时,断言才会失败。例如,永远不要使用断言来检查用户输入。
断言测试“不可能发生”的条件。例外情况是“不应该发生但会发生”的情况。
断言很有用,因为在构建时(甚至运行时)您可以更改它们的行为。例如,通常在发布版本中,甚至不检查断言,因为它们引入了不必要的开销。这也是需要注意的一点:您的测试甚至可能不会被执行。
如果你使用异常而不是断言,你会失去一些价值:
代码更加冗长,因为测试和抛出异常至少需要两行,而断言只有一行。您的测试和抛出代码将始终运行,而断言可以被编译掉。您失去了与其他开发人员的一些沟通,因为断言与检查和抛出的产品代码具有不同的含义。如果您真的在测试编程断言,请使用断言。
更多信息:http://nedbatchelder.com/text/assert.html
编辑:响应您在帖子中所做的编辑/注释:听起来使用异常是正确的,而不是使用断言来处理您要完成的事情的类型。我认为您遇到的心理障碍是您正在考虑例外和断言以实现相同的目的,因此您试图找出哪个“正确”使用。虽然在如何使用断言和异常方面可能存在一些重叠,但不要混淆它们是同一问题的不同解决方案——它们不是。断言和例外都有自己的目的、优势和劣势。
我打算用我自己的话打出一个答案,但这比我想象的更公平:
使用 assert 语句可以是在运行时捕获程序逻辑错误的有效方法,但它们很容易从生产代码中过滤掉。开发完成后,只需在编译期间定义预处理器符号 NDEBUG [禁用所有断言],就可以消除这些编码错误的冗余测试的运行时成本。但是,请务必记住,在生产版本中将省略放置在断言本身中的代码。只有当以下所有条件都成立时,才最好使用断言来测试条件: * 如果代码正确,则条件永远不会为假, * 条件不是那么微不足道,以至于显然总是为真,并且 * 条件是在某种意义上是软件主体的内部。断言几乎不应该用于检测软件正常运行期间出现的情况。例如,通常不应使用断言来检查用户输入中的错误。然而,使用断言来验证调用者是否已经检查了用户的输入可能是有意义的。
基本上,对需要在生产应用程序中捕获/处理的事情使用异常,使用断言来执行对开发有用但在生产中关闭的逻辑检查。
我认为一个(人为的)实际示例可能有助于阐明差异:
(改编自 MoreLinq's Batch extension)
// '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 等人所说,您只断言您期望正确的内容,以防您(开发人员)在其他地方不小心用错了,所以你可以修复你的代码。当您无法控制或无法预测会出现什么时,您基本上会抛出异常,例如用于用户输入,以便任何给它的不良数据都可以适当地响应(例如用户)。
来自 Code Complete 的另一块金块:
“断言是一个函数或宏,如果假设不正确,它会大声抱怨。使用断言来记录代码中的假设并清除意外情况。......“在开发过程中,断言会清除矛盾的假设、意外情况,传递给例程的错误值,等等。”
他接着就应该和不应该断言的内容添加了一些指导方针。
另一方面,例外情况:
“使用异常处理来引起对意外情况的注意。异常情况的处理方式应使其在开发过程中显而易见,并且在生产代码运行时可恢复。”
如果你没有这本书,你应该买它。
默认情况下 Debug.Assert 仅适用于调试版本,因此如果您想在发布版本中捕获任何类型的不良意外行为,您需要使用异常或在项目属性中打开调试常量(在一般不是一个好主意)。
对可能但不应该发生的事情使用断言(如果不可能,你为什么要断言?)。
这听起来不像是使用 Exception
的情况吗?为什么要使用断言而不是 Exception
?
因为应该有代码在你的断言之前被调用,这会阻止断言的参数为假。
通常在您的 Exception
之前没有代码可以保证它不会被抛出。
为什么在 prod 中编译掉 Debug.Assert()
会很好?如果您想在调试中了解它,您不想在产品中了解它吗?
您只需要在开发期间使用它,因为一旦发现 Debug.Assert(false)
情况,您就可以编写代码来保证 Debug.Assert(false)
不会再次发生。开发完成后,假设您已找到 Debug.Assert(false)
情况并修复了它们,则 Debug.Assert()
可以安全地编译掉,因为它们现在是多余的。
假设您是一个相当大的团队的成员,并且有几个人都在使用相同的通用代码库,包括在类上重叠。您可以创建一个由其他几个方法调用的方法,并且为了避免锁争用,您不会向它添加单独的锁,而是“假设”它先前已被调用方法使用特定锁锁定。如,Debug.Assert(RepositoryLock.IsReadLockHeld || RepositoryLock.IsWriteLockHeld);其他开发人员可能会忽略调用方法必须使用锁的注释,但他们不能忽略这一点。