我试图找到有关标准化和接受惯用D-way处理错误的资源,但我找不到任何资源。如果正在阅读error handling上的官方文档,那么可以在那里找到以下非常重要的陈述:
- 错误不是程序正常流程的一部分。错误是特殊的,不寻常的和意外的。
- 由于错误不常见,因此错误处理代码的执行对性能不严重。
- 程序逻辑的正常流程对性能至关重要。
我认为它们很重要,因为使用例外情况来推理例外案例是导致文章得出结论的原因,毕竟错误是特殊情况,例外是这样的无论成本是多少,都要去。再次来自同一篇文章:
因为错误是异常的,所以错误处理代码的执行对性能不是很重要。异常处理堆栈展开是一个相对缓慢的过程。
在某些特殊情况下,可能无法明确处理异常,但无论如何它们的存在都会影响事物的状态,应该使用exception safe scope
guards。
我的主要问题是,上述解决方案及其在文档中的示例确实是例外情况,当我们遇到与内存相关的问题时非常有用,但我们不希望我们的程序失败,我们想要保持完整性并尽可能从这些场景中恢复,但其他情况呢?
众所周知,错误不仅用于特殊情况和意外情况,而且它们是调用者和 callees 之间进行通信的方式。例如,可以在消毒剂中使用错误。假设我们想要为关联数组实现模式验证。单独的类型系统不能定义键和值的约束,因此我们创建了一个在运行时检查这些对象的函数。那么如果架构失败会发生什么?由于我们对它如何失败感兴趣,因此发生的错误(即找到的无效数据)也应该包含有关错误的信息,因此调用者将知道如何对其进行操作。根据第一篇文章的作者,使用异常是一种昂贵的抽象。根据同一篇文章中的同一作者,使用C风格的函数约定,其中返回值全部用于错误状态是错误的方法。
那么,处理D中不是例外的错误的正确和惯用方法是什么?
答案 0 :(得分:3)
嗯,TLDR版本是使用异常是处理D中错误条件的惯用方法,但当然,细节会比这更复杂。
部分问题是什么构成错误。术语错误用于许多事情,因此,谈论错误可能会非常混乱。某些类型的错误是程序错误(因此是程序中的错误的结果),其他错误不是程序错误,但是灾难性很强,程序无法继续,而其他错误则依赖于用户输入和通常可以从中恢复。
对于程序错误和灾难性错误,D具有Error
类,该类派生自Throwable
。 Error
的两个常用子类是AssertError
和RangeError
- AssertError
是断言失败的结果,RangeError
是你得到的结果尝试索引一个索引超出范围的数组。这两个都是程序错误;它们是程序中的错误的结果,从它们中恢复毫无意义,因为根据定义,您的程序在此时处于无效状态。一个错误的例子不是错误,但通常是灾难性的,应该终止你的程序是MemoryError
,当new
无法分配内存时会抛出该错误。
当抛出Error
时,无法保证将运行任何清理代码(例如,可以跳过析构函数和scope
语句,因为假设是因为您的代码在一个无效的状态,清理代码实际上可能会使事情变得更糟)。该程序只是展开堆栈,打印出Error
的消息和堆栈跟踪,并终止您的程序。因此,尝试捕获Error
并继续执行程序几乎总是一个糟糕的想法,因为该程序处于未知和无效状态。如果某些内容被视为Error
,那么这种情况就是错误条件被认为是不可恢复的,并且程序不应该尝试从中恢复。
在大多数情况下,您可能无法使用Error
做任何明确的事情。您不会在代码中添加断言以在未使用-release进行编译时捕获错误,但您可能无法明确地抛出任何Error
。它们主要是D运行时的结果或代码中的断言,您正在运行程序中捕获错误。
从Throwable
派生的另一个类是Exception
。它用于问题不程序中的错误但由于用户输入或环境导致的问题(例如用户提供的XML无效或文件)你的程序试图打开不存在)。例外为函数报告其输入无效或由于其控制之外的问题而无法完成其任务提供了一种方法。然后程序可以选择捕获Exception
并尝试从中恢复,或者它可以让它冒泡到顶部并杀死程序(尽管通常,它更方便用户捕获它们并打印出比带有堆栈跟踪的消息更加用户友好的东西。与Error
不同,Exception
s 执行会导致所有清理代码运行。因此,抓住它们并继续执行是完全安全的。
然而,程序可以做什么来响应异常以及它是否可以做更多事情而不是向用户报告错误发生和终止取决于异常是什么以及程序正在做什么(这是为什么的一部分)一些代码子类Exception
- 它提供了一种报告除了错误消息之外的错误的方法,并允许程序根据出错的类型以编程方式响应它而不是简单地响应& #34;出错了#34;出错了)。通过使用异常来报告出现问题时,它允许代码不直接处理错误,除非它在代码中的位置要处理错误,从而导致整体代码更清晰,但是你的缺点是如果你不熟悉那些可能被抛出的东西,你有时候可能会被抛出异常。但这也意味着报告的错误不会像错误代码一样被遗漏。如果你忘记处理异常,你会在发生异常时知道它,而对于类似错误代码的东西,它很容易忘记检查它或者没有意识到你需要,并且错误可能是错过。因此,虽然意外的异常可能很烦人,但它们有助于确保在程序发生时捕获程序中的问题。
现在,使用断言与异常的最佳时间可能是一种艺术。例如,使用契约设计,您可以使用断言来检查函数的输入,因为任何使用无效参数调用该函数的代码都违反了合同,因此被认为是错误的,而在防御性编程中,您不需要。假设输入有效,所以函数总是检查它的输入(不仅仅是在不使用-release进行编译时),并且在失败时抛出Exception
。哪种方法更有意义取决于您正在做什么以及该功能的输入可能来自何处。但是,使用断言来检查用户输入或程序控制之外的任何内容都是不合适的,因为错误的输入不是程序中的错误。
然而,虽然一般来说,在D中处理错误情况的惯用方法可能是抛出异常,有时候真的没有意义。例如,如果错误条件实际上极有可能发生,抛出异常是一种非常昂贵的处理方式。对于一直没有发生的情况,异常通常足够快,但对于经常发生的事情 - 特别是在性能关键代码中 - 它们可能过于昂贵。在这种情况下,执行类似错误代码的操作可能更有意义 - 或者执行类似返回Nullable
并在函数无法获得结果时将其设为null的操作。
一般情况下,当假设函数成功和/或简化代码以使其不必担心错误时,异常最有意义。条件。
例如,想象一下编写一个使用错误代码而不是异常的XML解析器。其实现中的每个函数都必须检查它调用的任何函数是否成功并返回它本身是否成功,这不仅容易出错,而且意味着你在整个过程中基本上都有错误处理代码。解析器。另一方面,如果使用异常,那么大多数解析器都不必关心XML中的错误。代替遇到无效XML必须返回调用它的函数必须处理的错误代码的代码,它只能抛出异常,而调用链中的任何代码实际上都是处理错误的好地方(可能是代码)然后,它首先调用解析器)是唯一必须处理错误的代码。程序中唯一的错误处理代码是代码,需要处理错误而不是大多数程序。这样的代码很多更干净。
异常真正清理代码的另一个例子是像std.file.isDir
这样的函数。它返回它给出的文件名是否对应于目录,并在出现问题时抛出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
获得的,那么你不必费心检查文件是否存在。无论哪种方式,结果都比处理错误代码更清晰,更不容易出错。
在任何情况下,最合适的解决方案取决于您的代码正在做什么,并且有些情况下异常真的没有意义,但一般来说,它们是处理非错误的惯用方法程序中的错误或内存不足等灾难性错误,Error
通常是处理遇到程序中的错误或灾难性错误的最佳方法。最终,知道何时以及如何使用异常与其他技术相比,这是一种艺术,并且通常需要经验才能有一种良好的感觉,这是为什么有关何时使用异常,断言和错误的问题的一部分代码会不时弹出。