显式引发异常的应用

Applications of explicitly raising exceptions

在程序中显式引发异常有哪些应用和优势。例如,如果我们考虑 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 美分作为一个答案。让我们从一般问题开始

But what are the advantages and application areas where we would need to raise exceptions explicitly?

引发异常的典型原因有几个。他们中的大多数都不是特定于 Ada 的。

首先,可能存在使用或不使用异常的一般设计决策。一些一般标准:

  • 异常处理程序可能会产生 运行 时间成本,即使实际上从未抛出异常(参见 https://gcc.gnu.org/onlinedocs/gnat_ugn/Exception-Handling-Control.html)。这可能是不可接受的。
  • 与其他语言的互操作性问题可能会排除异常的使用,或者至少需要 none 保留使用 Ada 编程的部分。
  • 在某种程度上,决定也是一个品味问题。来自无一例外的语言的程序员可能会对仅依赖于检查 return 值的设计更有信心。
  • 有些程序比其他程序更能从异常中获益。如果传统的错误处理掩盖了实际的程序结构,那么可能是异常的时候了。另一方面,如果潜在的错误很少,很容易检测到并且可以在本地处理,那么异常可能比传统的处理错误更能掩盖潜在的执行路径。

一旦做出使用异常的一般决定,问题就会出现,何时和何时不在您的代码中引发它们是合适的。我在评论中提到了一个一般标准。想到什么:

  • 异常不应该是正常的、预期的程序流程的一部分(它们被称为 exceptions,而不是 expectations ;-))。这部分是因为控制流更难看到,部分是因为潜在的 运行 时间成本。
  • 可以在本地处理的错误不需要异常。 (虽然为了有一个统一的错误处理,提出一个仍然是有用的。我将在下面讨论你的代码片段。)
  • 另一方面,如果一个函数不知道如何处理错误,异常是很好的。对于可以从各种上下文(GUI、控制台程序、嵌入式、服务器...)调用的实用程序和库函数尤其如此。异常允许错误在调用链中向上传播,直到有人可以处理它,中间层中没有任何错误处理代码。
  • 有人说图书馆应该 only expose custom exceptions, 至少对于任何预期的错误。例如。当发生 I/O 异常时,将其包装在自定义异常中并显式 raise 该自定义异常。

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

Is there any advantage or good practice if I raise an exception in the above function and handle it when the passed string length bound exceeds the tolerable limits? Or a simple If-else handler logic should do the business?

我不是特别喜欢那样(尽管我并不觉得它很糟糕)因为我上面的一般论点(“如果你能在本地处理它,就不要加注”)表明一个简单的 if/else 更清楚。1 例如,如果函数很长,异常处理程序将远离错误位置,因此可能想知道异常可能发生的位置(并找到一个raise位置并不能保证一个人已经找到了所有的,所以审稿人必须仔细检查整个功能!)。

不过要看具体情况。如果错误可能发生在多个地方,则引发异常可能是优雅的。例如,如果几个字符串可能太短,那么通过异常处理程序进行集中式错误处理可能会更好,而不是将 if/then/elses(嵌套??)分散在函数体中。这种情况是如此普遍,以至于 a legitimate case can be made for using goto constructs 在语言中无一例外。一个例外显然更优越。


1但实际上,您如何处理那里的错误?您有可靠的日志记录工具吗?你return做什么?调用者是否知道结果可能无效?也许你应该扔 而不是接住。

给定的例子有两个问题:

  1. 很简单,控制流不需要异常。然而,情况并非总是如此,我稍后会回到这一点。
  2. Constraint_Error 是一个非常糟糕的异常,用于检测字符串长度错误。标准异常 Program_ErrorConstraint_ErrorStorage_Error 应该保留用于编程错误条件,并且在大多数情况下应该在可执行文件造成任何损坏之前关闭可执行文件,并提供足够的调试信息(至少一个堆栈回溯)让你找到错误并保证它永远不会再发生。

让一个 Constraint_Error 诡异地指向你的错误,而不是以后发生任何未定义的行为,这是非常令人满意的......(学习如何打开堆栈回溯很有用,这是'通常默认情况下打开)。

相反,您可能想要定义自己的 String_Size_Error 异常,引发并处理它。然后,将正确调试未显示代码中引发 Constraint_Error 的任何其他内容,而不是静默生成错误的 Chars_Ptr.


对于引发异常的有效用例,请考虑使用 SPICE 等电路模拟器(或用于气流的 CFD 模拟器等)。由于矩阵计算中发生的数值问题,这些工具即使在正常工作时也容易出现故障。 (两项抵消,产生零 +/- 舍入误差,这会导致不可行的大数字或稍后被零除)。它通常是一个迭代近似,其中误差应该在每一步中减少,直到它是一个可以接受的低值。但是如果发生故障,错误项将开始增长...

通常模拟是一步一步发生的,其中每一步都是足够小的时间步长,可能是 1 us 或 1 ns。主循环请求一个步骤,该请求将传递给模拟中的数千个代理,代表电路中的组件或 CFD 网格中的三角形。

这些代理中的任何一个都可能无法计算解决方案,处理失败的最干净的方法是引发异常,也许 Convergence_Error。可能有数千个可能的点可以引发异常。

测试数千个 return 代码会很快变得丑陋。但除了例外,主循环只需要一个处理程序,它会采取一些纠正措施,例如减少模拟步长和 运行 再次执行该步骤。

清理浏览器中的用户文本输入可能是另一个很好的用例,更接近于示例代码。


关于异常的运行时成本的一句话:Gnat 编译器及其 RTS 支持 "Zero Cost Exception" (ZCX) 模型 - 至少对于某些目标。引发异常时会有更大的惩罚,作为在正常情况下消除惩罚的权衡。如果惩罚对您很重要,请参阅文档以查看在您的情况下是否值得 9 甚至可能)。