「try…catch…finally」構文の「finally」部分は必要ですか?


25

一部の言語(C ++や初期バージョンのPHPなど)はfinallytry ... catch ... finally構成要素の一部をサポートしていません。でfinally、これまで必要?その中のコードは常に実行されるため、なぜ句のtry ... catchないブロックの後にそのコードを配置しない/しないのfinallyですか?なぜ使用するのですか?(私は、使用する/使用しない理由/動機を探しfinallyていますが、「キャッチ」をやめる理由や、そうすることが合法である理由ではありません。)


コメントは詳細なディスカッション用ではありません。この会話はチャットに移動さました
maple_shaft

回答:


36

他の人が言ったことに加えて、catch句内で例外をスローすることもできます。このことを考慮:

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

この例ではCleanup()、catch句で例外がスローされ、コールスタックの次に高いキャッチがそれをキャッチするため、関数は実行されません。finallyブロックを使用すると、このリスクが排除され、コードがよりクリーンに起動します。


4
理論と「言語XはYよりも優れている」領域に逸脱しない簡潔で直接的な回答をありがとう。
アギハマーシーフ

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でない必要が、それはです便利

Bjarne StroustrupはRAIIの方が優れていると考えているfinallyため、C ++にはありません。少なくともほとんどの場合はこれで十分です。

C ++が「最終的に」コンストラクトを提供しないのはなぜですか?

C ++は、ほぼ常に優れた代替手段をサポートしているため、「リソースの取得は初期化」手法(TC ++ PL3セクション14.4)です。基本的な考え方は、ローカルオブジェクトによってリソースを表現し、ローカルオブジェクトのデストラクタがリソースを解放することです。そうすれば、プログラマはリソースを解放することを忘れることができません。


1すべての例外をキャッチし、スタックトレース情報を失わずに再スローする特定のコードは、言語によって異なります。例外が作成されたときにスタックトレースがキャプチャされるJavaを使用しました。C#では、単に使用しますthrow;


8
handleError()2番目のケースでも例外をキャッチする必要がありますか?
ジュリロブル

1
エラーをスローすることもあります。私はそれを言い換えてcatch (Throwable t) {}、try .. catchブロックで初期ブロック全体をhandleError
囲み

1
呼び出し時に省略した余分なtry-catchを実際に追加します。handleErro();これにより、finallyブロックが有用である理由に関する議論がさらに良くなります(元の質問ではない場合でも)。
アレックス

1
この答えは、C ++になぜないのかという問題に実際には対応していませんfinally
DeadMG

1
ネストされた@AgiHammerthief try内にあるcatchため、特定の例外。第二に、例外を調べるまでエラーを正常に処理できるかどうか、または例外の原因によってエラーを処理できない(少なくともそのレベルでは)かどうかわからない可能性があります。I / Oを行う場合、これはかなり一般的です。cleanUp実行を保証する唯一の方法はすべてをキャッチすることであるため、再スローがありますが、元のコードでは、catch (SpecificException e)ブロックで発生した例外を上方に伝播できます。
ドーバル

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を使用するのはくだらないようです。正しい使用例は、リソースまたは類似の操作をクリーンアップすることです。
キック

3
おそらくさらに一般的なのは、catchブロックの内部ではなく、tryブロックの内部に戻ることです。
マイケルアンダーソン

私の考えでは、コードはの使用を適切に説明していませんfinally。(私が働いている場所では複数のreturnステートメントが推奨されていないため、2番目のブロックのようにコードを使用します。)
Agi Hammerthief

15

既に推測したように、はい、C ++はそのメカニズムなしで同じ機能を提供します。したがって、厳密に言えば、try/ finallyメカニズムは実際には必要ありません。

とは言っても、それなしで行うと、残りの言語の設計方法にいくつかの要件が課せられます。C ++では、同じアクションのセットがクラスのデストラクタに組み込まれています。C ++でのデストラクタ呼び出しは決定論的であるため、これは主に(排他的に?)動作します。これは、オブジェクトのライフタイムに関するかなり複雑なルールにつながり、そのいくつかは明らかに直感的ではありません。

他のほとんどの言語は、代わりに何らかの形式のガベージコレクションを提供します。ガベージコレクションに関して物議を醸すもの(たとえば、他のメモリ管理方法と比較した場合の効率)がありますが、一般的にはそうではありません。オブジェクトのスコープに。これにより、正しい操作に単純に必要な場合や、クリーンアップがwhen意的に遅延しないほど貴重なリソースを処理する場合に、クリーンアップを決定論的に行う必要がある場合に使用できなくなります。try/ finallyは、そのような言語がその決定論的なクリーンアップを必要とする状況に対処する方法を提供します。

この機能のC ++構文はJavaよりも「友好的ではない」と主張する人々は、むしろその点を失っていると思います。さらに悪いことに、彼らは、構文をはるかに超えた責任分担について、はるかに重要なポイントを見逃しており、コードの設計方法により多くのことが関係しています。

C ++では、この決定論的なクリーンアップはオブジェクトのデストラクタで行われます。つまり、オブジェクトはそれ自体をクリーンアップするように設計できます(通常は設計する必要があります)。これは、オブジェクト指向設計の本質です。クラスは、抽象化を提供し、独自の不変条件を強制するように設計する必要があります。C ++では、それを正確に行います。オブジェクトが破棄されると、オブジェクトによって制御されるリソース(メモリだけでなく、すべて)が正しく破棄されるという不変条件の1つです。

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では、クラス内でオブジェクトが少しだけ使用されるコードを少し交換します。これは当初、かなり均等なトレードオフのように見えます。しかし実際には、ほとんどの典型的なコードでは1か所でクラスを定義するだけで、多くの場所で使用しているため、そこからは程遠いものです。C ++アプローチとは、クリーンアップを1か所で処理するためのコードのみを記述することを意味します。Javaのアプローチとは、クリーンアップを何度も何度も処理するコードを記述する必要があることを意味します。あらゆる場所で、そのクラスのオブジェクトを使用するたびに。

要するに、Javaのアプローチは、基本的に、提供しようとする多くの抽象化が「漏れやすい」ことを保証します。決定論的なクリーンアップを必要とするすべてのクラスは、クラスのクライアントにクリーンアップの詳細とクリーンアップ方法を知る義務を負います、それらの詳細がクラス自体に隠されるのではなく。

上記では「Javaアプローチ」と呼んでいますが、try/ finallyおよび他の名前の同様のメカニズムはJavaに完全に制限されているわけではありません。顕著な例の1つとして、ほとんどの(すべて?).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(); 
}

このメソッドはこれらの例外に対して有用なことは何もできないため、どこにもcatch句はありません。それらは、コールスタックを伝播できるハンドラに伝播するために残されます。すべてのメソッドで例外をキャッチして再スローすることは、特に同じ例外を再スローするだけの場合、悪い考えです。それは完全に構造化例外処理がされる方法に反するはず(とある仕事にかなりちょうど例外の「形状」で、近くのすべてのメソッドから、「エラーコード」を返すことに)。

ただし、このメソッドが行う必要があるのは、それ自体をクリーンアップすることです。そのため、「外の世界」は、自身が入った混乱について何も知る必要がありません。finally節はちょうどことない-と呼ばれる方法がどのように動作するかに関係なく、finally節は方法の「外途中で」実行されます(そして同じことが当てはまりあるすべての例外がスローされた時点の間にfinally節とそれを処理する最終的なcatch句); 呼び出しスタックが「巻き戻される」たびに実行されます。


9

予期しない例外がスローされた場合はどうなりますか。試行は途中で終了し、catch句は実行されません。

最終ブロックは、それを支援し、例外に関係なくクリーンアップが発生することを保証することです。


4
これは、aの十分な理由ではありません。finally「予期しない」例外catch(Object)catch(...)キャッチオールを防ぐことができるからです。
–MSalters

1
これは回避策のように聞こえます。概念的には最終的にクリーンになります。私はめったにそれを使用しないと告白しなければなりませんが。
すぐに今すぐ

7

一部の言語では、オブジェクトのコンストラクタとデストラクタの両方を提供しています(C ++など)。これらの言語を使用するfinallyと、デストラクタで通常行われることのほとんど(ほぼすべて)を実行できます。そのようなものとして-それらの言語では- finally句は不要かもしれません。

デストラクタのない言語(Javaなど)では、finally句なしで正しいクリーンアップを実現することは困難です(不可能な場合もあります)。注意-Javaにはfinaliseメソッドがありますが、呼び出される保証はありません。


破壊が決定論的である場合、デストラクタがリソースのクリーニングを支援することに注意することは有用です。オブジェクトがいつ破棄またはガベージコレクションされるかわからない場合、デストラクタは十分に安全ではありません。
モーウェン

@Morwenn-良い点。私はJavaを参照してそれをほのめかしましたが、finalise現時点では、デストラクタ/ファイナリーに関する政治的議論に入らないことを望みます。
-OldCurmudgeon

C ++では、破壊は決定論的です。自動オブジェクトを含むスコープが終了すると(スタックからポップされるなど)、そのデストラクターが呼び出されます。(C ++では、ヒープだけでなくスタックにオブジェクトを割り当てることができます。)
ロブK

@RobK-これはaの正確な機能ですfinaliseが、拡張可能なフレーバーとoopのようなメカニズムの両方を備えています-非常に表現力がありfinalise、他の言語のメカニズムに匹敵します。
OldCurmudgeon

1

最後に試してみて、キャッチしてみてください: "try"キーワードのみを共有する2つの異なるもの。個人的に私はその違いを見たいと思います。それらを一緒に見る理由は、例外が「ジャンプ」を生成するためです。

そして、最終的にtryは、プログラミングフローが飛び出してもコードを実行するように設計されています。それが例外なのか他の理由によるのか。これは、リソースを取得し、ジャンプを心配することなくリソースをクリーンアップするための適切な方法です。


3
.NETでは、個別のメカニズムを使用して実装されます。ただし、Javaでは、JVMによって認識される唯一の構造は、「on error goto」、つまり直接サポートするtry catchがサポートしないパターンと意味的に同等try finallyです。後者を使用するfinallyコードは、実行する必要があるコードのすべての場所でブロックのコンテンツをコピーすることにより、前者のみを使用するコードに変換されます。
-supercat

@supercat素晴らしい、Javaに関する追加情報をありがとう。
ピーターB

1

この質問ではC ++を言語として指定していないので、C ++とJavaの混在を検討します。これらは代替手段の1つとして提案されているオブジェクト破棄に対する異なるアプローチを採用しているためです。

try-catchブロックの後のコードではなく、finallyブロックを使用する理由

  • 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();
    }
    

    対:

    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();
    }
    

    対:

    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にはこのアプローチに対する2つの欠点があります。1。構文はあまり使いやすいものではありません。2.建設の順序が破壊の順序の逆であると、物事が明確になりません。

Javaでは、いつ発生するかわからないため、ファイナライズメソッドをフックしてクリーンアップを実行することはできません。物事-あなたがそれを期待するときにしばしば-それはあなたが予想するよりも早くまたは遅く-そしてそれはホットスポットコンパイラが起動するにつれて変化する可能性があります...ため息...)


1

プログラミング言語で論理的に「必要」なのは、命令だけです。

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

上記の命令のみを使用して、任意のアルゴリズムを実装できます。他のすべての言語構成要素は、プログラムをより簡単に作成し、他のプログラマーが理解しやすくするためにあります。

このような最小限の命令セットを使用した実際のハードウェアについては、昔ながらの世界的なコンピューターを参照してください。


1
あなたの答えは確かに真実ですが、私はアセンブリでコーディングしていません。痛すぎる。私は、言語の最低限の命令セットが何であるかではなく、それをサポートする言語のポイントが表示されない機能を使用する理由を尋ねています。
アギハマーシーフ

1
重要なのは、これら5つの操作だけを実装する言語であれば、アルゴリズムを実装できるということです。高レベル言語のほとんどのvers / operatorsは、目標が単にアルゴリズムを実装することである場合、「必要」ではありません。読み取り可能な保守可能なコードの迅速な開発を目標とする場合、ほとんどが必要ですが、「読み取り可能」および「保守可能」は測定可能ではなく、非常に主観的です。優れた言語開発者は多くの機能を追加しました。一部の機能を使用できない場合は、使用しないでください。
ジェームズアンダーソン

0

実際、私にとって大きなギャップは、finallyデストラクタをサポートしているがデストラクタがない言語にあります。これは、手動でクリーンアップを処理することなく、デストラクタを通じて「クリーンアップ」(2つのカテゴリに分けます)に関連付けられたすべてのロジックをモデル化できるためです関連するすべての関数のロジック。手動でミューテックスのロックを解除し、finallyブロック内のファイルを閉じるなどのことを行うC#またはJavaコードを見ると、デストラクタを介してC ++で人間がその責任を解放するように自動化されていると、古くてCコードのように感じます。

ただし、C ++が含まれfinallyている場合でも、2種類のクリーンアップがあるため、まだ便利です。

  1. ローカルリソースの破棄/解放/ロック解除/クローズなど(デストラクタはこれに最適です)。
  2. 外部副作用の取り消し/ロールバック(これにはデストラクタが適切です)。

2番目の方法は、少なくともリソース破壊の概念にそれほど直観的には対応していませんが、コミット前に破壊された変更を自動的にロールバックするスコープガードを使用してうまく行うことができます。そこfinally間違いなく、少なくとも提供し、わずかに(ちょうどティーニービットで)スコープガードよりも仕事のためのより簡単なメカニズムを。

ただし、さらに簡単なメカニズムは、rollbackこれまでにどの言語でも見たことがないブロックです。例外処理を伴う言語を設計したことがあるなら、それは私の夢のようなものです。これは次のようになります。

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.
}

これは、副作用のロールバックをモデル化する最も簡単な方法ですが、デストラクタはローカルリソースのクリーンアップに最適なメカニズムです。スコープガードソリューションからコードを2、3行だけ保存するようになりましたが、これを使用して言語を表示したいのは、副作用のロールバックが例外処理の最も軽視されている(しかし最も扱いにくい)傾向があるためです可変性を中心に展開する言語で。この機能により、開発者は、関数が副作用を引き起こして完了に失敗するたびにトランザクションをロールバックするという観点から、適切な方法で例外処理を検討することを奨励すると思います。そもそも副作用のないより多くの関数を書くことを好むかもしれません。

また、タイムスタンプを記録するなど、関数の終了方法に関係なく、関数を終了する際にさまざまなことをしたいというあいまいなケースもあります。そこfinallyだけラムダとかなり便利なあなたはうまくそれを行うことができていても(本当に奇妙な感じとタイムスタンプだけをログに記録することを唯一の目的のためにそのデストラクタを使用するようにオブジェクトをインスタンス化しようとしているので、仕事のための最も簡単かつ完璧なソリューションは、間違いなくあります)。


-9

C ++言語に関する他の多くの異常なことと同様に、try/finallyコンストラクトの欠如は、実際の設計作業がまったく行われていないと思われる言語で呼び出すことができる場合、設計上の欠陥です。

RAII(クリーンアップのためのスタックベースのオブジェクトに対するスコープベースの決定論的なデストラクタ呼び出しの使用)には2つの重大な欠陥があります。1つ目は、スタックベースのオブジェクトの使用が必要なことです。これは、リスコフの代替原則に違反する忌まわしい行為です。イプシロン内では、C ++が以前またはそれ以降に他のオブジェクト指向言語を使用しなかった理由はたくさんあります。DはC ++に大きく基づいており、とにかく市場シェアがないため、カウントされません。そして、それらが引き起こす問題を説明することは、この答えの範囲を超えています。

第二に、finallyできることはオブジェクト破壊のスーパーセットです。C ++でRAIIを使用して行われることの多くは、Delphi言語で記述され、ガベージコレクションはありません。次のパターンがあります。

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

これは、明示されたRAIIパターンです。上記の1行目と3行目に相当するものだけを含むC ++ルーチンを作成する場合、コンパイラが生成するものは、基本構造で記述したもののようになります。そしてtry/finally、C ++が提供する構造体への唯一のアクセスであるため、C ++開発者はやや近視眼的な見方をtry/finallyすることになります:持っているものがすべてハンマーである場合、すべてはいわばデストラクタのように見え始めます。

しかし、経験豊富な開発者がfinallyコンストラクトでできることは他にもあります。例外が発生した場合でも、それは決定論的な破壊ではありません。例外が発生した場合でも、確定的なコード実行に関するものです。

Delphiコードでよく見られるもう1つのことは、ユーザーコントロールがバインドされたデータセットオブジェクトです。データセットは外部ソースからのデータを保持し、コントロールはデータの状態を反映します。大量のデータをデータセットにロードしようとしている場合、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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.