ChatGPT解决这个技术问题 Extra ChatGPT

Debug.Assert 与异常抛出

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

那么,如果有的话,我为什么要使用 Debug.Assert 而不是普通的异常?将断言放在不应该出现的地方可能只会导致各种“不需要的行为”,所以在我看来,通过使用断言而不是抛出异常,我真的没有任何收获。你同意我的观点,还是我在这里遗漏了什么?

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

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

对于您所说的“如果在生产版本中发现错误,我仍然希望“断言”会失败”,您应该使用异常
性能是唯一的原因。始终对所有内容进行空检查会降低速度,尽管它可能完全不明显。这主要用于不应该发生的情况,例如,您知道您已经在之前的函数中对它进行了空检查,没有必要浪费循环再次检查它。 debug.assert 有效地充当通知您的最后机会单元测试。

E
Eric Lippert

虽然我同意你的推理是合理的——也就是说,如果一个断言被意外违反,通过抛出来停止执行是有意义的——我个人不会使用异常来代替断言。原因如下:

正如其他人所说,断言应该记录不可能的情况,如果据称不可能的情况发生,则通知开发人员。相比之下,异常为异常、不太可能或错误的情况提供了一种控制流机制,但不是不可能的情况。对我来说,主要区别在于:

应该总是可以生成一个执行给定 throw 语句的测试用例。如果无法生成这样的测试用例,那么您的程序中有一个永远不会执行的代码路径,应该将其作为死代码删除。

永远不可能产生导致断言触发的测试用例。如果一个断言触发,要么代码错误,要么断言错误;无论哪种方式,都需要在代码中进行一些更改。

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


断言的问题是它们不在生产版本中。假设条件失败意味着您的程序已进入未定义的行为领域,在这种情况下,负责任的程序必须尽快停止执行(展开堆栈也有些危险,具体取决于您想要获得的严格程度)。是的,断言通常应该是不可能触发的,但是当事情在野外发生时你不知道什么是可能的。您认为不可能的事情可能会在生产中发生,负责任的程序应该检测违反的假设并迅速采取行动。
@kixxx2:这是 C#,因此您可以使用 Trace.Assert 在生产代码中保留断言。您甚至可以使用 app.config 文件将生产断言重定向到文本文件,而不是对最终用户无礼。
@AnorZaken:您的观点说明了异常中的设计缺陷。正如我在其他地方所指出的,例外是 (1) 致命的灾难,(2) 绝不应该发生的愚蠢错误,(3) 使用异常来表示非异常情况的设计失败,或 (4) 意外的外生条件.为什么这四种完全不同的东西都用异常来表示?如果我有我的 druthers,那么愚蠢的“null 被取消引用”异常将根本无法捕获。这是不对的,它应该在它造成更多伤害之前终止你的程序。它们应该更像断言。
@EricLippert那么在理论编程语言中,每个程序员的错误都会导致编译时错误,不需要断言吗?比如说,在 Rust 中,没有人需要断言与内存管理相关的任何事情,因为编译器会确保某些有问题的情况实际上是不可能的?在 C# 中,只要他们按预期使用多态性和泛型,就没有人需要断言正确的类型,因此它是类型安全的吗?所以有人可以说断言是一种工具,可以保护程序员免受正在使用的编程语言无法保护他们的难以推理的情况?
@BrunoZell:这是一个很好的总结,是的。在 C# 中,您可能会编写一个字符串变量不为空的断言,但您永远不会编写一个字符串变量不是盒装整数的断言;你为什么要?程序要么没有编译,要么在赋值尝试时抛出异常。断言表示语言不强制执行的不变量。
N
Ned Batchelder

断言用于检查程序员对世界的理解。只有当程序员做错了什么时,断言才会失败。例如,永远不要使用断言来检查用户输入。

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

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

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

代码更加冗长,因为测试和抛出异常至少需要两行,而断言只有一行。您的测试和抛出代码将始终运行,而断言可以被编译掉。您失去了与其他开发人员的一些沟通,因为断言与检查和抛出的产品代码具有不同的含义。如果您真的在测试编程断言,请使用断言。

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


如果它“不可能发生”,那么为什么要写一个断言。这不是多余的吗?如果它实际上可以发生但不应该发生,那么这与“不应该发生但应该发生”不一样吗?
“不可能发生”是有原因的:只有当程序员在程序的另一部分做错了事情时才会发生。断言是对程序员错误的检查。
@NedBatchelder 不过,当您开发库时,程序员一词有点含糊。那些“不可能的情况”对图书馆用户来说应该是不可能的,但是当图书馆作者犯了错误时,这些“不可能的情况”是可能的吗?
T
Tom Neyland

编辑:响应您在帖子中所做的编辑/注释:听起来使用异常是正确的,而不是使用断言来处理您要完成的事情的类型。我认为您遇到的心理障碍是您正在考虑例外和断言以实现相同的目的,因此您试图找出哪个“正确”使用。虽然在如何使用断言和异常方面可能存在一些重叠,但不要混淆它们是同一问题的不同解决方案——它们不是。断言和例外都有自己的目的、优势和劣势。

我打算用我自己的话打出一个答案,但这比我想象的更公平:

C# Station: Assertions

使用 assert 语句可以是在运行时捕获程序逻辑错误的有效方法,但它们很容易从生产代码中过滤掉。开发完成后,只需在编译期间定义预处理器符号 NDEBUG [禁用所有断言],就可以消除这些编码错误的冗余测试的运行时成本。但是,请务必记住,在生产版本中将省略放置在断言本身中的代码。只有当以下所有条件都成立时,才最好使用断言来测试条件: * 如果代码正确,则条件永远不会为假, * 条件不是那么微不足道,以至于显然总是为真,并且 * 条件是在某种意义上是软件主体的内部。断言几乎不应该用于检测软件正常运行期间出现的情况。例如,通常不应使用断言来检查用户输入中的错误。然而,使用断言来验证调用者是否已经检查了用户的输入可能是有意义的。

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


我意识到这一切。但问题是,您标记为粗体的同一声明也适用于例外情况。所以我看到它的方式,而不是断言,我可以抛出一个异常(因为如果“永远不应该发生的情况”确实发生在已部署的版本上,我仍然想知道它[加上,应用程序可能会进入损坏状态,所以我一个异常是合适的,我可能不想继续正常的执行流程)
断言应该用于不变量;应该使用异常,例如,某些东西不应该为空,但它会(就像方法的参数)。
我想这一切都归结为你想要编码的防御性。
我同意,对于您似乎需要的东西,例外是要走的路。你说你想要:在生产中检测到故障,记录错误信息的能力,以及执行流控制等。这三件事让我觉得你需要做的是抛出一些异常。
C
Community

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

(改编自 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 等人所说,您只断言您期望正确的内容,以防(开发人员)在其他地方不小心用错了,所以你可以修复你的代码。当您无法控制或无法预测会出现什么时,您基本上会抛出异常,例如用于用户输入,以便任何给它的不良数据都可以适当地响应(例如用户)。


你的 3 个断言不是完全多余的吗?他们的参数不可能评估为假。
这就是重点——断言的存在是为了记录不可能的事情。为什么要这么做?因为您可能有类似 ReSharper 的东西,它在 DoSomethingImpl 方法中警告您“您可能在此处取消引用 null”,并且您想告诉它“我知道我在做什么,这永远不能为 null”。这也是一些后来的程序员的指示,他们可能不会立即意识到 DoSomething 和 DoSomethingImpl 之间的联系,尤其是当它们相隔数百行时。
A
Andrew Cowenhoven

来自 Code Complete 的另一块金块:

“断言是一个函数或宏,如果假设不正确,它会大声抱怨。使用断言来记录代码中的假设并清除意外情况。......“在开发过程中,断言会清除矛盾的假设、意外情况,传递给例程的错误值,等等。”

他接着就应该和不应该断言的内容添加了一些指导方针。

另一方面,例外情况:

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

如果你没有这本书,你应该买它。


我看过书,很好。但是..你没有回答我的问题:)
你说得对,我没有回答。我的回答是否定的,我不同意你的看法。断言和例外是如上所述的不同动物以及此处发布的其他一些答案。
M
Mez

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


第一部分是正确的,其余的通常是一个坏主意:断言是假设并且没有验证(如上所述),在发布中启用调试确实是没有选择的。
D
David Klempfner

对可能但不应该发生的事情使用断言(如果不可能,你为什么要断言?)。

这听起来不像是使用 Exception 的情况吗?为什么要使用断言而不是 Exception

因为应该有代码在你的断言之前被调用,这会阻止断言的参数为假。

通常在您的 Exception 之前没有代码可以保证它不会被抛出。

为什么在 prod 中编译掉 Debug.Assert() 会很好?如果您想在调试中了解它,您不想在产品中了解它吗?

您只需要在开发期间使用它,因为一旦发现 Debug.Assert(false) 情况,您就可以编写代码来保证 Debug.Assert(false) 不会再次发生。开发完成后,假设您已找到 Debug.Assert(false) 情况并修复了它们,则 Debug.Assert() 可以安全地编译掉,因为它们现在是多余的。


d
daniel

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