D 中惯用的错误处理

Idiomatic error handling in D

我试图找到关于什么是 standardisedaccepted 惯用的错误处理方式的资源,但我找不到找到任何。如果你正在阅读关于 error handling 的官方文档,那么你会发现以下非常重要的陈述:

  • Errors are not part of the normal flow of a program. Errors are exceptional, unusual, and unexpected.
  • Because errors are unusual, execution of error handling code is not performance critical.
  • The normal flow of program logic is performance critical.

我称它们为重要,因为对此类 异常 情况使用异常的推理是导致文章得出结论的原因,错误毕竟是特殊情况,例外是必经之路,无论成本如何。再次来自同一篇文章:

Because errors are unusual, execution of error handling code is not performance critical. Exception handling stack unwinding is a relatively slow process.

在一些特殊情况下,异常可能没有被显式处理,但它们的存在无论如何都会影响事物的状态,应该使用 exception safe scope guards.

我的主要问题是,上面提到的解决方案及其在文档中的示例确实是例外情况,当我们遇到与内存相关的问题时非常有用,但我们不希望我们的程序失败,我们希望保持完整性并在可能的情况下从这些场景中恢复,但其他情况呢?

众所周知,错误不仅用于异常情况和意外情况,而且它们是调用者被调用者[=32=之间进行通信的方式].例如,错误可以用在消毒剂中。假设我们要为关联数组实现模式验证。类型系统本身无法定义键和值的约束,因此我们创建了一个函数来在 运行 时间内检查此类对象。那么如果模式失败应该怎么办?由于我们对它如何失败感兴趣,因此其中发生的错误(即发现无效数据)也应包含有关出错原因的信息,因此调用者将知道如何对其采取行动。根据第一篇文章的作者,使用异常是一种昂贵的抽象。根据同一作者在同一篇文章中的说法,使用 C 风格的函数约定,其中 return 值全部用于错误状态是错误的方法。

那么,在 D 中处理不是异常的错误的正确且惯用的方法是什么?

好吧,TLDR 版本是使用异常是 D 中处理错误条件的惯用方法,但当然,细节比这更复杂。

问题的一部分是什么构成了错误。术语错误用于很多事情,因此,谈论错误可能会让人非常困惑。一些 classes 错误是程序错误(因此是程序错误的结果),其他错误不是程序错误但非常严重以至于程序无法继续,而其他错误则取决于用户输入之类的东西并且经常可以从中恢复。

对于程序性错误和灾难性错误,D 有 Error class,派生自 ThrowableError 的两个常用子 class 是 AssertErrorRangeError - AssertError 是断言失败的结果,而 RangeError当您尝试使用超出范围的索引对数组进行索引时,您会得到什么。这两个都是程序错误;它们是您程序中错误的结果,从中恢复是没有意义的,因为根据定义,您的程序此时处于无效状态。 MemoryError 不是错误但通常是灾难性的错误示例,您的程序应该被终止,它在 new 无法分配内存时抛出。

当抛出 Error 时,无法保证任何清理代码将是 运行(例如,可能会跳过析构函数和 scope 语句,因为假设是那是因为您的代码处于无效状态,清理代码实际上会使事情变得更糟)。该程序只是展开堆栈,打印出 Error 的消息和堆栈跟踪,然后终止您的程序。因此,尝试捕获 Error 并让程序继续运行几乎总是一个糟糕的主意,因为程序处于未知和无效状态。如果某些东西被认为是 Error,那么它就是错误条件被认为不可恢复的那种情况,程序不应尝试从中恢复。

在大多数情况下,您可能不会对 Error 做任何明确的事情。当不使用 -release 编译时,您将在代码中放置断言以捕获错误,但您可能不会显式抛出任何 Errors。它们主要是 D 的 运行 时间或代码断言的结果,您正在 运行ning 捕获程序中的错误。

Throwable派生的另一个class是Exception。它用于问题 不是 程序中的错误而是由于用户输入或环境引起的问题(例如,用户提供的 XML 无效,或者您的程序试图打开的文件不存在)。异常为函数提供了一种方法来报告其输入无效或由于无法控制的问题而无法完成其任务。然后程序可以选择捕获 Exception 并尝试从中恢复,或者它可以让它冒泡到顶部并终止程序(尽管通常情况下,捕获它们并打印出一些东西对用户更友好比带有堆栈跟踪的消息更用户友好)。与 Errors 不同,Exceptions do 导致所有清理代码为 运行。所以,抓住他们并继续执行是完全安全的。

但是,程序可以做什么来响应异常以及它是否可以做更多的事情而不是向用户报告错误发生并终止取决于异常是什么以及程序正在做什么(这是部分为什么有些代码 subclasses Exception - 它提供了一种报告出错的方法,而不仅仅是错误消息,并允许程序根据出错的类型以编程方式响应它,而不是而不是简单地回应 "something" 出错的事实)。通过使用异常来报告出现问题时,它允许代码不直接处理错误,除非它是代码中您想要处理错误的地方,从而使代码整体更简洁,但缺点是有时您可能会遇到异常如果您对什么时候可能抛出的东西不够熟悉,就会被抛出您没有预料到的。但这也意味着报告的错误不会像错误代码一样被遗漏。如果您忘记处理异常,您会在它发生时知道它,而对于错误代码之类的东西,很容易忘记检查它或没有意识到您需要检查它,并且可能会错过错误。因此,虽然意外异常可能很烦人,但它们有助于确保您在程序出现问题时发现它们。

现在,使用断言与异常的最佳时机可能有点像一门艺术。例如,使用契约式设计,您使用断言来检查函数的输入,因为任何使用无效参数调用该函数的代码都违反了契约,因此被认为是错误的,而在防御性编程中,您不假设输入有效,因此该函数始终检查其输入(不仅仅是在未使用 -release 进行编译时),并在失败时抛出 Exception。哪种方法更有意义取决于您在做什么以及函数的输入可能来自何处。但是使用断言来检查用户输入或程序控制之外的任何东西是不合适的,因为错误的输入不是程序中的错误。

然而,虽然一般来说,在 D 中处理错误情况的惯用方法可能是抛出异常,但有时这确实没有意义。例如,如果错误条件实际上极有可能发生,则抛出异常是一种非常昂贵的处理方式。对于并非一直发生的情况,异常通常足够快,但对于经常发生的事情——尤其是在性能关键代码中——它们可能太昂贵了。在这种情况下,做一些类似错误代码的事情可能更有意义 - 或者做一些类似 returning a Nullable 的事情,并在函数未能获得结果时将其设置为 null。

一般来说,当可以合理地假设函数会成功时,异常最有意义 and/or 当它简化代码以使其不必担心错误情况时。

例如,假设编写一个使用错误代码而不是异常的 XML 解析器。其实现中的每个函数都必须检查它调用的任何函数是否成功以及 return 它自己是否成功,这不仅容易出错,而且意味着您基本上有错误处理代码遍及整个解析器。另一方面,如果您使用异常,那么大多数解析器都不必关心 XML 中的错误。而不是遇到无效 XML 的代码必须 return 调用它的函数必须处理的错误代码,它可以抛出异常,调用链中的任何代码实际上都是一个好地方处理错误(可能首先调用解析器的代码)是唯一必须处理错误的代码。程序中唯一的错误处理代码是需要处理错误的代码,而不是程序的大部分代码。这样代码更清晰。

另一个异常真正清理代码的例子是像 std.file.isDir 这样的函数。它 returns 它给出的文件名是否对应于一个目录,并在出现问题时抛出 FileException (例如文件不存在,或者用户没有权限访问它) .为了让它与错误代码一起工作,你会被困在做类似

的事情
int isDir(string filename, ref bool result);

这意味着你不能简单地把它放在

这样的条件下
if(file.isDir)
{
    ...
}

你会被像

这样丑陋的东西困住
bool result;
immutable error = file.isDir(result);
if(error != 0)
{
    ...
}
else if(result)
{
    ...
}

的确,在许多情况下,文件不存在的风险很高,这将成为使用错误代码的理由,但 std.file.exists 可以在调用 isDir 并因此确保 isDir 失败是不常见的情况 - 或者如果有问题的代码是以文件很可能存在的方式编写的(例如,它是从 dirEntries 获得的) ,那么您就不必费心检查文件是否存在。无论哪种方式,结果都比处理错误代码更清晰,更不容易出错。

在任何情况下,最合适的解决方案取决于您的代码在做什么,并且在某些情况下异常确实没有意义,但总的来说,它们是处理没有意义的错误的惯用方法程序中的错误或诸如 运行ning 内存不足之类的灾难性错误,而 Error's 通常是处理程序中遇到的错误或灾难性错误的最佳方法。归根结底,与其他技术相比,了解何时以及如何使用异常是一门艺术,通常需要经验才能对它有良好的感觉,这就是为什么要问何时使用异常、断言和错误的部分原因代码不时弹出。