Ada 编程/错误处理
本章介绍各种错误处理技术。首先描述技术,然后用示例函数及其调用来展示其使用。我们使用√函数,该函数在以负参数调用时应报告错误条件。
具体地说:异常是 Ada 的正确做法。
procedureError_Handling_1isfunctionSquare_Root (X :inFloat)returnFloatisuseAda.Numerics.Elementary_Functions;beginifX < 0.0thenreturn-1.0;elsereturnSqrt (X);endif;endSquare_Root;beginC := Square_Root (A ** 2 + B ** 2);ifC < 0.0thenT_IO.Put ("C cannot be calculated!");elseT_IO.Put ("C is "); F_IO.Put (Item => C, Fore => F_IO.Default_Fore, Aft => F_IO.Default_Aft, Exp => F_IO.Default_Exp);endif;endError_Handling_1;
我们的示例利用了√所有有效返回值都为正的事实,因此-1可以用作错误指示器。但是,当所有可能的返回值都是有效的,并且没有返回值可用作错误指示器时,此技术将不起作用;因此,使用此方法是一个非常糟糕的想法。
错误条件通过额外的out参数返回。传统上,指示器要么是布尔值,其中“true = 成功”,要么是枚举值,其中第一个元素为“Ok”,其他元素指示各种错误条件。
procedureError_Handling_2isprocedureSquare_Root (Y :outFloat; X :inFloat; Success :outBoolean)isuseAda.Numerics.Elementary_Functions;beginifX < 0.0thenY := 0.0; Success := False;elseY := Sqrt (X); Success := True;endif;return;endSquare_Root;beginSquare_Root (Y => C, X => A ** 2 + B ** 2, Success => Success);ifSuccessthenT_IO.Put ("C is "); F_IO.Put (Item => C);elseT_IO.Put ("C cannot be calculated!");endif;endError_Handling_2;
Ada 到 Ada 2005 的一个限制是函数不能有 out 参数。(函数可以有任何副作用,但不能显示它们)。因此,对于我们的示例,我们不得不使用过程。坏消息是 Success 参数值很容易被忽略。
在 Ada 2012 中,函数可以具有任何模式的参数;因此这最终是可能的
functionSquare_Root (X :inFloat; Success :outBoolean)returnFloatis...
这种技术在数学计算中看起来不太好;因此也不是一个好主意。
错误条件存储在全局变量中。然后通过函数直接或间接读取该变量。
procedureError_Handling_3isFloat_Error : Boolean;functionSquare_Root (X :inFloat)returnFloatisuseAda.Numerics.Elementary_Functions;beginifX < 0.0thenFloat_Error := True;return0.0;elsereturnSqrt (X);endif;endSquare_Root;beginFloat_Error := False; -- reset the indicator before use C := Square_Root (A ** 2 + B ** 2);ifFloat_ErrorthenT_IO.Put ("C cannot be calculated!");elseT_IO.Put ("C is "); F_IO.Put (Item => C, Fore => F_IO.Default_Fore, Aft => F_IO.Default_Aft, Exp => F_IO.Default_Exp);endif;endError_Handling_3;
从源代码中可以看出,此技术的麻烦之处在于选择重置标志的位置。您可以让被调用者或调用者执行此操作。
此外,这种技术不适合多线程。
对于像这样的情况使用全局变量确实是一个非常糟糕的想法,实际上是其中最糟糕的一种。
Ada 支持一种错误处理形式,这种形式长期以来被其他语言使用,例如经典的ON ERROR GOTO ...(来自早期 Basic 方言)到try ... catch 异常处理(来自现代面向对象的语言)。
这个想法是:您将程序的某个部分注册为错误处理程序,以便在发生错误时调用。您甚至可以定义多个处理程序来分别处理不同类型的错误。一旦发生错误,执行就会跳转到错误处理程序并在那里继续;无法返回到发生错误的位置。
这就是 Ada 的方式!
procedureError_Handling_4isRoot_Error:exception;functionSquare_Root (X :inFloat)returnFloatisuseAda.Numerics.Elementary_Functions;beginifX < 0.0thenraiseRoot_Error;elsereturnSqrt (X);endif;endSquare_Root;beginC := Square_Root (A ** 2 + B ** 2); T_IO.Put ("C is "); F_IO.Put (Item => C, Fore => F_IO.Default_Fore, Aft => F_IO.Default_Aft, Exp => F_IO.Default_Exp);exceptionwhenRoot_Error => T_IO.Put ("C cannot be calculated!");endError_Handling_4;
异常处理的强大之处在于它可以在一个异常处理程序中阻止多个操作。这减轻了错误处理的负担,因为不必独立检查每个函数或过程调用以确保执行成功。
在契约式设计 (DbC) 中,必须使用正确的参数调用操作。这是调用者对契约的部分。如果实际参数的子类型与形式参数的子类型匹配,并且如果实际参数具有使函数的前置条件为真的值,则子程序将有机会满足其后置条件。否则会发生错误条件。
现在您可能想知道这将如何运作。让我们先看看示例
procedureError_Handling_5issubtypeSquare_Root_TypeisFloatrange0.0 .. Float'Last;functionSquare_Root (X :inSquare_Root_Type)returnSquare_Root_TypeisuseAda.Numerics.Elementary_Functions;beginreturnSqrt (X);endSquare_Root;beginC := Square_Root (A ** 2 + B ** 2); T_IO.Put ("C is "); F_IO.Put (Item => C, Fore => F_IO.Default_Fore, Aft => F_IO.Default_Aft, Exp => F_IO.Default_Exp);return;endError_Handling_5;
如您所见,该函数需要一个X >= 0 的前置条件——也就是说,该函数只能在X ≥ 0 时调用。作为回报,该函数承诺作为后置条件,返回值也 ≥ 0。
在一个完整的 DbC 方法中,后置条件将陈述一个关系,该关系完全描述了运行函数时产生的值,例如result ≥ 0 and X = result * result。此后置条件是√对契约的部分。使用断言、注释或语言类型系统来表达前置条件X >= 0 体现了契约式设计的两个重要方面
- 可能存在编译器或分析工具可以帮助检查契约的方法。(例如,当
X ≥ 0来自 X 的子类型,并且调用时√的参数是相同的子类型,因此也 ≥ 0 时,就会出现这种情况。) - 可以在调用函数之前机械地检查前置条件。
第一个方面增加了安全性:没有程序员是完美的。每个需要由程序员自己检查的契约部分都有很高的出错概率。
第二个优化方面非常重要——当合约可以在编译时进行检查时,就不需要运行时检查。你可能没有注意到,但如果你仔细想想: 永远不会为负数,前提是指数运算符和加法运算符以通常的方式工作。
我们已经为一段永远不会失败的代码提供了 5 个不错的错误处理示例。这为控制 DbC 的一些运行时方面提供了绝佳的机会:现在你可以安全地关闭检查,代码优化器可以省略实际的范围检查。
DbC 语言在合约违反时如何表现方面有所区别。
- 真正的 DbC 编程语言将 DbC 与异常处理结合起来——在运行时检测到合约违反时抛出异常,并提供重新启动失败例程或以已知良好状态阻塞的方法。
- 静态分析工具在分析时检查所有合约,并要求代码以一种永远不会在运行时违反合约的方式编写。
Ada 2012 引入了前置条件和后置条件方面。