Debug.Assert 与异常抛出
Posted
技术标签:
【中文标题】Debug.Assert 与异常抛出【英文标题】:Debug.Assert vs Exception Throwing 【发布时间】:2010-11-30 20:30:05 【问题描述】:我已经阅读了大量关于如何以及何时使用断言的articles(以及其他一些类似 发布在 *** 上的问题),并且我非常了解它们。但是,我仍然不明白什么样的动机应该驱使我使用Debug.Assert
而不是抛出一个普通的异常。我的意思是,在 .NET 中,对失败断言的默认响应是“停止世界”并向用户显示一个消息框。虽然这种行为可以修改,但我觉得它非常烦人和多余
为此,虽然我可以改为抛出一个合适的异常。这样,我可以在抛出异常之前轻松地将错误写入应用程序的日志,而且我的应用程序不一定会冻结。
那么,如果有的话,我为什么要使用Debug.Assert
而不是普通的异常?将断言放在不应该出现的地方可能只会导致各种“不需要的行为”,所以在我看来,通过使用断言而不是抛出异常,我真的没有任何收获。你同意我的观点,还是我在这里遗漏了什么?
注意:我完全理解“理论上”的区别(调试与发布、使用模式等),但在我看来,最好还是抛出异常而不是抛出异常执行断言。因为如果在生产版本中发现错误,我仍然希望“断言”失败(毕竟,“开销”非常小),所以最好还是抛出异常。
编辑:在我看来,如果断言失败,则意味着应用程序进入了某种损坏的意外状态。那么我为什么要继续执行呢?应用程序是在调试版本还是发布版本上运行并不重要。两者都一样
【问题讨论】:
对于您所说的“如果在生产版本中发现错误,我仍然希望“断言”会失败”,您应该使用异常 性能是唯一的原因。始终对所有内容进行空检查会降低速度,尽管它可能完全不明显。这主要用于不应该发生的情况,例如,您知道您已经在之前的函数中对它进行了空检查,没有必要浪费循环再次检查它。 debug.assert 就像最后一次机会单元测试一样有效地通知您。 【参考方案1】:虽然我同意你的推理是似是而非的——也就是说,如果一个断言被意外违反,通过抛出来停止执行是有意义的——我个人不会使用异常来代替断言。原因如下:
正如其他人所说,断言应该记录不可能的情况,这样如果据称不可能的情况发生,开发人员就会被告知。相比之下,异常为异常、不太可能或错误的情况提供了一种控制流机制,但不是不可能的情况。对我来说,主要区别在于:
应该总是可以生成一个执行给定 throw 语句的测试用例。如果无法生成这样的测试用例,那么您的程序中有一个永远不会执行的代码路径,应该将其作为死代码删除。
永远不可能生成导致断言触发的测试用例。如果一个断言触发,要么代码错误,要么断言错误;无论哪种方式,都需要在代码中进行一些更改。
这就是为什么我不会用异常替换断言的原因。如果断言实际上无法触发,则将其替换为异常意味着您的程序中有不可测试的代码路径。我不喜欢不可测试的代码路径。
【讨论】:
断言的问题是它们在生产版本中不存在。假设条件失败意味着您的程序已进入未定义的行为领域,在这种情况下,负责任的程序必须尽快停止执行(展开堆栈也有些危险,具体取决于您想要获得的严格程度)。是的,断言通常应该不可能触发,但是当事情在野外发生时你不知道什么是可能的。您认为不可能的事情可能会在生产中发生,负责任的程序应该检测违反假设并迅速采取行动。 @kixxx2:这是 C#,因此您可以使用 Trace.Assert 在生产代码中保留断言。您甚至可以使用 app.config 文件将生产断言重定向到文本文件,而不是对最终用户无礼。 @AnorZaken:您的观点说明了异常中的设计缺陷。正如我在其他地方所指出的,例外是 (1) 致命的灾难,(2) 绝不应该发生的愚蠢错误,(3) 使用异常来表示非异常情况的设计失败,或 (4) 意外的外生条件.为什么这四种完全不同的东西都用异常来表示?如果我有我的 druthers,那么愚蠢的“null 被取消引用”异常将不会被捕获根本。它永远不会正确,它应该在您的程序造成更多伤害之前终止您的程序。它们应该更像断言。 @EricLippert 那么在理论上的编程语言中,每个程序员的错误都会导致编译时错误,不需要断言吗?比如说,在 Rust 中,没有人需要断言与内存管理相关的任何事情,因为编译器会确保某些有问题的情况实际上是不可能的?在 C# 中,只要他们按预期使用多态性和泛型,就没有人需要断言正确的类型,因此它是类型安全的吗?所以有人可以说断言是一种工具,可以保护程序员免受正在使用的编程语言无法保护他们的难以推理的情况? @BrunoZell:这是一个很好的总结,是的。在 C# 中,您可能会编写一个字符串变量不为空的断言,但您永远不会编写一个字符串变量不是盒装整数的断言;你为什么要?程序要么没有编译,要么在赋值尝试时抛出异常。断言表示语言不强制执行的不变量。【参考方案2】:断言用于检查程序员对世界的理解。只有当程序员做错了什么时,断言才会失败。例如,永远不要使用断言来检查用户输入。
断言测试“不可能发生”的条件。例外情况是“不应该发生但确实发生”的情况。
断言很有用,因为在构建时(甚至运行时)您可以更改它们的行为。例如,通常在发布版本中,甚至不检查断言,因为它们引入了不必要的开销。这也是需要注意的一点:您的测试甚至可能不会被执行。
如果你使用异常而不是断言,你会失去一些价值:
代码比较冗长,因为测试和抛出异常至少有两行,而断言只有一行。
您的测试和抛出代码将始终运行,而断言可以被编译掉。
您失去了与其他开发人员的一些沟通,因为断言与检查和抛出的产品代码具有不同的含义。如果您真的在测试编程断言,请使用断言。
更多:http://nedbatchelder.com/text/assert.html
【讨论】:
如果它“不可能发生”,那么为什么要写一个断言。这不是多余的吗?如果它实际上可以发生但不应该发生,那么这与“不应该发生但做”不一样吗?这是针对例外的? “不能发生”是有原因的:它只能在程序员在程序的另一部分做错事时发生。断言是对程序员错误的检查。 @NedBatchelder 不过,当您开发库时,programmer 一词有点含糊。那些“不可能的情况”应该对于图书馆用户来说是不可能的,但是当图书馆作者犯了一个错误时,这些“不可能的情况”是正确的吗?【参考方案3】:编辑: 针对您在帖子中所做的编辑/注释: 听起来使用异常是正确的事情,而不是使用断言来完成您要完成的事情的类型。 我认为您遇到的心理障碍是您正在考虑例外和断言以实现相同的目的,因此您试图找出哪个“正确”使用。虽然在如何使用断言和异常方面可能存在一些重叠,但不要混淆它们是同一问题的不同解决方案——它们不是。断言和异常都有自己的目的、优势和劣势。
我打算用我自己的话输入一个答案,但这比我想的更公平:
C# Station: Assertions
断言语句的使用可以是 捕捉程序逻辑的有效方法 运行时的错误,但它们是 很容易从生产中过滤掉 代码。开发完成后, 这些冗余的运行时成本 编码错误的测试可以是 只需通过定义 预处理器符号 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.
几乎不应该使用断言来检测出现的情况 在软件正常运行期间。 例如,通常断言应该 不用于检查错误 用户的输入。然而,它可能使 有意义地使用断言来验证 来电者已经检查了用户的 输入。
基本上,对需要在生产应用程序中捕获/处理的事情使用异常,使用断言来执行对开发有用但在生产中关闭的逻辑检查。
【讨论】:
我意识到这一切。但问题是,您标记为粗体的同一声明也适用于例外情况。所以我看到它的方式,而不是断言,我可以抛出一个异常(因为如果“永远不应该发生的情况”确实发生在已部署的版本上,我仍然想知道它[加上,应用程序可能会进入损坏状态,所以我一个异常是合适的,我可能不想继续正常的执行流程) 断言应该用在不变量上;应该使用异常,例如,某些东西不应该为空,但它会(就像方法的参数)。 我想这一切都取决于你想要编码的防御程度。 我同意,对于你所需要的,例外是要走的路。你说你想要:在生产中检测到故障,记录错误信息的能力,以及执行流控制等。这三件事让我觉得你需要做的就是抛出一些异常。【参考方案4】:我认为一个(人为的)实际示例可能有助于阐明差异:
(改编自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 等人所说,你只断言你期望正确的东西,以防你(开发人员)不小心用错了其他地方,所以你可以修复你的代码。当您无法控制或无法预料会发生什么时,您基本上会抛出异常,e.g.用于用户输入,以便任何给它的不良数据都可以适当地响应(例如用户)。
【讨论】:
你的 3 个断言不是完全多余的吗?他们的参数不可能评估为假。 这就是重点——断言用于记录不可能的事情。为什么要这么做?因为您可能有类似 ReSharper 的东西,它在 DoSomethingImpl 方法中警告您“您可能在此处取消引用 null”,并且您想告诉它“我知道我在做什么,这永远不能为 null”。这也是一些后来的程序员的一个指示,他们可能不会立即意识到 DoSomething 和 DoSomethingImpl 之间的联系,尤其是当它们相隔数百行时。【参考方案5】:来自Code Complete的另一个掘金:
"断言是一个函数或宏 如果一个假设会大声抱怨 不是真的。使用断言来记录 代码中的假设并刷新 出意外情况。 ...
“在开发过程中,断言刷新 排除矛盾的假设, 意外情况,糟糕的价值观 传递给例程,等等。”
他接着就应该和不应该断言的内容添加了一些指导方针。
另一方面,例外情况:
"使用异常处理绘制 注意意外情况。 特殊情况应处理 一种使它们在 开发和恢复时 生产代码正在运行。”
如果你没有这本书,你应该买它。
【讨论】:
我读过这本书,非常好。但是..你没有回答我的问题:) 你说得对,我没有回答。我的回答是否定的,我不同意你的看法。 Assertions 和 Exceptions 是上面提到的不同的动物,这里还有一些其他发布的答案。【参考方案6】:Debug.Assert 默认只能在调试版本中工作,因此如果您想在发布版本中捕获任何类型的不良意外行为,您需要使用异常或在项目属性中打开调试常量(其中一般认为不是一个好主意)。
【讨论】:
第一个部分句子是正确的,其余的通常是一个坏主意:断言是假设并且没有验证(如上所述),在发布中启用调试实际上是没有选择的。【参考方案7】:对是可能但不应该发生的事情使用断言(如果不可能,你为什么要断言?)。
这听起来不像是使用Exception
的情况吗?为什么要使用断言而不是Exception
?
因为应该有代码在你的断言之前被调用,这会阻止断言的参数为假。
通常在您的Exception
之前没有代码可以保证它不会被抛出。
为什么 Debug.Assert()
在 prod 中被编译掉是好事?如果你想在 debug 中了解它,难道你不想在 prod 中了解它吗?
您只需要在开发过程中使用它,因为一旦您发现Debug.Assert(false)
的情况,您就可以编写代码来保证Debug.Assert(false)
不会再次发生。
开发完成后,假设您已找到 Debug.Assert(false)
情况并修复它们,则可以安全地编译掉 Debug.Assert()
,因为它们现在是多余的。
【讨论】:
【参考方案8】:假设您是一个相当大的团队的成员,并且有几个人都在使用相同的通用代码库,包括在类上重叠。 您可以创建一个由其他几种方法调用的方法,并且为了避免锁争用,您不向其添加单独的锁,而是“假设”它先前已被调用方法使用特定锁锁定。 如, Debug.Assert(RepositoryLock.IsReadLockHeld || RepositoryLock.IsWriteLockHeld); 其他开发人员可能会忽略调用方法必须使用锁的注释,但他们不能忽略这一点。
【讨论】:
以上是关于Debug.Assert 与异常抛出的主要内容,如果未能解决你的问题,请参考以下文章
单元测试是不是使 Debug.Assert() 变得不必要?
C#:Release-Build-DLL 中的 Debug.Assert() 似乎由 Debug-Build-Project 激活