“尝试……抓住……最终”构造的“最终”部分是否必要?


25

某些语言(例如C ++和PHP的早期版本)不支持构造的finally一部分try ... catch ... finally。有finally必要吗?因为其中的代码始终运行,所以为什么不/不应该将代码放在try ... catch没有finally子句的块之后?为什么要用一个?(我正在寻找使用/不使用的原因/动机finally,不是取消“捕获”的理由,或者这样做的合法性。)


评论不作进一步讨论;此对话已转移至聊天
maple_shaft

Answers:


36

除了其他人所说的之外,在catch子句中也可能引发异常。考虑一下:

try { 
    throw new SomeException();
} catch {
    DoSomethingWhichUnexpectedlyThrows();
}
Cleanup();

在此示例中,该Cleanup()函数永远不会运行,因为在catch子句中引发了异常,并且调用堆栈中的下一个最高捕获将捕获该异常。使用finally块可以消除这种风险,并使代码更干净地引导。


4
感谢您提供的简洁直接的答案,不会脱离理论,并且“ X语言胜过Y语言”领域。
Agi Hammerthief,2015年

56

正如其他人提到的那样,try除非您捕获所有可能的异常,否则无法保证语句后的代码将执行。也就是说:

try {
   mightThrowSpecificException();
} catch (SpecificException e) {
   handleError();
} finally {
   cleanUp();
}

可以改写为1

try {
   mightThrowSpecificException();
} catch (SpecificException e) {
   try {
       handleError();
   } catch (Throwable e2) {
       cleanUp();
       throw e2;
   }
} catch (Throwable e) {
   cleanUp();
   throw e;
}
cleanUp();

但是后者要求您捕获所有未处理的异常,复制清除代码,并记住重新抛出。因此finally没有必要,但很有用

C ++没有,finally因为Bjarne Stroustrup认为RAI​​I更好,或者至少在大多数情况下就足够了:

C ++为什么不提供“最终”构造?

因为C ++支持几乎总是更好的替代方法:“资源获取是初始化”技术(TC ++ PL3第14.4节)。基本思想是用本地对象表示资源,以便本地对象的析构函数将释放该资源。这样,程序员就不会忘记释放资源。


1用于捕获所有异常并重新抛出而不丢失堆栈跟踪信息的特定代码因语言而异。我使用过Java,在创建异常时捕获了堆栈跟踪。在C#中,您只需使用throw;


8
handleError()在第二种情况下,您还必须捕获异常,不是吗?
朱里·罗伯

1
您可能还会抛出一个错误。我要改写一下catch (Throwable t) {},在整个初始块周围尝试.. catch块(也可以从中捕获可handleError
抛物

1
实际上,我会添加调用时省略的多余try-catch,handleErro();这将使它更好地说明为什么finally块很有用(即使这不是原始问题)。
Alex

1
这个答案并没有真正解决C ++为什么没有的问题finally,这更加细微。
DeadMG

1
@AgiHammerthief嵌套在特定异常的try内部。其次,您可能不知道在检查了异常之前是否能够成功处理错误,或者异常原因也阻止了您处理错误(至少在该级别)。在执行I / O时,这是相当普遍的。重新抛出是因为可以保证运行的唯一方法是捕获所有内容,但是原始代码将允许源自块的异常向上传播。catchcleanUpcatch (SpecificException e)
2015年

22

finally 块通常用于清除资源,当使用多个return语句时,这些资源有助于提高可读性:

int DoSomething() {
    try {
        open_connection();
        return get_result();
    }
    catch {
        return 2;
    }
    finally {
        close_connection();
    }
}

int DoSomething() {
    int result;
    try {
        open_connection();
        result = get_result();
    }
    catch {
        result = 2;
    }
    close_connection();
    return result;
}

2
我认为这是最好的答案。用finally代替通用异常似乎很糟糕。正确的用例是清理资源或类似的操作。
Kik 2015年

3
也许更常见的是在try块内返回,而不是在catch块内返回。
迈克尔·安德森

在我看来,代码无法充分说明的使用finally。(我会在第二个块中使用代码,因为不鼓励在我工作的地方使用多个return语句。)
Agi Hammerthief 2015年

15

正如您显然已经推测的那样,是的,C ++在没有该机制的情况下提供了相同的功能。因此,严格来说,try/ finally机制并不是必需的。

就是说,如果没有它,确实对其余语言的设计方式提出了一些要求。在C ++中,相同的一组动作体现在类的析构函数中。这主要(唯一地)起作用,因为C ++中的析构函数调用是确定性的。反过来,这导致了一些有关对象生存期的相当复杂的规则,其中某些规则显然是不直观的。

其他大多数语言都提供某种形式的垃圾回收。尽管关于垃圾回收的某些事情存在争议(例如,相对于其他内存管理方法的效率),但通常没有一件事:垃圾回收器“清理”对象的确切时间并没有直接联系在一起。到对象的范围。当清理需要确定性地进行清理,或者只是为了进行正确操作而需要清理时,或者在处理非常宝贵的资源以至于大多数清理不会被任意延迟时,这将阻止使用清理。try/ finally为此类语言提供了一种方法,以处理需要确定性清除的情况。

我认为那些声称使用此功能的C ++语法比Java的“不友好”的人更没有抓住重点。更糟糕的是,他们缺少关于责任划分的更重要的要点,这远远超出了语法,并且与代码的设计有很大关系。

在C ++中,这种确定性清除发生在对象的析构函数中。这意味着可以(通常应该)将对象设计为在其自身之后进行清理。这符合面向对象设计的本质-应该设计一个类以提供抽象,并强制其自己的不变式。在C ++中,精确地做到了这一点-它提供的不变式之一是,当对象被销毁时,由该对象控制的资源(全部,而不仅仅是内存)将被正确销毁。

Java(和类似的)有些不同。尽管他们确实提供了某种支持finalize,这些支持在理论上可以提供类似的功能,但是这种支持是如此的薄弱,以至于它基本上是不可用的(实际上,基本上从未使用过)。

结果,类的客户端需要采取步骤来执行此操作,而不是使类本身能够执行所需的清除操作。如果我们做一个比较短视的比较,乍一看似乎这种差异很小,并且Java在这方面与C ++相当有竞争力。我们最终得到这样的结果。在C ++中,该类如下所示:

class Foo {
    // ...
public:
    void do_whatever() { if (xyz) throw something; }
    ~Foo() { /* handle cleanup */ }
};

...而客户端代码如下所示:

void f() { 
    Foo f;
    f.do_whatever();
    // possibly more code that might throw here
}

在Java中,我们交换了一些代码,而在类中使用该对象的代码却少了一些。最初,这似乎是一个相当平衡的选择。实际上,它远非如此,因为在大多数典型的代码中,我们只在一个地方定义类,但是在很多地方使用它。C ++方法意味着我们只编写该代码以在一处处理清理工作。Java方法意味着我们必须在许多地方编写该代码以多次进行清理-在每个地方我们都使用该类的对象。

简而言之,Java方法基本上保证了我们尝试提供的许多抽象是“泄漏的”-任何需要确定性清除的类都必须使该类的客户端知道清除内容以及如何执行清除的详细信息,而不是将这些细节隐藏在类本身中。

尽管我在上面将其称为“ Java方法”,但try/ finally和其他名称下的类似机制并不完全限于Java。举一个著名的例子,大多数(全部?)。NET语言(例如C#)都提供了相同的语言。

在这方面,Java和C#的最新迭代在“经典” Java和C ++之间也提供了一个中间点。在C#中,想要自动进​​行清理的对象可以实现该IDisposable接口,该接口提供了Dispose(至少在某种程度上)类似于C ++析构函数的方法。尽管可以通过Java中的try/ 来使用它finally,但是C#使用一条语句使任务自动化了一点,该using语句使您可以定义将在输入范围时创建的资源,并在退出范围时销毁资源。尽管仍远远低于C ++提供的自动化和确定性级别,但这仍然是对Java的实质性改进。特别是,班级设计人员可以集中如何处理类的实现IDisposable。留给客户程序员的是减轻编写using语句以确保应在IDisposable适当的时候使用该接口的负担。在Java 7及更高版本中,名称已更改以保护罪名,但基本思想基本相同。


1
完美的答案。析构函数是C ++ 中必备功能。
托马斯·爱丁

13

不能相信没有其他人提出了这个(没有双关语意) -你不必需要一个条款!

这是完全合理的:

try 
{
   AcquireManyResources(); 
   DoSomethingThatMightFail(); 
}
finally 
{
   CleanUpThoseResources(); 
}

没有赶上条款的任何地方是哪里,因为这种方法不能做任何有用的那些例外; 他们留下来传播回调用堆栈的处理程序,即可。在每种方法中捕获和重新抛出异常是一个坏主意,特别是如果您只是重新抛出相同的异常。这完全违背了结构化异常处理是如何应该工作(和是非常接近的每一个方法返回一个“错误代码”,只是在一个异常的“形状”)。

什么这个方法确实需要做的,虽然,它清理后本身,让“外面的世界”永远需要了解的烂摊子,它本身钻进任何东西。最终条款就是这样做的-无论调用的方法的行为,最终条款将以“的出路”的方法的执行(和同样适用于每一个在其抛出异常,并且点之间finally从句处理它的最终catch子句);每个都在调用栈“展开”时运行。


9

如果引发了您不期望的异常,将会发生什么。尝试将在其中途退出,并且不执行catch子句。

最后一个步骤是帮助您完成此任务,并确保无论发生什么异常都将进行清理。


4
这不是使用的充分理由finally,因为您可以防止“意外”异常catch(Object)catch(...)包罗万象。
MSalters 2015年

1
听起来像是在变通。从概念上讲,最后是更清洁的方法。尽管我必须承认很少使用它。
quick_now 2015年

7

一些语言为它们的对象提供构造函数和析构函数(例如,我相信C ++)。使用这些语言,您可以执行finally析构函数中通常执行的大多数操作(可能是全部操作)。因此,在那些语言中,finally子句可能是多余的。

在没有析构函数的语言(例如Java)中,如果没有该finally子句,则很难(甚至是不可能)实现正确的清除。注意:在Java中有一种finalise方法,但不能保证会被调用。


可能需要指出的是,在确定破坏性的情况下,析构函数有助于清理资源。如果我们不知道何时将销毁和/或垃圾回收对象,则销毁器不够安全。
Morwenn 2015年

@Morwenn-好点。我在提到Java时就暗示了这一点,finalise但我现在不希望进入有关析构函数/最终确定的政治争论。
OldCurmudgeon 2015年

在C ++中,销毁是确定性的。当包含自动对象的范围退出时(例如,将其弹出堆栈),将调用其析构函数。(C ++允许您在堆栈上分配对象,而不仅仅是堆。)
Rob K

@RobK-这是a的确切功能,finalise但具有可扩展的味道和类似oop的机制-非常具有表现力,可与finalise其他语言的机制媲美。
OldCurmudgeon 2015年

1

最终尝试和尝试捕获是两个仅共享“ try”关键字的不同事物。我个人希望看到不同的地方。一起看到它们的原因是,异常会产生“跳跃”。

即使编程流程跳出,最终尝试也被设计为运行代码。无论是因为例外还是其他原因。这是一种获取资源并确保无需担心跳转就将其清理干净的好方法。


3
在.NET中,它们是使用单独的机制实现的;但是,在Java中,JVM识别的唯一构造在语义上等效于“错误时跳转”,该模式直接支持try catch但不支持try finally;通过将finally块的内容复制到可能需要执行的代码的所有位置,可以将使用后者的代码转换为仅使用前者的代码。
2015年

@supercat很好,谢谢您有关Java的额外信息。
Pieter B

1

由于此问题并未将C ++指定为一种语言,因此我将考虑C ++和Java的混合使用,因为它们采用了不同的方法来销毁对象,因此建议将其作为替代方法之一。

您可能使用finally块而不是try-catch块之后的代码的原因

  • 您可以从try块提前返回:考虑一下

    Database db = null;
    try {
     db = open_database();
     if(db.isSomething()) {
       return 7;
     }
     return db.someThingElse();
    } finally {
      if(db!=null)
        db.close();
    }
    

    和....相比:

    Database db = null;
    int returnValue = 0;
    try {
     db = open_database();
     if(db.isSomething()) {
       returnValue = 7;
     } else {
       returnValue = db.someThingElse();
     }
    } catch(Exception e) {
      if(db!=null)
        db.close();
    }
    return returnValue;
    
  • 您从catch块提前返回:比较

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      return 7;
    } catch (DBIsADonkeyException e ) {
      return 11;
    } finally {
      if(db!=null)
        db.close();
    }
    

    vs:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      if(db!=null) 
        db.close();
      return 7;
    } catch (DBIsADonkeyException e ) {
      if(db!=null)
        db.close();
      return 11;
    }           
    db.close();
    
  • 您抛出异常。比较:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      throw convertToRuntimeException(e,"DB was wonkey");
    } finally {
      if(db!=null)
        db.close();
    }
    

    vs:

    Database db = null;
    try {
     db = open_database();
     db.doSomething();
    } catch (DBIntegrityException e ) {
      if(db!=null)
        db.close();
      throw convertToRuntimeException(e,"DB was wonkey");
    } 
    if(db!=null)
      db.close();
    

这些示例看起来并不算太糟糕,但是通常您会遇到其中几种情况的交互,并且正在使用一种以上的异常/资源类型。finally可以帮助您避免使代码成为纠结的维护噩梦。

现在,在C ++中,可以使用基于范围的对象来处理这些对象。但是IMO对于这种方法有两个缺点:1.语法不太友好。2.建造顺序与破坏顺序相反,这会使事情变得不清楚。

在Java中,您无法挂钩finalize方法来进行清理,因为您不知道何时会发生-(好吧,但这就是一条充满有趣竞争条件的路径-JVM在决定何时销毁它方面有很大的范围事情-通常不是您期望的那样-比您预期的要早或更晚-并且可能随着热点编译器的启动而改变...


1

在编程语言中,所有在逻辑上“必需”的指令是:

assignment a = b
subtract a from b
goto label
test a = 0
if true goto label

任何算法都只能使用上面的指令来实现,所有其他语言构造都可以使程序更易于编写,并且对其他程序员更易理解。

有关使用此类最小指令集的实际硬件,请参见老式计算机


1
您的回答当然是正确的,但我没有汇编代码。太痛苦了 我在问为什么要使用一种我看不到的功能来支持它,而不是某种语言的最低限度的指令集是什么。
Agi Hammerthief,2015年

1
关键是实现这5种操作的任何语言都可以实现任何算法-尽管相当曲折。如果目标仅仅是实现一种算法,那么大多数高级语言的vers / operator并不是“必需的”。如果目标是快速开发可读的可维护代码,则大多数都是必需的,但是“可读的”和“可维护的”是不可测量的并且非常主观。漂亮的语言开发人员引入了许多功能:如果您对某些功能没有用处,则不要使用它们。
James Anderson

0

实际上,对我来说更大的差距通常是在支持finally但缺少析构函数的语言中,因为您可以在中央级别通过析构函数对与“清理”(我将分为两类)相关的所有逻辑进行建模,而无需手动处理清理每个相关功能中的逻辑。当我看到C#或Java代码执行诸如手动解锁互斥锁和关闭finally块中文件的操作时,当所有这些通过析构函数在C ++中以使人免于承担责任的方式自动化时,感觉就像C代码一样过时了。

但是,如果包含C ++,我仍然会觉得很方便finally,这是因为有两种清除类型:

  1. 销毁/释放/解锁/关闭/等本地资源(析构函数非常适合此操作)。
  2. 撤消/减少外部副作用(使用析构函数就足够了)。

第二个问题至少没有直观地映射到资源销毁的想法,尽管您可以使用范围保护很好地做到这一点,范围保护可以在提交变更之前将其销毁时自动回滚更改。这里finally可以说是提供至少稍微(只是一个很小位)的工作比范围卫士更简单的机制。

但是,一个更直接的机制是一个rollback块,这是我以前从未用任何一种语言见过的。如果我曾经设计一种涉及异常处理的语言,那简直就是我梦pipe以求的事情。它类似于:

try
{
    // Cause external side effects. These side effects should
    // be undone if we don't finish successfully.
}
rollback
{
    // Reverse external side effects. This block is *only* executed 
    // if the 'try' block above faced a premature return out 
    // of the function. It is different from 'finally' which 
    // gets executed regardless of whether or not the function 
    // exited prematurely. This block *only* gets executed if we 
    // exited prematurely from  the try block so that we can undo 
    // whatever side effects it failed to finish making. If the try 
    // block succeeded and didn't face a premature exit, then we 
    // don't want this block to execute.
}

这是建模副作用回滚的最直接方法,而析构函数几乎是本地资源清理的理想机制。现在,它仅从范围卫士解决方案中节省了几行代码,但是我想在其中看到一种语言的原因是副作用回滚往往是异常处理中最被忽略(但最棘手)的方面围绕可变性的语言。我认为此功能会鼓励开发人员在功能引起副作用且无法完成时回滚事务时考虑异常处理的正确方法,并且,当人们看到正确执行回滚有多困难时,这是一个附带的好处,他们可能会倾向于首先编写没有副作用的更多功能。

在某些模糊的情况下,无论退出函数如何退出,无论它如何退出,您都只想做其他事情,例如记录时间戳。finally可以说这是最简单,最完美的解决方案,因为尝试实例化一个对象仅将其析构函数用于记录时间戳的唯一目的确实感觉很奇怪(即使您可以使用lambda很好且很方便地做到这一点) )。


-9

与C ++语言的许多其他异常情况一样,缺少try/finally构造也是设计缺陷,如果您甚至可以将其称为一种似乎经常没有进行任何实际设计工作的语言。

RAII(在基于堆栈的对象上使用基于范围的确定性析构函数调用进行清理)有两个严重缺陷。首先是它要求使用基于堆栈的对象,这是违反Liskov替换原理的可憎对象。有很多充分的理由说明为什么在C ++之前或之后没有其他OO语言可以在epsilon中使用;D并不重要,因为D很大程度上基于C ++并且没有市场份额-并且解释它们引起的问题超出了此答案的范围。

其次,finally可以做的是对象破坏的超集。在C ++中使用RAII所做的大部分工作都将以Delphi语言描述,该语言没有垃圾回收,具有以下模式:

myObject := MyClass.Create(arguments);
try
   doSomething(myObject);
finally
   myObject.Free();
end;

这是明确的RAII模式;如果您要创建一个仅包含与上面第一行和第三行等效的C ++例程,那么编译器将生成的内容最终将类似于我在其基本结构中编写的内容。而且,由于它try/finally是C ++提供的唯一访问构造的方法,因此C ++开发人员最终对以下观点持一种近视的看法try/finally:当您拥有的只是一把锤子时,可以这么说,一切开始看起来像是析构函数。

但是,经验丰富的开发人员还可以使用finally构造进行其他操作。这与确定性破坏无关,即使面对提出异常的情况也是如此。它是关于确定性代码的执行,即使遇到异常时也是如此。

在Delphi代码中,您可能会经常看到另一件事:绑定了用户控件的数据集对象。数据集保存来自外部源的数据,控件反映数据的状态。如果您打算将一堆数据加载到数据集中,则需要临时禁用数据绑定,以免对UI造成奇怪的影响,并尝试使用输入的每条新记录不断地对其进行更新。 ,因此您可以这样编写:

dataset.DisableControls();
try
   LoadData(dataset);
finally
   dataset.EnableControls();
end;

显然,这里没有物体被摧毁,也不需要一个。该代码简单,简洁,明确和高效。

用C ++怎么做?好吧,首先您必须编写整个课程。它可能会被称为DatasetEnabler或类似的东西。它的全部存在将作为RAII的帮助者。然后,您需要执行以下操作:

dataset.DisableControls();
{
   raiiGuard = DatasetEnabler(dataset);
   LoadData(dataset);
}

是的,必须使用那些显然多余的花括号来管理适当的作用域,并确保立即(而不是在方法结束时)重新启用数据集。因此,最终得到的代码不会花费更少的代码行(除非您使用埃及括号)。它需要创建一个多余的对象,这会产生开销。(不是C ++代码应该很快吗?)它不是显式的,而是依赖于编译器魔术。在此方法中没有描述执行的代码,而是驻留在一个完全不同的类中,可能位于一个完全不同的文件中。简而言之,这绝对不是能够自己编写try/finally块的更好的解决方案。

这种问题在语言设计中很常见,因此有个名字:抽象反转。 当在低层结构之上构建高层结构,然后在语言中不直接支持低层结构时,就会发生这种情况,这需要那些希望使用它的人根据语言重新实现它。高级别的构造,通常会严重损害代码的可读性和效率。


评论旨在澄清或改善问题和答案。如果您想对此答案进行讨论,请访问聊天室。谢谢。
maple_shaft
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.