我可能会调用Pythonistas的愤怒(不知道,因为我不使用Python多),或者从这样的回答其他语言的程序员,但在我看来,大多数的功能应该不会有catch
块,在理想情况下。为了说明原因,让我将其与在80年代末和90年代初使用Turbo C时必须进行的那种手动错误代码传播进行对比。
假设我们有一个加载图像或类似功能的功能,以响应用户选择要加载的图像文件,它是用C和汇编语言编写的:
我省略了一些低级函数,但是我们可以看到,根据它们对错误处理的职责,已经识别出不同类别的函数,并用颜色进行了编码。
故障点和恢复点
现在,不难编写我称之为“可能的故障点”(throw
即故障)和“错误恢复与报告”功能(即故障)的功能类别catch
。
在可用异常处理之前,这些函数总是很难正确地编写,因为可能会遇到外部故障(例如,无法分配内存)的函数仅会返回NULL
或0
或-1
或设置全局错误代码或类似的结果。错误恢复/报告始终很容易,因为一旦您沿调用堆栈向下移动到可以恢复和报告故障的地步,您只需获取错误代码和/或消息并将其报告给用户。很自然,这个层次结构的叶子上的函数无论将来如何更改都永远不会失败(Convert Pixel
),对于正确编写(至少在错误处理方面)而言,它简直太简单了。
错误传播
但是,容易发生人为错误的繁琐功能是错误传播程序,它们不会直接导致失败,而是称为可能在层次结构中更深处失败的功能。此时,Allocate Scanline
可能必须处理malloc
的错误Convert Scanlines
,然后Convert Scanlines
将错误返回到,然后必须检查该错误,然后将其传递到Decompress Image
,然后Decompress Image->Parse Image
,和Parse Image->Load Image
,以及Load Image
最终报告该错误的用户端命令。 。
这是很多人犯错误的地方,因为只有一个错误传播者才能检查并传递错误,以使整个函数层次结构在正确处理错误时得以推倒。
此外,如果错误代码是由函数返回的,那么我们成功地会在90%的代码库中失去成功返回感兴趣的值的能力,因为如此多的函数将必须保留其返回值才能在函数中返回错误代码。失败。
减少人为错误:全局错误代码
那么如何减少人为错误的可能性?在这里,我什至可能会引起一些C程序员的愤怒,但我认为直接的改进是使用全局错误代码,例如OpenGL与glGetError
。这至少释放了函数成功时返回有意义的兴趣值的能力。有许多方法可以使错误代码本地化到线程,从而使该线程安全且高效。
在某些情况下,某个函数可能会遇到错误,但是由于发现先前的错误而使它过早返回之前,继续运行一段时间会相对无害。这样就可以发生这种情况,而不必针对每个函数中的90%的函数调用检查错误,因此它仍然可以进行适当的错误处理而不必太细致。
减少人为错误:异常处理
但是,上述解决方案仍然需要很多功能来处理手动错误传播的控制流程方面,即使它可能减少了手动if error happened, return error
类型代码的行数。不能完全消除它,因为仍然经常需要至少一个位置检查错误并为几乎每个错误传播函数返回。因此,这是图片中加入异常处理以节省一天的时间(排序)。
但是,这里异常处理的价值在于释放了处理手动错误传播的控制流方面的需求。这意味着它的价值与避免在catch
整个代码库中编写大量的块的能力紧密相关。在上图中,唯一必须有一个catch
块的Load Image User Command
位置是报告错误的位置。理想情况下,catch
其他任何事情都不需要做,因为否则它将开始变得像错误代码处理一样乏味且容易出错。
因此,如果您问我,如果您的代码库真正可以从优雅的异常处理中受益,那么它应该具有最少的catch
块数(至少我不是指零,而对于每个唯一的高位块来说更像一个)最终用户操作可能失败,如果通过中央命令系统调用所有高端用户操作,则失败的次数甚至更少。
资源清理
但是,异常处理仅解决了避免手动处理与正常执行流分开的异常路径中错误传播的控制流方面的需求。通常,即使现在使用EH自动执行此功能,也可以充当错误传播者,但该功能可能仍会获取一些需要销毁的资源。例如,这样的函数可能会打开一个临时文件,无论该文件是什么,都需要先关闭它,然后再从该函数返回,或者锁定一个互斥量,无论如何它都需要解锁。
为此,我可能会从各种语言中引起很多程序员的愤怒,但我认为采用C ++的方法是理想的选择。该语言引入了析构函数,这些析构函数在对象超出范围时即以确定性方式被调用。因此,C ++代码(例如,通过析构函数通过作用域互斥对象锁定互斥对象)无需手动解锁,因为一旦对象超出范围,无论发生什么情况(即使发生异常,都会自动将其解锁)。遇到)。因此,实际上完全不需要编写良好的C ++代码来处理本地资源清理。
在缺少析构函数的语言中,他们可能需要使用一个finally
块来手动清理本地资源。就是说,如果您不必catch
到处都是异常的情况,那么仍然需要手动进行错误传播来解决代码。
扭转外部副作用
这是在解决最困难的观念问题。如果有任何功能(无论是错误传播者还是故障点)引起外部副作用,那么它需要回滚或“撤消”这些副作用,以使系统恢复为从未发生过的状态,而不是“半有效”状态表示操作成功。我不知道有哪一种语言可以使此概念性问题变得更加容易,除了那些可以简单地减少大多数功能需要首先引起外部副作用的语言(例如围绕不变性和持久性数据结构的功能性语言)。
在finally
围绕可变性和副作用的语言中,这可以说是解决问题的最优雅的解决方案之一,因为这种类型的逻辑通常非常特定于特定的函数,并且与“资源清理”的概念映射得不太好。 ”。并且我建议finally
在这些情况下使用宽泛的代码,以确保函数能够以支持它的语言来消除副作用,而不管您是否需要一个catch
块(同样,如果您问我,编写良好的代码应具有的最小数量)。catch
块,所有catch
块都应放在最有意义的位置,如上图所示Load Image User Command
。
梦语
但是,IMO finally
对于副作用逆转而言接近理想状态,但还不是很理想。我们需要引入一个boolean
变量来在过早退出(从抛出的异常或其他异常退出)的情况下有效回滚副作用,如下所示:
bool finished = false;
try
{
// Cause external side effects.
...
// Indicate that all the external side effects were
// made successfully.
finished = true;
}
finally
{
// If the function prematurely exited before finishing
// causing all of its side effects, whether as a result of
// an early 'return' statement or an exception, undo the
// side effects.
if (!finished)
{
// Undo side effects.
...
}
}
如果我能设计一种语言,那么解决这个问题的理想方法就是将上述代码自动化:
transaction
{
// Cause external side effects.
...
}
rollback
{
// This block is only executed if the above 'transaction'
// block didn't reach its end, either as a result of a premature
// 'return' or an exception.
// Undo side effects.
...
}
...带有析构函数以自动清理本地资源,因此我们只需要transaction
,rollback
和catch
(尽管我可能仍想补充finally
说,例如使用不清理自己的C资源)。但是,finally
使用boolean
变量是最简单的方法,因为到目前为止我发现我缺少梦想的语言。我发现的第二个最直接的解决方案是C ++和D等语言中的范围卫士,但是我在概念上总是发现范围卫士有点尴尬,因为它模糊了“资源清理”和“副作用反转”的思想。我认为这些是非常不同的想法,需要以不同的方式加以解决。
我对语言的小梦想也将极大地围绕不变性和持久性数据结构,以使其(尽管并非必需)更容易编写高效的函数,即使该函数导致没有副作用。
结论
因此,不管怎么说,try/finally
考虑到Python没有C ++的析构函数,我认为关闭套接字的代码很好并且很好,而且我个人认为应该将它自由地用于需要消除副作用的地方并尽量减少您需要去catch
的地方。