在不同的软件工程学科中,有很多关于库应如何处理错误或其他特殊情况的哲学。我见过的一些:
- 返回一个错误代码,其结果由指针参数返回。这就是PETSc所做的。
- 通过哨兵值返回错误。例如,如果malloc无法分配内存,
sqrt
则返回NULL;如果传递负数,则将返回NaN,等等。此方法在许多libc函数中使用。 - 抛出异常。用于Deal.II,Trilinos等
- 返回变量类型;例如一个C + +函数,
Result
如果它运行正确并返回类型的对象,并使用类型Error
描述其失败的方式将返回std::variant<Error, Result>
。 - 使用断言和崩溃。用于p4est和igraph的某些部分。
每种方法的问题:
- 检查每个错误都会引入很多额外的代码。必须始终先声明将结果存储到的值,这会引入大量只能使用一次的临时变量。这种方法解释了发生了什么错误,但是很难确定为什么要进行深度调用堆栈,在哪里。
- 错误情况很容易忽略。最重要的是,如果整个输出类型范围都是合理的结果,那么许多函数甚至都没有有意义的前哨值。许多与#1相同的问题。
- 仅适用于C ++,Python等,不适用于C或Fortran。可以使用setjmp / longjmp巫术或libunwind在C中进行模仿。
- 仅在C ++,Rust,OCaml等中可用,而在C或Fortran中则不可用。可以使用宏魔术在C语言中模仿。
- 可以说是最有用的。但是,如果将这种方法用于一个C库,然后为该C库编写Python包装器,那么一个愚蠢的错误(例如将越界索引传递给数组)将使Python解释器崩溃。
互联网上有关错误处理的许多建议都是从操作系统,嵌入式开发或Web应用程序的角度编写的。崩溃是不可接受的,您必须担心安全性。科学应用程序几乎完全没有这些问题。
另一个要考虑的是什么类型的错误是可以恢复的。malloc失败是无法恢复的,并且在任何情况下,操作系统内存不足杀手都会先解决它。数组大小超出范围的索引也无法恢复。对于作为用户的我来说,库可以做的最好的事情就是崩溃,并显示一条信息丰富的错误消息。另一方面,可以通过使用直接因式分解求解器来解决迭代线性求解器收敛的失败。
科学图书馆应如何报告错误并期望对其进行处理? 我当然知道这取决于实现该库所使用的语言。但是据我所知,对于任何足够有用的库,人们都希望从某种语言(而不是其所使用的一种语言)来调用它。
顺便说一句,我认为如果C语言库将全局声明处理程序函数指针定义为公共API的一部分,则可以大大改善C语言库的方法#5。断言处理程序将默认报告文件/行号并崩溃。该库的C ++绑定将定义一个新的断言处理程序,该处理程序将引发C ++异常。同样,Python绑定将定义一个声明处理程序,该处理程序使用CPython API引发Python异常。但我不知道采用这种方法的任何示例。