【问题标题】:Design by contract using assertions or exceptions? [closed]使用断言或异常按合同设计? [关闭]
【发布时间】:2008-09-22 19:57:57
【问题描述】:

当通过契约编程一个函数或方法时,首先检查其先决条件是否得到满足,然后再开始履行其职责,对吧?进行这些检查的两种最突出的方法是assertexception

  1. 断言仅在调试模式下失败。确保(单元)测试所有单独的合同先决条件以查看它们是否真的失败是至关重要的。
  2. 异常在调试和发布模式下失败。这样做的好处是测试的调试行为与发布行为相同,但会导致运行时性能损失。

你觉得哪个更好?

查看相关问题here

【问题讨论】:

  • 按合同设计背后的全部要点是,您不需要(并且可以说不应该)在运行时验证先决条件。您在将输入传递到带有前提条件的方法之前验证输入,这就是您尊重合同的您的结束的方式。如果输入无效或违反了您的合同,程序通常会在其正常的操作过程中失败(您想要的)。
  • 好问题,但我认为你真的应该改变接受的答案(投票也显示)!
  • 永远以后,我知道,但是这个问题真的应该有 c++ 标签吗?我一直在寻找这个答案,以在另一种语言(Delpih)中使用,我无法想象任何具有不遵循相同规则的异常和断言的语言。 (仍在学习 Stack Overflow 指南。)
  • in this response 给出的非常简洁的响应:“换句话说,异常处理应用程序的健壮性,而断言处理其正确性。”

标签: exception assert design-by-contract


【解决方案1】:

经验法则是,当你试图捕捉自己的错误时,你应该使用断言,而当你试图捕捉别人的错误时,你应该使用异常。换句话说,您应该使用异常来检查公共 API 函数的先决条件,以及每当您获得系统外部的任何数据时。您应该对系统内部的功能或数据使用断言。

【讨论】:

  • 序列化/反序列化位于不同的模块/应用程序中并最终不同步怎么办?我的意思是在读者方面,如果我试图以错误的方式阅读内容,那总是我的错误,所以我倾向于使用断言,但另一方面,我有外部数据,最终可能会在不通知的情况下更改格式。
  • 如果数据是外部的,那么你应该使用异常。在这种特殊情况下,您可能还应该捕获这些异常,并以某种合理的方式处理它们,而不是让您的程序死掉。另外,我的回答是经验法则,而不是自然法则。 :) 所以你必须单独考虑每个案例。
  • 如果你的函数 f(int* x) 中包含一行 x->len ,那么 f(v) 其中 v 被证明为 null 肯定会崩溃。此外,如果在更早的时候 v 被证明为空,而 f(v) 被证明是被调用的,那么你就有一个逻辑矛盾。这与 a/b 相同,其中 b 最终被证明为 0。理想情况下,此类代码应该无法编译。关闭假设检查是完全愚蠢的,除非问题是检查的成本,因为它掩盖了违反假设的位置。它至少必须被记录。无论如何,您应该有一个在崩溃时重新启动的设计。
【解决方案2】:

在发布版本中禁用断言就像在说“我在发布版本中永远不会有任何问题”,但通常情况并非如此。所以断言不应该在发布版本中被禁用。但是您也不希望发布版本在发生错误时崩溃,是吗?

所以要使用异常并好好利用它们。使用良好、可靠的异常层次结构并确保您可以捕获并且可以在调试器中捕获异常抛出以捕获它,并且在发布模式下您可以补偿错误而不是直接崩溃。这是更安全的方式。

【讨论】:

  • 断言至少在检查正确性无效或无法正确实施的情况下很有用。
  • 断言的重点不是纠正错误,而是提醒程序员。在发布版本中保持启用它们是无用的,因为那个原因:通过断言触发你会得到什么?开发人员将无法进入并调试它。断言是一种调试辅助,它们不是异常的替代品(异常也不是断言的替代品)。异常会警告程序出现错误情况。断言提醒开发者。
  • 但是当内部数据在修复之后被破坏时,应该使用断言——如果断言触发,你不能对程序的状态做任何假设,因为它意味着某些东西是/错误的/。如果一个断言已经失效,你就不能假设任何数据都是有效的。这就是为什么发布版本应该断言 - 不是告诉程序员问题出在哪里,而是让程序可以关闭并且不会冒更大问题的风险。当数据可以信任时,程序应该尽其所能促进以后的恢复。
  • @jalf,虽然您不能在发布版本中将挂钩放入调试器,但您可以利用日志记录,以便开发人员看到与您的断言失败相关的信息。在本文档 (martinfowler.com/ieeeSoftware/failFast.pdf) 中,Jim Shore 指出:“请记住,客户站点上发生的错误通过了您的测试过程。您可能无法重现它。这些错误是最难发现的,并且一个恰当的断言来解释问题可以节省你几天的精力。”
  • 就我个人而言,我更喜欢通过合同方法进行设计的断言。异常是防御性的,并且正在函数内部进行参数检查。此外,dbc 前提条件不会说“如果您使用超出工作范围的值,我将无法工作”,而是“我不能保证提供正确的答案,但我仍然可以这样做”。断言向开发人员提供了他们正在调用违反条件的函数的反馈,但如果他们觉得自己知道得更多,请不要阻止他们使用它。违规可能会导致异常发生,但我认为这是另一回事。
【解决方案3】:

我遵循的原则是:如果可以通过编码实际避免某种情况,则使用断言。否则使用异常。

断言是为了确保合同得到遵守。合同必须是公平的,因此客户必须能够确保其遵守。例如,您可以在合同中声明 URL 必须有效,因为关于什么是有效 URL 和不是有效 URL 的规则是已知且一致的。

例外情况是客户端和服务器都无法控制的情况。异常意味着出现了问题,并且没有任何办法可以避免它。例如,网络连接不在应用程序控制范围内,因此无法采取任何措施来避免网络错误。

我想补充一点,断言/异常的区别并不是真正考虑它的最佳方式。您真正想要考虑的是合同以及如何执行合同。在我上面的 URL 示例中,最好的办法是有一个封装 URL 的类,并且是 Null 或有效 URL。它是将字符串转换为 URL 来执行合同,如果无效则抛出异常。带有 URL 参数的方法比带有 String 参数和指定 URL 的断言的方法更清晰。

【讨论】:

    【解决方案4】:

    断言用于发现开发人员做错的事情(不仅是您自己,还包括您团队中的另一位开发人员)。如果用户错误可能造成这种情况是合理的,那么它应该是一个例外。

    同样考虑后果。断言通常会关闭应用程序。如果有任何现实的期望可以恢复条件,您可能应该使用异常。

    另一方面,如果问题可能是由程序员错误引起的,那么请使用断言,因为您想尽快了解它。异常可能会被捕获和处理,而您永远不会发现它。是的,您应该在发布代码中禁用断言,因为您希望应用程序在有最小机会时恢复。即使您的程序状态被严重破坏,用户也可以保存他们的工作。

    【讨论】:

      【解决方案5】:

      “断言仅在调试模式下失败”并不完全正确。

      在 Bertrand Meyer 的 Object Oriented Software Construction, 2nd Edition 中,作者为在发布模式下检查先决条件敞开了大门。在这种情况下,当断言失败时会发生什么……引发断言冲突异常!在这种情况下,无法从这种情况中恢复:虽然可以做一些有用的事情,即自动生成错误报告,并且在某些情况下,重新启动应用程序。

      这样做的动机是,先决条件通常比不变量和后置条件的测试成本更低,而且在某些情况下,发布版本中的正确性和“安全性”比速度更重要。即,对于许多应用程序来说,速度不是问题,但鲁棒性(当程序的行为不正确时,即当合同被破坏时,程序以安全方式运行的能力)才是问题。

      是否应该始终启用前置条件检查?这取决于。由你决定。没有普遍的答案。如果您正在为银行开发软件,最好用一条警告信息中断执行,而不是转移 1,000,000 美元而不是 1,000 美元。但是,如果您正在编写游戏怎么办?也许你需要你能得到的所有速度,如果有人因为先决条件没有发现的错误(因为它们没有被启用)而得到 1000 分而不是 10 分,那么运气不好。

      在这两种情况下,理想情况下,您都应该在测试期间发现该错误,并且您应该在启用断言的情况下进行大部分测试。这里讨论的是,对于那些由于测试不完整而未在早期检测到的情况下生产代码中的先决条件失败的罕见情况,最佳策略是什么。

      总而言之,您可以拥有断言并仍然自动获取异常,如果您让它们保持启用状态 - 至少在 Eiffel 中是这样。我认为在 C++ 中做同样的事情你需要自己输入。

      另请参阅:When should assertions stay in production code?

      【讨论】:

      • 你的观点绝对有效。 SO 没有指定特定的语言——在 C# 的情况下,标准断言是 System.Diagnostics.Debug.Assert,它确实仅在 Debug 构建中失败,并将在编译时删除发布版本中的时间。
      【解决方案6】:

      有一个巨大的threadthread 关于在 comp.lang.c++.moderated 上的发布版本中启用/禁用断言,如果你有几个星期的时间,你会看到对此的看法是多么的多样化。 :)

      coppro相反,我相信如果您不确定可以在发布版本中禁用断言,那么它不应该是断言。断言是为了防止程序不变量被破坏。在这种情况下,就您的代码的客户端而言,可能会出现以下两种结果之一:

      1. 因某种操作系统类型故障而死,导致调用中止。 (没有断言)
      2. 直接调用 abort 就死掉了。 (带断言)

      这对用户来说没有什么不同,但是,断言可能会在代码中增加不必要的性能成本,而这些成本出现在代码不会失败的绝大多数运行中。

      这个问题的答案实际上更多地取决于 API 的客户是谁。如果您正在编写一个提供 API 的库,那么您需要某种形式的机制来通知您的客户他们错误地使用了 API。除非您提供两个版本的库(一个带有断言,一个没有),否则断言不太可能是合适的选择。

      但是,就我个人而言,我也不确定我是否会在这种情况下遇到例外情况。例外情况更适合可以进行适当恢复形式的地方。例如,您可能正在尝试分配内存。当您捕获“std::bad_alloc”异常时,可能会释放内存并重试。

      【讨论】:

        【解决方案7】:

        我在这里概述了我对此事状态的看法:How do you validate an object's internal state?。通常,主张您的主张并因他人违反而提出。要在发布版本中禁用断言,您可以这样做:

        • 为昂贵的检查禁用断言(例如检查范围是否有序)
        • 保持启用琐碎检查(例如检查空指针或布尔值)

        当然,在发布版本中,失败的断言和未捕获的异常应该以另一种方式处理,而不是在调试版本中(它可以只调用 std::abort)。将错误日志写入某处(可能写入文件),告诉客户发生了内部错误。客户将能够向您发送日志文件。

        【讨论】:

          【解决方案8】:

          您是在询问设计时错误和运行时错误之间的区别。

          asserts 是“嘿,程序员,这是坏掉了”的通知,它们的作用是提醒你在它们发生时你不会注意到的错误。

          例外情况是“嘿,用户,出了点问题”通知(显然您可以编写代码来捕捉它们,这样用户就不会被告知),但这些通知设计为在 Joe 用户使用应用程序时在运行时发生。

          因此,如果您认为可以解决所有错误,请仅使用异常。如果你认为你不能......使用例外。当然,您仍然可以使用调试断言来减少异常数量。

          不要忘记,许多前提条件都是用户提供的数据,因此您需要一种很好的方法来告知用户他的数据不好。为此,您通常需要将错误数据沿调用堆栈返回到他正在与之交互的位。断言将没有用 - 如果您的应用程序是 n 层,则更是如此。

          最后,我都不会使用 - 错误代码对于您认为会经常发生的错误要好得多。 :)

          【讨论】:

            【解决方案9】:

            我更喜欢第二个。虽然您的测试可能运行良好,但Murphy 表示会出现意想不到的问题。因此,您最终不会在实际的错误方法调用中获得异常,而是在更深 10 个堆栈帧的情况下追踪 NullPointerException(或等效的)。

            【讨论】:

              【解决方案10】:

              前面的答案是正确的:对公共 API 函数使用异常。您可能希望改变此规则的唯一时间是检查的计算量很大。在这种情况下,您可以将其放入断言中。

              如果您认为可能违反该先决条件,请将其作为例外保留,或重构先决条件。

              【讨论】:

                【解决方案11】:

                你应该同时使用。断言是为了方便您作为开发人员。异常会捕获您在运行时遗漏或未预料到的事情。

                我越来越喜欢glib's error reporting functions,而不是普通的旧断言。它们的行为类似于断言语句,但它们不会停止程序,而是返回一个值并让程序继续运行。它工作得非常好,作为奖励,当函数没有返回“它应该返回的内容”时,您可以看到程序的其余部分会发生什么。如果它崩溃了,你就知道你的错误检查在其他地方是松懈的。

                在我的上一个项目中,我使用这些风格的函数来实现前置条件检查,如果其中一个失败,我会将堆栈跟踪打印到日志文件但继续运行。当其他人在运行我的调试版本时遇到问题时,为我节省了大量的调试时间。

                #ifdef DEBUG
                #define RETURN_IF_FAIL(expr)      do {                      \
                 if (!(expr))                                           \
                 {                                                      \
                     fprintf(stderr,                                        \
                        "file %s: line %d (%s): precondition `%s' failed.", \
                        __FILE__,                                           \
                        __LINE__,                                           \
                        __PRETTY_FUNCTION__,                                \
                        #expr);                                             \
                     ::print_stack_trace(2);                                \
                     return;                                                \
                 };               } while(0)
                #define RETURN_VAL_IF_FAIL(expr, val)  do {                         \
                 if (!(expr))                                                   \
                 {                                                              \
                    fprintf(stderr,                                             \
                        "file %s: line %d (%s): precondition `%s' failed.",     \
                        __FILE__,                                               \
                        __LINE__,                                               \
                        __PRETTY_FUNCTION__,                                    \
                        #expr);                                                 \
                     ::print_stack_trace(2);                                    \
                     return val;                                                \
                 };               } while(0)
                #else
                #define RETURN_IF_FAIL(expr)
                #define RETURN_VAL_IF_FAIL(expr, val)
                #endif
                

                如果我需要对参数进行运行时检查,我会这样做:

                char *doSomething(char *ptr)
                {
                    RETURN_VAL_IF_FAIL(ptr != NULL, NULL);  // same as assert(ptr != NULL), but returns NULL if it fails.
                                                            // Goes away when debug off.
                
                    if( ptr != NULL )
                    {
                       ...
                    }
                
                    return ptr;
                }
                

                【讨论】:

                • 我认为我在 OP 问题中没有看到任何与 C++ 相关的内容。我认为它不应该包含在您的答案中。
                • @ForceMagic:当我发布这个答案时,这个问题在 2008 年有 C++ 标签,实际上 C++ 标签仅在 5 小时前被删除。无论如何,代码说明了一个独立于语言的概念。
                【解决方案12】:

                我尝试在这里用我自己的观点综合其他几个答案。

                在您想在生产中禁用它的情况下使用断言,而不是把它们留在里面。在生产中而不是在开发中禁用的唯一真正原因是加速程序。在大多数情况下,这种加速并不显着,但有时代码对时间要求很高,或者测试的计算量很大。如果代码是关键任务,那么尽管速度慢,异常可能是最好的。

                如果有任何真正的恢复机会,请使用异常,因为断言并非旨在从中恢复。例如,代码很少设计用于从编程错误中恢复,但它旨在从网络故障或锁定文件等因素中恢复。错误不应仅仅因为超出程序员的控制而作为异常处理。相反,与编码错误相比,这些错误的可预测性使它们更易于恢复。

                关于调试断言更容易的论点:正确命名异常的堆栈跟踪与断言一样容易阅读。好的代码应该只捕获特定类型的异常,所以异常不应该因为被捕获而被忽视。但是,我认为 Java 有时会强制您捕获所有异常。

                【讨论】:

                  【解决方案13】:

                  对我来说,经验法则是使用断言表达式来查找内部错误和外部错误的异常。您可以从 here 的 Greg 的以下讨论中受益匪浅。

                  断言表达式用于查找编程错误:程序逻辑本身的错误或相应实现中的错误。断言条件验证程序是否保持在定义的状态。 “定义的状态”基本上是与程序假设一致的状态。请注意,程序的“已定义状态”不必是“理想状态”甚至“通常状态”,甚至“有用状态”,稍后会详细介绍这一点。

                  要了解断言如何适合程序,请考虑以下例程 即将取消引用指针的 C++ 程序。现在应该 例行测试指针在取消引用之前是否为 NULL,或 它是否应该断言指针不为 NULL,然后继续 无论如何取消引用它?

                  我想大多数开发人员都想做这两个,添加断言, 但还要检查指针是否为 NULL 值,以免崩溃 如果断言条件失败。从表面上看,同时执行 测试和检查似乎是最明智的决定

                  与断言的条件不同,程序的错误处理(异常)不是指 程序中的错误,但程序从其获得的输入 环境。这些通常是某人的“错误”,例如用户 尝试在不输入密码的情况下登录帐户。和 即使错误可能会阻止程序的成功完成 任务,没有程序故障。程序无法登录用户 由于外部错误而没有密码 - 用户的错误 部分。如果情况不同,并且用户输入 密码正确但程序无法识别;那么虽然 结果还是一样,失败现在属于 程序。

                  错误处理(异常)的目的有两个。首先是沟通 向用户(或其他一些客户端)表明程序输入中的错误 被检测到以及这意味着什么。第二个目标是恢复 应用程序检测到错误后,进入一个明确定义的状态。笔记 在这种情况下程序本身没有错误。诚然, 程序可能处于非理想状态,甚至是可以做的状态 没什么用,但没有编程错误。相反, 因为错误恢复状态是程序的预期状态 设计,是程序可以处理的。

                  PS:您可能想查看类似的问题:Exception Vs Assertion

                  【讨论】:

                    【解决方案14】:

                    另见this question

                    在某些情况下,断言在构建发布时被禁用。你可以 无法控制这一点(否则,您可以使用断言构建 on),所以这样做可能是个好主意。

                    “更正”输入值的问题是调用者将 没有得到他们所期望的,这可能会导致问题,甚至 在程序的完全不同的部分崩溃,使调试成为 噩梦。

                    我通常在 if 语句中抛出异常来接管角色 的断言,以防它们被禁用

                    assert(value>0);
                    if(value<=0) throw new ArgumentOutOfRangeException("value");
                    //do stuff
                    

                    【讨论】:

                      猜你喜欢
                      • 2012-12-11
                      • 2017-12-14
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2022-06-16
                      • 2018-09-26
                      • 2020-10-25
                      • 2018-08-21
                      相关资源
                      最近更新 更多