按合同使用断言或例外进行devise?
在通过合同进行编程时,首先检查一下function或方法的先决条件是否被满足,然后才开始工作,对吗? 做这些检查的两个最突出的方法是assert
和exception
。
- 断言只在debugging模式下失败。 为了确保(单位)testing所有单独的合同前提条件以确定它们是否真的失败是至关重要的。
- 在debugging和发布模式下exception失败。 这具有testingdebugging行为与发布行为相同的好处,但会导致运行时性能损失。
你认为哪一个更可取?
在这里看到相关的问题
在发布版本中禁用断言就像是说:“在发布版本中我永远不会有任何问题”,而这往往不是这种情况。 所以assert不应该在发布版本中被禁用。 但是,如果发生错误,您不希望发布版本崩溃,是吗?
所以使用例外,并使用它们。 使用一个好的,坚实的exception层次结构,并确保你抓住,你可以把一个exception抛出钩在你的debugging器捕捉它,并在释放模式,你可以弥补错误,而不是直接的崩溃。 这是更安全的方式去。
经验法则是,当你试图捕捉你自己的错误时,你应该使用断言,当试图捕捉他人的错误时,应该使用断言。 换句话说,您应该使用exception来检查公共API函数的前提条件,以及何时获取系统外部的任何数据。 您应该对系统内部的函数或数据使用断言。
我遵循的原则是这样的:如果通过编码可以实际避免某种情况,那么就使用一个断言。 否则使用exception。
断言是为了确保合同得到遵守。 合同必须公平,客户必须能够保证合规。 例如,您可以在合同中声明一个URL必须是有效的,因为关于什么是和不是有效的URL的规则是已知的且一致的。
例外情况是不受客户端和服务器控制的情况。 一个例外意味着出了什么问题,并且没有什么可以做的来避免它。 例如,networking连接在应用程序控制之外,因此没有任何事情可以避免networking错误。
我想补充一点,断言/例外的区别并不是真正考虑它的最好方法。 你真正想要的是合同以及如何执行。 在我上面的URL例子中,最好的办法是有一个封装URL的类,它可以是空的,也可以是有效的URL。 它是将string转换为强制实施合同的URL,如果无效,则抛出exception。 带有URL参数的方法更清楚一点,即带有String参数的方法和指定URL的断言。
断言是为了捕捉开发者做错的事情(不仅仅是你自己 – 也是你团队的另一个开发者)。 如果用户的错误可能造成这种情况是合理的,那么它应该是一个例外。
同样想想后果。 断言通常会closures应用程序。 如果有任何现实的期望条件可以恢复,你应该使用一个例外。
另一方面,如果问题只能由程序员错误引起,那么使用断言,因为你想尽快知道它。 一个exception可能会被捕获和处理,你永远不会发现它。 是的,你应该禁用释放代码中的断言,因为如果有一点机会的话,你希望应用程序恢复。 即使你的程序状态被深刻地破坏了,用户也许能够保存他们的工作。
“断言只在debugging模式下失败”并不完全正确。
在Bertrand Meyer的“ 面向对象的软件构造”(第2版)中 ,作者在释放模式中打开了一扇门来检查前提条件。 在这种情况下,当断言失败时会发生什么…断言违反exception被引发! 在这种情况下,没有从情况中恢复:尽pipe可以做一些有用的事情,并且是自动生成错误报告,并且在某些情况下,重新启动应用程序。
这样做的动机是,先决条件通常比不variables和后置条件testing成本更低,并且在某些情况下,发布版本的正确性和“安全性”比速度更重要。 即,对于许多应用程序来说,速度不是问题,但是鲁棒性 (当程序的行为不正确时,即当合同被破坏时,程序以安全的方式运行的能力)是。
你应该总是离开先决条件检查启用? 这取决于。 随你便。 没有普遍的答案。 如果你正在为一家银行开发软件,那么最好用一个警告信息来中断执行,而不是转移$ 1,000,000而不是$ 1,000。 但是如果你正在编程一个游戏呢? 也许你需要所有的速度,如果有人得到1000点而不是10点,因为前提条件没有抓住(因为它们没有启用)的错误,运气不好。
在这两种情况下,理想情况下都应该在testing期间捕捉到这个错误,并且应该在启用断言的情况下执行重要的testing。 这里讨论的是对于那些由于未完成testing而未能及早发现的情况下,在生产代码中先决条件失败的罕见情况下,最好的策略是什么。
总结一下,如果你把它们启用,至less在埃菲尔, 你可以自动断言并自动获取exception 。 我想在C ++中也需要自己input。
另请参见: 什么时候应该断言生产代码?
关于在comp.lang.c ++的release版本中启用/禁用断言有一个巨大的线索 ,如果你有几周的时间,你可以看到这个意见是多么的多样化。 🙂
与coppro相反,我相信如果您不确定在发布版本中是否可以禁用断言,那么它不应该是断言。 断言是为了防止程序不变式被破坏。 在这种情况下,就您的代码的客户而言,会有两种可能的结果之一:
- 死于某种操作系统types的故障,导致呼叫中止。 (没有断言)
- 通过直接呼叫中止。 (有断言)
对用户来说没有任何区别,但是,在绝大多数运行代码不失败的代码中,断言可能会增加不必要的性能成本。
这个问题的答案实际上取决于API的客户是谁。 如果您正在编写提供API的库,那么您需要某种forms的机制来通知您的客户他们错误地使用了API。 除非你提供两个版本的库(一个断言,一个没有),那么断言是不太可能的合适的select。
但是,就个人而言,我不确定我是否也会为这个案例做例外。 例外情况更适合于可以进行适当forms的恢复的地方。 例如,它可能是你正在尝试分配内存。 当你捕捉到“std :: bad_alloc”exception时,可能释放内存然后重试。
我在这里概述了我对这个问题状态的看法: 你如何validation一个对象的内部状态? 。 一般来说,主张你的主张,并抛出其他人的侵犯。 要在发布版本中禁用断言,你可以这样做:
- 禁用昂贵的检查(如检查范围是否有序)
- 保持平凡的检查启用(如检查空指针或布尔值)
当然,在发布版本中,失败的断言和未捕获的exception应该以另一种方式进行处理,而不是在debugging版本中(可能只是调用std :: abort)。 写一个错误日志(可能是文件),告诉客户发生了内部错误。 客户将能够向您发送日志文件。
你在问devise时间和运行时间错误的区别。
断言是'嘿程序员,这是坏的'通知,他们在那里提醒你,当他们发生的时候你不会注意到的错误。
例外是'嘿用户,一些错误的'通知(显然你可以编码捕捉它们,所以用户永远不会被告知),但是这些被devise成在Joe用户使用该应用的运行时发生。
所以,如果你认为你可以得到所有的错误,只使用exception。 如果你认为你不能……使用例外。 当然,仍然可以使用debugging断言来减lessexception数量。
不要忘记,许多先决条件将是用户提供的数据,所以您需要一个很好的方式告知用户他的数据不好。 为此,您经常需要将调用堆栈中的错误数据返回到与之交互的位。 断言不会有用,那么 – 如果你的应用程序是n层的,那么双重的。
最后,我不会使用 – 错误代码远胜于你认为会定期发生的错误。 🙂
我更喜欢第二个。 虽然你的testing可能运行良好, 墨菲说,意想不到的事情会出错。 因此,不是在实际的错误方法调用中获得exception,而是最终追溯到NullPointerException(或等价的)10个栈帧更深。
以前的答案是正确的:使用公共API函数的例外。 唯一一次你可能想要弯曲这条规则的时候,检查的计算成本很高。 在这种情况下,你可以把它置于一个断言。
如果你认为有可能违反这个前提条件,那就把它作为一个例外,或者把前提重构掉。
你应该使用两者。 断言是作为开发人员的方便。 例外可以捕获你在运行时错过或不期望的事情。
我越来越喜欢glib的错误报告function,而不是简单的旧的断言。 他们的行为像断言,而不是暂停程序,他们只是返回一个值,让程序继续。 它的工作出人意料的好,作为一个奖励,你会看到当一个函数没有返回“它应该做的”时,你的程序的其余部分会发生什么。 如果它崩溃,你知道你的错误检查是松懈在路上的其他地方。
在我的最后一个项目中,我使用了这些风格的函数来实现预处理检查,如果其中一个失败了,我会打印一个堆栈跟踪到日志文件,但是继续运行。 当其他人在运行我的debugging版本时遇到问题时,我节省了大量的debugging时间。
#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; }
我尝试用我自己的观点综合其他答案。
在你想在生产中禁用它的情况下使用断言,错误地将它们留在它们中。在生产中禁用而不是在开发中的唯一真正原因是加速该程序。 在大多数情况下,这种加速不会很显着,但是有时候代码是时间关键的,或者testing的计算成本很高。 如果代码是关键任务,那么exception可能是最好的,尽pipe减速。
如果有任何真正的恢复机会,使用一个exception作为断言是不是旨在从中恢复。 例如,代码很lessdevise为从编程错误中恢复,但它旨在从networking故障或locking文件等因素中恢复。 错误不应被视为例外,仅仅是为了不在程序员的控制之下。 相反,与编码错误相比,这些错误的可预测性使得它们更易于恢复。
再次声明debugging断言更容易:来自正确命名的exception的堆栈跟踪与断言一样容易阅读。 好的代码只应该捕获特定types的exception,所以exception不会因为被捕获而被忽视。 但是,我认为Java有时会迫使你去捕捉所有的例外。
另请参阅此问题 :
在某些情况下,断言在构build发布时被禁用。 你可能无法控制这个(否则,你可以用assert打开),所以这样做可能是一个好主意。
“纠正”input值的问题是调用者不会得到他们期望的结果,这会导致程序完全不同的部分出现问题甚至崩溃,使得debugging成为一场噩梦。
我通常在if语句中抛出一个exception,以便在被禁用的情况下接pipe断言的angular色
assert(value>0); if(value<=0) throw new ArgumentOutOfRangeException("value"); //do stuff
对我而言,经验法则是使用assertexpression式来查找内部错误和外部错误的例外情况。 格雷格从这里开始的讨论可以使你受益匪浅。
断言expression式用于查找编程错误:程序逻辑本身中的错误或其相应实现中的错误。 断言条件validation程序保持在定义的状态。 “定义的状态”基本上与程序的假设一致。 请注意,程序的“定义状态”不一定是“理想状态”,甚至“通常状态”,甚至不是“有用状态”,而是稍后在那个重要的点上。
要理解断言如何适合程序,请考虑C ++程序中的一个例程,该程序即将引用一个指针。 现在,例程应该在取消引用之前testing指针是否为NULL,还是应该断言指针不是NULL,然后继续并取消引用它?
我想大多数开发人员会想要做这两个,添加断言,但也检查指针的NULL值,为了不崩溃,如果断言条件失败。 表面上,执行testing和检查似乎是最明智的决定
与其声明的条件不同,程序的error handling(例外)不是指程序中的错误,而是指程序从其环境中获取input。 这些往往是某些人的“错误”,例如用户尝试login帐户而不input密码。 即使这个错误可能会阻止程序成功完成任务,程序也不会失败。 由于外部错误,程序无法在没有密码的情况下login用户,这是用户方面的错误。 如果情况不同,用户input正确的密码,程序无法识别; 那么虽然结果仍然是一样的,那么失败现在就属于这个计划。
error handling(例外)的目的是双重的。 首先是与用户(或其他客户端)通信,程序input中的错误已经被检测到,意味着什么。 第二个目标是在检测到错误之后将应用程序恢复到一个明确的状态。 请注意,在这种情况下程序本身没有错误。 当然,程序可能处于非理想的状态,甚至是一个无用的状态,但是没有编程错误。 相反,由于错误恢复状态是程序devise期望的状态,所以程序可以处理错误状态。
PS:你可能想看看类似的问题: Exception Vs Assertion 。