为什么使用try…最终没有catch子句?


79

经典的编程方式是使用try ... catch。什么时候适合使用trycatch

在Python中,以下内容看起来合法且有意义:

try:
  #do work
finally:
  #do something unconditional

但是,代码没有catch任何内容。类似地,在Java中可能会认为如下:

try {
    //for example try to get a database connection
}
finally {
  //closeConnection(connection)
}

看起来不错,突然间我不必担心异常类型等。如果这是一种好的做法,那么什么时候才是好的做法?另外,为什么这不是良好做法或不合法?(我没有编译源代码。我正在询问它,因为它可能是Java的语法错误。我检查了Python是否确实可以编译。)

我遇到的一个相关问题是:我继续编写函数/方法,最后必须返回一些内容。但是,它可能位于不应该到达的地方,必须是返回点。因此,即使我处理了上面的异常,我仍会NULL在代码中某些不应该到达的位置(通常是方法/函数的结尾)返回或返回一个空字符串。我一直设法对代码进行重组,以使它不必一定要进行重组return NULL,因为这看起来绝对不像是一种好习惯。


4
在Java中,为什么不将return语句放在try块的末尾?
凯文·克莱恩

2
我很遗憾try..finally和try..catch都使用try关键字,除了都以try开头之外,它们是2个完全不同的结构。
Pieter B

3
try/catch不是“经典的编程方式”。这是经典的C ++编程方式,因为C ++缺少适当的try / finally结构,这意味着您必须使用涉及RAII的丑陋技巧来实现有保证的可逆状态更改。但是体面的OO语言没有这个问题,因为它们提供了try / finally。它与try / catch的用途完全不同。
梅森·惠勒2013年

3
通过外部连接资源,我看到了很多。您需要例外,但需要确保您不留下打开的连接等。如果发现该异常,则在某些情况下,只要将其重新扔到下一层即可。
钻机

4
@MasonWheeler “丑陋的黑客”,请解释一下让对象处理自身清理有什么不好吗?
Baldrickk,2015年

Answers:


146

这取决于您是否可以处理此时可能引发的异常。

如果可以在本地处理异常,则应该这样做,并且最好将错误处理在尽可能接近引发错误的位置。

如果您不能在本地处理它们,那么仅使用一个try / finally块是完全合理的-假设无论该方法是否成功,都需要执行一些代码。例如(从Neil的评论中),打开一个流然后将该流传递给要加载的内部方法是一个很好的示例,说明何时需要try { } finally { }使用finally子句以确保无论成功或失败都关闭该流。读取失败。

但是,您仍然需要在代码中的某个地方使用异常处理程序-除非您当然希望您的应用程序完全崩溃。它取决于处理程序所在位置的应用程序体系结构。


8
“而且最好将错误尽可能地靠近错误产生的地方。” 嗯,这取决于。如果您可以恢复并仍然完成任务(可以这么说),那么当然可以。如果无法做到,最好让异常一直蔓延到顶部,在这种情况下(可能)需要用户干预来处理发生的事情。
2012年

13
@will-这就是为什么我使用短语“尽可能”的原因。
克里斯·弗雷德(ChrisF)

8
好的答案,但是我想举一个例子:打开一个流并将该流传递给要加载的内部方法是一个很好的例子,说明何时需要使用try { } finally { },利用finally子句确保最终关闭流,而不管是否成功/失败。
尼尔2012年

因为有时最上层的路尽可能近
Newtopian

“仅仅尝试一下/最终阻止是完全合理的”,正是在寻找这个答案。谢谢@ChrisF
尼尔AISE

36

finally块用于必须始终运行的代码,而无论是否发生错误条件(异常)。

finally块中的代码在try块完成后运行,如果发生捕获的异常,则在相应的catch块完成后运行。即使在trycatch块中发生未捕获的异常,它也始终运行。

finally块通常用于关闭在该try块中打开的文件,网络连接等。原因是文件或网络连接必须关闭,无论使用该文件或网络连接的操作成功还是失败。

在该finally块中应格外小心,以确保它本身不会引发异常。例如,请确保检查所有变量是否为null,等等。


8
+1:“必须清理”很常见。的大多数用法try-finally都可以用with语句代替。
S.Lott 2012年

12
各种语言对结构都有非常有用的特定于语言的增强try/finally。C#有using,Python有with
yfeldblum

5
@yfeldblum- using和之间存在细微的差异try-finally,因为如果对象的构造函数中发生异常,则该Dispose方法将不会被using块调用IDisposabletry-finally即使对象的构造函数引发异常,也允许您执行代码。
Scott Whitlock

5
@ScottWhitlock:这是一个很好的事情吗?您想做什么,在未构造的对象上调用方法?那是十亿种坏事。
DeadMG

1
@DeadMG:另请参阅stackoverflow.com/q/14830534/18192
Brian

17

在Java 中尝试不带catch子句的最终尝试是合适的(甚至更多,是惯用的),这是在并发实用程序locks包中使用Lock的示例。

  • 以下是在API文档中对其进行解释和说明的方式(引号中的粗体是我的):

    ...缺少块结构锁,消除了同步方法和语句发生的锁的自动释放。在大多数情况下,应使用以下惯用法

     Lock l = ...;
     l.lock();
     try {
         // access the resource protected by this lock
     } finally {
         l.unlock();
     }
    

    当锁定和解锁发生在不同的范围内时,必须小心以确保通过try-finally或try-catch保护在持有锁定时执行的所有代码,以确保在必要时释放锁定


我可以放进去l.lock()试试吗?try{ l.lock(); }finally{l.unlock();}
RMachnik 2015年

1
从技术上讲,您可以。我没有把它放在那里,因为从语义上讲,它意义不大。try本摘要中的内容旨在包装资源访问权限,为什么用与此无关的内容来污染资源访问权限
gnat 2015年

4
您可以,但如果l.lock()失败,则finally如果l.lock()try块位于该块内,则该块仍将运行。如果您按照gnat的建议进行操作,finally则只有在我们知道已获取锁后,该块才会运行。
Wtrmute

12

在一个基本水平catch,并finally解决两个相关但不同的问题:

  • catch 用于处理您调用的代码报告的问题
  • finally 用于清除当前代码创建/修改的数据/资源,无论是否发生问题

因此,两者都以某种方式与问题(异常)相关,但这几乎是它们的共同点。

一个重要的区别是,该finally必须使用与创建资源相同的方法(以避免资源泄漏),并且不能放在调用堆栈的不同级别上。

catch然是一个不同的问题:它的正确位置取决于在那里你可以实际处理的例外。在一个您无能为力的地方捕获异常是没有用的,因此有时最好还是让它通过。


2
Nitpick:“ ... finally块必须使用与创建资源相同的方法...”。这样做绝对是个好主意,因为更容易看到没有资源泄漏。但是,这不是必要的先决条件。即你没有必须这样做的。您可以在finally(静态或动态)封闭的try语句中释放资源...,并且仍然是100%防泄漏的。
Stephen C

6

@yfeldblum有一个正确的答案:最后,没有catch语句的try-finally通常应替换为适当的语言构造。

在C ++中,它使用RAII和构造函数/析构函数。在Python中,这是一个with声明;在C#中,这是一条using语句。

这些几乎总是更优雅,因为初始化和完成代码位于一个位置(抽象对象),而不是位于两个位置。


2
在Java中,它是ARM块
MarkJ 2012年

2
假设您有一个设计不良的对象(例如,一个在C#中未适当实现IDisposable的对象),但它并不总是可行的选择。
mootinator 2012年

1
@mootinator:您不能从设计不良的对象继承并修复它吗?
尼尔G

2
还是封装?呸。我的意思是,当然。
mootinator 2012年

4

在许多语言中,finally语句也会在return语句之后运行。这意味着您可以执行以下操作:

try {
  // Do processing
  return result;
} finally {
  // Release resources
}

不管方法如何以异常或常规return语句结束,都将释放资源。

这是好是坏尚待争论,但try {} finally {}并不总是限于异常处理。


0

我可能会调用Pythonistas的愤怒(不知道,因为我不使用Python多),或者从这样的回答其他语言的程序员,但在我看来,大多数的功能应该不会catch块,在理想情况下。为了说明原因,让我将其与在80年代末和90年代初使用Turbo C时必须进行的那种手动错误代码传播进行对比。

假设我们有一个加载图像或类似功能的功能,以响应用户选择要加载的图像文件,它是用C和汇编语言编写的:

在此处输入图片说明

我省略了一些低级函数,但是我们可以看到,根据它们对错误处理的职责,已经识别出不同类别的函数,并用颜色进行了编码。

故障点和恢复点

现在,不难编写我称之为“可能的故障点”(throw即故障)和“错误恢复与报告”功能(即故障)的功能类别catch

在可用异常处理之前,这些函数总是很难正确地编写,因为可能会遇到外部故障(例如,无法分配内存)的函数仅会返回NULL0-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.
    ...
}

...带有析构函数以自动清理本地资源,因此我们只需要transactionrollbackcatch(尽管我可能仍想补充finally说,例如使用不清理自己的C资源)。但是,finally使用boolean变量是最简单的方法,因为到目前为止我发现我缺少梦想的语言。我发现的第二个最直接的解决方案是C ++和D等语言中的范围卫士,但是我在概念上总是发现范围卫士有点尴尬,因为它模糊了“资源清理”和“副作用反转”的思想。我认为这些是非常不同的想法,需要以不同的方式加以解决。

我对语言的小梦想也将极大地围绕不变性和持久性数据结构,以使其(尽管并非必需)更容易编写高效的函数,即使该函数导致没有副作用。

结论

因此,不管怎么说,try/finally考虑到Python没有C ++的析构函数,我认为关闭套接字的代码很好并且很好,而且我个人认为应该将它自由地用于需要消除副作用的地方并尽量减少您需要去catch的地方。


-66

即使不是强制性的,也强烈建议捕获错误/异常并以整洁的方式处理它们。

我之所以这样说,是因为我相信每个开发人员都应该了解并解决其应用程序的行为,否则他不会适当地完成工作。在任何情况下,try-finally块都不会取代try-catch-finally块。

我将给您提供一个简单的示例:假设您已编写了用于在服务器上上传文件而不捕获异常的代码。现在,如果由于某种原因上传失败,客户端将永远不会知道出了什么问题。但是,如果您捕获了异常,则可以显示整洁的错误消息,说明发生了什么问题以及用户如何解决该问题。

黄金法则:总是捕获异常,因为猜测需要时间


22
-1:在Java中,可能需要finally子句来释放资源(例如,关闭文件或释放DB连接)。这与处理异常的能力无关。
凯文·克莱恩

@kevincline,他不是在问是否要使用finally,...他所要问的只是是否需要捕获异常。最重要的一部分,我们都知道这一点,所以它的使用....
潘卡Upadhyay

63
@Pankaj:您的回答建议,catch只要有,就应始终存在子句try。包括我在内的更多经验丰富的撰稿人都认为这是不明智的建议。您的推理有缺陷。包含的try方法不是捕获异常的唯一可能的地方。通常最简单,最好的做法是允许从最高级别捕获和报告异常,而不是在整个代码中复制catch子句。
凯文·克莱恩

@kevincline,我相信其他人对问题的看法有所不同。问题恰好是关于我们是否应该捕获例外……我在这方面回答了。可以通过多种方式而不是最终尝试来处理欺骗。但是,这不是OP的问题。如果提出例外对您和其他人足够好,那么我必须说好运。
Pankaj Upadhyay,2012年

除例外情况外,您希望中断语句的正常执行(并且无需在每个步骤中手动检查是否成功)。在深层嵌套的方法调用中尤其如此-在某个库中的方法4层不能仅仅“吞噬”异常;而是将方法嵌套在其中。它需要从所有层次上扔掉。当然,每一层都可以包装异常并添加其他信息。通常由于各种原因而无法做到这一点,额外的时间发展和不必要的冗长是最大的两个。
Daniel B
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.