显式抛出异常的应用场景

3

明确抛出异常在程序中的应用和优势是什么?例如,如果我们具体考虑Ada语言,在程序中提供了一个接口来引发异常。示例:

raise <Exception>;

但是,我们需要明确提出异常的优势和应用领域是什么呢?

例如,在一个将参数之一设为字符串的过程中:

function Fixed_Str_To_Chr_Ptr (Source_String : String) return C.Strings.Chars_Ptr is
...
begin
...
  -- Check whether source string is of acceptable length
  if Source_String'Length <= 100 then
  ...
  else
    ...
    raise Constraint_Error;
  end if;

  return Ptr;

exception
     when Constraint_Error=>
        .. Do Something..
end Fixed_Str_To_Chr_Ptr;

如果我在上述函数中引发异常并在传递的字符串长度边界超出可容忍限制时进行处理,是否有任何优势或良好做法?还是简单的if-else处理程序逻辑就足够了?


2
这是一个非常通用的设计问题。一般的答案很简单:“尽可能在本地处理,对于其余部分则引发异常”。(异常很有用,因为(1)如果调用者无法处理错误,则会立即出错;(2)提供源代码位置和调用堆栈信息给调用者或程序员;(3)如果直接调用者本身无法正确处理它,则可以将错误传递到更高层次而不需要任何编码工作。) - Peter - Reinstate Monica
正如@PeterA.Schneider所说,在这种情况下,您绝对不应该引发异常,而是在else部分中执行“.. Do Something ..”。 - Simon Wright
1
@SimonWright 你确定吗?在这样一个实用函数中,异常可能是正确的选择,因为它不知道将在哪个上下文中使用。 (调用者可能决定打开对话框、写日志消息、中止操作,因为它无法恢复——我们无法知道。)顺便说一句,这与Ada没有太大关系,除了异常在某些语言中比其他语言更常见(不确定Ada的位置)。 - Peter - Reinstate Monica
@PeterA.Schneider,感谢您的建议。我想再问一件事,对于像 when E : others => 这样的“其他”情况,在设计(专业产品中的)异常处理程序时,是否是一种好方法,尤其是当您真的不知道将来可能会引发哪些异常?或者我们应该限定在我们分析过的可能引发的异常范围内? - Akay
2
@PeterA.Schneider,我看不出在同一子程序中引发异常然后处理它的意义!@AKay,“when others => null;”是不好的,因为你永远不会发现自己的错误。“when others => Report_The_Exception_And_Reset;”在顶层可能更合理。 - Simon Wright
4个回答

3
我会把我的两分钱作为一个答案来捆绑各个方面。让我们从一般问题开始:
“但是我们需要在哪些优势和应用领域中明确引发异常呢?”
引发异常的原因有几个典型的情况。其中大多数并不特定于Ada。
首先,可能存在一般性的设计决策,使用或不使用异常。一些一般性的标准:
  • 异常处理程序即使实际上从未抛出异常,也可能会产生运行时成本(例如参见https://gcc.gnu.org/onlinedocs/gnat_ugn/Exception-Handling-Control.html)。这可能是无法接受的。
  • 与其他语言的互操作性问题可能排除了使用异常,或者至少要求不将异常传递到Ada编程部分之外。
  • 在某种程度上,决策也是品味问题。来自没有异常的语言的程序员可能更喜欢仅依赖于检查返回值的设计。
  • 有些程序比其他程序更适合使用异常。如果传统的错误处理模糊了实际的程序结构,那么现在可能是使用异常的时候了。另一方面,如果潜在错误很少,容易检测并且可以在本地处理,则异常可能会比传统错误处理方式更加模糊潜在的执行路径。

一旦决定在代码中使用异常,就会出现何时以及何时不适当地引发异常的问题。我在评论中提到了一个一般标准。以下是我想到的:

  • 异常不应该成为正常的、预期的程序流程的一部分(它们被称为异常,而不是预期;-))。这部分是因为控制流更难看到,另一部分是由于潜在的运行时成本。
  • 可以在本地处理的错误不需要异常。(尽管在统一的错误处理中引发异常仍然很有用。当我到达您的代码片段时,我将讨论这个问题。)
  • 另一方面,如果一个函数不知道如何处理错误,那么异常就非常好用。这对于实用程序和库函数特别适用,因为它们可以从各种上下文(GUI、控制台程序、嵌入式、服务器等)调用。异常允许错误传播到调用链上,直到有人能够处理它,而在中间层中没有任何错误处理代码。
  • 有些人说,库应该仅公开自定义异常,至少对于任何预期的错误。例如,当出现I/O异常时,将其包装在自定义异常中,并显式地raise该自定义异常。

现在来回答你具体的代码问题:

在上述函数中,如果传递的字符串长度超过可容忍的限制,我是否应该引发异常并处理它?或者一个简单的if-else处理程序逻辑就可以解决问题了吗?我不是特别喜欢这种方式(尽管我不认为它很糟糕),因为我的一般论点(“如果您可以在本地处理它,请不要引发”)表明简单的if/else更清晰。例如,如果函数很长,则异常处理程序将远离错误位置,因此人们可能会想知道异常确切发生的位置(而找到一个raise位置并不能保证已经找到了所有位置,因此审核人员必须仔细检查整个函数!)。但具体情况取决于具体情况。如果错误可能发生在多个位置,则引发异常可能更加优雅。例如,如果有多个字符串太短,则通过异常处理程序进行集中的错误处理可能比在函数体中分散if / then / else(嵌套??)更好。这种情况非常普遍,以至于在没有异常的语言中使用goto结构可以提出合理的案例。然后,异常显然更优越。
但实际上,你如何处理那个错误呢?你有一个可靠的日志记录设施吗?你会返回什么?调用者是否知道结果可能无效?也许你应该抛出异常而不是捕获它。

一些针对裸板系统的Ada运行时根本不允许异常传播;因此您需特别小心,不要让异常情况发生! - Simon Wright

2

你可以明确地引发异常来控制子程序向用户报告哪个异常。- 或者在某些情况下,仅控制与引发的异常相关联的消息。

在非常特殊的情况下,您还可以引发异常作为程序流程控制。


2
给出的示例有两个问题:
1.它太简单了,控制流不需要异常。然而,这并不总是情况,稍后我会回到这个问题上来。
2.在检测字符串长度错误时引发Constraint_Error异常非常糟糕。标准异常Program_ErrorConstraint_ErrorStorage_Error应当保留给程序错误条件,并且在大多数情况下,应在可执行文件运行前将其关闭,并提供足够的调试信息(至少是堆栈跟踪),以便您找到错误并确保它永远不会再次发生。
得到一个指向错误附近的Constraint_Error异常是令人满意的,而不是后面发生的任何未定义行为……(学习如何开启堆栈跟踪,一般默认情况下没有开启,将对您有所帮助)。
相反,您可能想要定义自己的String_Size_Error异常,并引发它并处理它。然后,在您未显示的代码中引发Constraint_Error的任何其他内容都将得到适当的调试,而不是生成一个有缺陷的Chars_Ptr
对于引发异常的有效用例,请考虑 SPICE 等电路模拟器(或气体流 CFD 模拟器)的情况。即使工作正常,这些工具也容易发生由于矩阵计算中出现的数值问题而引起的故障。(两个术语相互抵消,产生零+/-舍入误差,从而导致后面出现不可行的大数或零除)。通常是一种迭代逼近,其中误差应该在每一步中减少,直到它达到可接受的低值。但如果发生故障,误差项将开始增长……
通常情况下,模拟按步骤进行,其中每个步骤都是足够小的时间步长,可能是 1 微秒或 1 纳秒。主循环请求一步,这个请求传递给模拟中代表电路组件或 CFD 网格中三角形的数千个代理。
这些代理中的任何一个都可能无法计算出解决方案,处理失败的最简单方法是引发异常,例如Convergence_Error。可能有成千上万个可以引发异常的可能点。
测试数千个返回代码将变得非常丑陋。但使用异常,主循环只需要一个处理程序,该处理程序采取一些纠正措施,如减小模拟步长并再次运行步骤。
在浏览器中对用户文本输入进行净化可能是另一个更接近示例代码的好用例。
关于异常运行时成本的一些注意事项:Gnat 编译器及其 RTS 支持“零成本异常”(ZCX)模型——至少针对某些目标。发生异常时会有更大的惩罚,以抵消正常情况下的惩罚。如果惩罚对您很重要,请参考文档,查看是否值得在您的情况下使用它(甚至是否可能)。

感谢提供如此出色的信息! - Akay
在一个函数中,假设在中间调用了多个其他函数,将异常处理程序仅保留在主函数中,还是在每个中间调用的函数中都保留,以便处理可能引发的异常,这样做是否明智?(我知道这取决于需求,但只是想知道一个通用答案)。我的意思是,在每个可能引发异常的函数中实现大量的异常处理程序是否看起来合适? - Akay
1
对我来说,无数的异常处理程序可能是代码异味。如果不同函数的异常需要不同的处理,则可以引发不同的异常,或者“raise”可以将字符串传递给处理程序以提供附加信息。 - user1818839

1

异常应该保持其名称的真实性,即代表异常情况。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接