制御フローとしての例外は深刻なアンチパターンと見なされますか?もしそうなら、なぜですか?


129

90年代後半に戻って、フロー制御として例外を使用するコードベースでかなり作業しました。テレフォニーアプリケーションを駆動する有限状態マシンを実装しました。最近、MVC Webアプリをやっているので、当時のことを思い出します。

どちらにもController、次に進む場所を決定し、データを宛先ロジックに提供するものがあります。DTMFトーンなどの旧式の電話のドメインからのユーザーアクションは、アクションメソッドのパラメーターになりましたがViewResult、aのようなものを返す代わりに、をスローしましたStateTransitionException

主な違いは、アクションメソッドがvoid関数であったことだと思います。私はこの事実で私がしたことをすべて覚えていませんが、15年前のようにその仕事以来、私は他の仕事の生産コードでこれを見たことがないので、私は多くのことを覚える道を行くことをheしました。これはいわゆるアンチパターンであるという兆候だと思いました。

これは事実ですか?もしそうなら、なぜですか?

更新:質問をしたとき、@ MasonWheelerの回答をすでに念頭に置いていたので、私の知識に最も追加された回答を使用しました。彼も同様に正しい答えだと思います。



25
いいえ。Pythonでは、制御フローとして例外を使用することは「pythonic」と見なされます。
user16764

4
場合、私は私のJavaそんなことをしていた、私は確かにそれのために例外をスローしません。私は、いくつかの非例外、非エラー、スロー可能階層から派生します。
トーマスエディング

2
既存の答えに追加するために、ここに私に役立った短いガイドラインがあります:-「幸せな道」のために例外を決して使わないでください。ハッピーパスは、(Webの場合)リクエスト全体の場合もあれば、単に1つのオブジェクト/メソッドの場合もあります。もちろん、他のすべての正気なルールが適用されます:)
ホウエン

8
例外は常にアプリケーションのフローを制御していませんか?

回答:


109

これについての詳細な議論は、WardのWikiにあります。一般に、制御フローの例外の使用はアンチパターンであり、多くの注目すべき状況および言語固有の(たとえば Pythonを参照)cough exception coughがあります。

一般的に、それがアンチパターンである理由の簡単な要約として:

  • 例外は、本質的に、洗練されたGOTOステートメントです。
  • したがって、例外を使用したプログラミングは、コードの読み取りと理解をより困難にします
  • ほとんどの言語には、例外を使用せずに問題を解決するために設計された既存の制御構造があります
  • 効率性の議論は、現代のコンパイラーでは意味がありません。これは、制御フローに例外が使用されないという仮定で最適化される傾向があります。

より詳細な情報については、Wardのwikiでの議論を読んでください。


また、この質問の重複を参照してくださいここに


18
あなたの答えは、例外がすべての状況にとって悪いように聞こえますが、質問はフロー制御として例外に焦点を当てています。
-whatsisname

14
@MasonWheeler違いは、for / whileループにはフロー制御の変更が明確に含まれ、コードが読みやすくなることです。コードにforステートメントが表示されている場合、どのファイルにループの終わりが含まれているかを把握する必要はありません。後藤のものは悪くはありません。一部の神は彼らがそうであると言ったので、彼らは単にループ構造よりも従うのが難しいという理由で悪いです。例外も同様であり、不可能ではありませんが、物事を混乱させるほど困難です。
ビルK

24
@BillKは、それを主張し、例外がいかに簡単であるかについて単純化した文をやり過ぎないでください。
ウィンストンイーバート

6
わかりましたが、真剣に、JavaScriptで空のcatchステートメントでエラーを埋めているサーバー側とアプリの開発者はどうなっていますか?それは私に多くの時間を費やした厄介なフェノモンであり、私は暴言なしで尋ねる方法がわかりません。エラーはあなたの友達です。
エリックReppen

12
@mattnz:ifforeachも洗練されているGOTOの。率直に言って、gotoとの比較は役に立たないと思います。それはほとんど軽de的な用語のようなものです。GOTOの使用は本質的に悪ではありません-それには本当の問題があり、例外それらを共有するかもしれませんが、共有しないかもしれません。これらの問題を聞くことはより有益です。
イーモンネルボンヌ

110

例外が設計されたユースケースは、「私はそれを処理するのに十分なコンテキストがないため、この時点で適切に対処できない状況に遭遇したばかりですが、私を呼び出したルーチン(または呼び出しをさらに進めるもの)スタック)それを処理する方法を知っている必要があります。」

セカンダリユースケースは、「深刻なエラーが発生したばかりで、データの破損やその他の損傷を防ぐためにこの制御フローから抜け出すことは、先に進むよりも重要です」です。

これらの2つの理由のいずれかのために例外を使用していない場合は、おそらくより良い方法があります。


18
しかし、これは質問に答えません。彼らが設計されたものは無関係です。関連する唯一のことは、制御フローにそれらを使用することが悪い理由であり、これはあなたが触れなかったトピックです。例として、C ++テンプレートは1つの目的のために設計されましたが、メタプログラミングに使用するのにまったく問題ありません。これは、デザイナーが予期していなかった使用法です。
トーマスボニーニ

6
@Krelp:デザイナーは、偶然にチューリング完全なテンプレートシステムで終わるなど、多くのことを予期していませんでした C ++テンプレートは、ここで使用する良い例ではありません。
メイソンウィーラー

15
@Krelp-C ++テンプレートは、メタプログラミングには「完全に最適」ではありません。あなたがそれらを正しく理解するまで、彼らは悪夢です、そして、あなたがテンプレート天才でないならば、彼らは書き込み専用のコードに向かう傾向があります。より良い例を選んでください。
マイケルコーネ

例外は、特に関数の構成に害を及ぼし、一般にコードの簡潔さを害します。例えば、私はプロジェクトで例外を使用しましたが、1)人々はそれをキャッチすることを覚えておく必要があるため、2)const myvar = theFunctionmyvarはtry-catchから作成する必要があるため、書くことができないため、プロジェクトで例外を使用しましたしたがって、それはもはや一定ではありません。何らかの理由でC#が主流であるため、C#で使用しないという意味ではありませんが、とにかくそれらを削減しようとしています。
ハイエンジェル

2
@AndreasBonini彼らが設計/意図した事項は、多くの場合、コンパイラ/フレームワーク/ランタイム実装の決定が設計に沿って行われるためです。たとえば、例外のスローは、スタックトレースなどのコードのデバッグに役立つ情報を収集するため、単純なリターンよりもはるかに「高価」です。
binki18年

29

例外は、ContinuationsやGOTO。これらは、汎用の制御フロー構造です。

一部の言語では、これらは唯一の汎用制御フロー構造です。たとえば、JavaScriptにはContinuationsもありませんしGOTO、適切なTail Callもありません。そのため、JavaScriptで洗練された制御フローを実装する場合、例外を使用する必要があります。

Microsoft Voltaプロジェクトは、任意の.NETコードをJavaScriptにコンパイルするための(現在廃止されている)研究プロジェクトです。.NETには例外があり、そのセマンティクスはJavaScriptに正確にマッピングされませんが、さらに重要なことには、スレッドがあり、それらを何らかの方法でJavaScriptにマッピングする必要があります。Voltaは、JavaScript例外を使用してVolta Continuationsを実装し、Volta Continuationsに関してすべての.NET制御フロー構成を実装することでこれを実現しました。彼らは持っていたので、制御フローなどの例外を使用する他の制御フローを構築していないがあるほど強力。

ステートマシンについて言及しました。SMは適切なテールコールを使用して実装するのは簡単です。すべての状態はサブルーチンであり、すべての状態遷移はサブルーチン呼び出しです。SMはGOTO、CoroutinesまたはContinuationを使用して簡単に実装することもできます。ただし、Javaにはこれらの4つはありませんが、例外あります。したがって、これらを制御フローとして使用することは完全に受け入れられます。(まあ、実際には、正しい選択はおそらく適切な制御フロー構成を持つ言語を使用することですが、Javaで動けなくなることがあります。)


7
適切な末尾呼び出し再帰があれば、JavaScriptを使用してクライアント側のWeb開発を支配し、その後、野火のような究極の汎プラットフォームソリューションとしてサーバー側とモバイルに拡大することができたかもしれません。残念ながらそうではありませんでした。ファーストクラスの機能、クロージャー、イベント駆動型のパラダイムが不十分だ。本当に必要なのは本物でした。
エリックReppen

20
@ErikReppen:私はあなたが単なる皮肉だということを知っていますが。。。私は本当に私たちは「JavaScriptでクライアント側のWeb開発を支配している」という事実は、言語の機能とは何かを持っているとは思いません。それはその市場で独占権を持っているので、皮肉では取り除けない多くの問題を回避することができました。
-ruakh

1
テール再帰は素晴らしいボーナスになります(将来的には機能の機能は廃止されます)が、はい、機能関連の理由でVB、Flash、アプレットなどに対して勝ちました。複雑さを軽減せず、手軽に正規化した場合、ある時点で本当の競争があったでしょう。私は現在Node.jsを実行して、恐ろしいC#+ Javaスタック用の21個の構成ファイルの書き換えを処理していますが、それを行っているのは私だけではないことはわかっています。それが得意なことはとても得意です。
エリックReppen

1
多くの言語には、goto(Gotoは有害と見なされます!)、コルーチンまたは継続がなく、ifs、while、および関数呼び出し(またはgosubs!)を使用するだけで完全に有効なフロー制御を実装します。上記のすべてを表すために継続を使用できるように、実際にはこれらのほとんどを相互にエミュレートするために使用できます。(たとえば、コードの臭い方法であっても、ifを実行するのにwhileを使用できます)。したがって、高度なフロー制御を実行するために例外は必要ありません。
シェーン

2
ええ、そうなら、あなたは正しいかもしれません。しかし、そのCスタイル形式の「For」は不自然に記述されたwhileとして使用でき、whileをifと組み合わせて使用​​して、他のすべてのフロー制御フォームをエミュレートできる有限状態マシンを実装できます。繰り返しますが、コードの臭い方法ですが、ええ。そして再び、gotoは有害と見なされました。(また、例外的でない状況で例外を使用する場合も同様です)。gotoも例外も提供せず、正常に機能する、完全に有効で非常に強力な言語があることに留意してください。
シェーン14年

19

他の人が多数言及しているように(たとえば、このStack Overflowの質問で)、最小限の驚きの原則は、制御フローのみの目的で例外を過度に使用することを禁じます。一方、100%正しいルールはなく、例外が「ちょうどいいツール」である場合が常にあります- gotoちなみに、Javaのような形式でbreakcontinueJavaのような言語で出荷される多くの場合、頻繁にネストされたループから飛び出すのに最適な方法ですが、常に回避できるとは限りません。

次のブログ投稿では、非ローカルの かなり複雑であるが、かなり興味深いユースケースについて説明していますControlFlowException

jOOQ(Java用のSQL抽象化ライブラリ)(免責事項:ベンダーで働いています)の内部で、このような例外を使用して、「まれな」条件が満たされたときにSQLレンダリングプロセスを早期に中止する方法を説明します。

そのような条件の例は次のとおりです。

  • バインド値が多すぎます。一部のデータベースは、SQLステートメントで任意の数のバインド値をサポートしていません(SQLite:999、Ingres 10.1.0:1024、Sybase ASE 15.5:2000、SQL Server 2008:2100)。これらの場合、jOOQはSQLレンダリングフェーズを中止し、インラインバインド値を使用してSQLステートメントを再レンダリングします。例:

    // Pseudo-code attaching a "handler" that will
    // abort query rendering once the maximum number
    // of bind values was exceeded:
    context.attachBindValueCounter();
    String sql;
    try {
    
      // In most cases, this will succeed:
      sql = query.render();
    }
    catch (ReRenderWithInlinedVariables e) {
      sql = query.renderWithInlinedBindValues();
    }

    クエリASTからバインド値を明示的に抽出して毎回カウントする場合、この問題の影響を受けないクエリの99.9%で貴重なCPUサイクルを浪費します。

  • 一部のロジックは、「部分的に」のみ実行するAPIを介して間接的にのみ使用できます。UpdatableRecord.store()この方法は、生成INSERTまたはUPDATEに応じて文Recordの内部フラグを。「外側」からは、どのような種類のロジックが含まれているかstore()(たとえば、楽観的ロック、イベントリスナー処理など)がわからないため、複数のレコードをバッチステートメントに格納するときにそのロジックを繰り返したくはありません。store()実際に実行するのではなく、SQLステートメントのみを生成したい場合。例:

    // Pseudo-code attaching a "handler" that will
    // prevent query execution and throw exceptions
    // instead:
    context.attachQueryCollector();
    
    // Collect the SQL for every store operation
    for (int i = 0; i < records.length; i++) {
      try {
        records[i].store();
      }
    
      // The attached handler will result in this
      // exception being thrown rather than actually
      // storing records to the database
      catch (QueryCollectorException e) {
    
        // The exception is thrown after the rendered
        // SQL statement is available
        queries.add(e.query());                
      }
    }

    store()オプションでSQLを実行しないようにカスタマイズできる「再利用可能な」APIにロジックを外部化していた場合、保守が難しく、再利用が困難なAPIの作成を検討していました。

結論

本質的に、これらの非ローカルgotosの使用法は、メイソン・ホイーラーが彼の答えで言ったことに沿っています。

「この時点で適切に対処できない状況に遭遇したばかりです。それを処理するのに十分なコンテキストがないためです。 」

両方の使用法はControlFlowExceptions、他の方法に比べて実装がかなり簡単で、関連する内部からリファクタリングすることなく幅広いロジックを再利用できます。

しかし、これが将来のメンテナーにとって少し驚きであるという感覚は残っています。コードはかなり繊細で、この場合は正しい選択でしたが、通常のブランチスルーの使用を避けやすいローカル制御フローには例外を使用しないことを常に望んでいif - elseます。


11

制御フローに例外を使用することは一般にアンチパターンと見なされますが、例外もあります(しゃれはありません)。

例外は例外的な条件のためのものであると千回言われています。切断されたデータベース接続例外的な状態です。数字のみを許可する必要がある入力フィールドに文字を入力するユーザーはそうではありません

null許可されていない場合など、不正な引数で関数が呼び出される原因となるソフトウェアのバグ、例外的な状態です。

例外ではないものに例外を使用することにより、解決しようとしている問題に対して不適切な抽象化を使用しています。

ただし、パフォーマンスが低下する可能性もあります。一部の言語には、多少効率的な例外処理の実装があります。そのため、選択した言語に効率的な例外処理がない場合、パフォーマンスが非常に高くなります*。

ただし、Rubyなどの他の言語には、制御フロー用の例外のような構文があります。例外的な状況はraise/ rescue演算子によって処理されます。ただし、例外のような制御フロー構造にはthrow/ catchを使用できます**。

したがって、例外は一般的に制御フローは使用されませんが、選択する言語には他のイディオムがある場合があります。

*例外のパフォーマンスを犠牲にして使用する例:私はかつて、パフォーマンスの低いASP.NET Webフォームアプリケーションを最適化するように設定されていました。大きなテーブルのレンダリングがint.Parse()約を呼び出していたことが判明しました。平均的なページに1000個の空の文字列があり、約 処理されている1000の例外。コードを置き換えることで、int.TryParse()1秒短縮されました!単一ページのリクエストごとに!

**これは、両方のように、他の言語からRubyのに来て、プログラマのための非常に混乱することができますthrowし、catch他の多くの言語での例外に関連付けられたキーワードです。


1
+1「例外的でないものに例外を使用することにより、解決しようとしている問題に対して不適切な抽象化を使用している」
ジョルジオ

8

例外を使用せずにエラー状態を処理することは完全に可能です。一部の言語、特にCは例外さえも持たず、人々はそれを使って非常に複雑なアプリケーションを作成することができます。例外が有用な理由は、同じコード内で2つの本質的に独立した制御フローを簡潔に指定できることです。1つはエラーが発生し、もう1つは発生しません。それらがなければ、次のような場所全体にコードができてしまいます。

status = getValue(&inout);
if (status < 0)
{
    logError("message");
    return status;
}

doSomething(*inout);

または、エラーステータスとして1つの値を含むタプルを返すなど、言語で同等です。多くの場合、「高価な」例外処理がどの程度かを指摘する人は、if上記のような追加のステートメントをすべて無視してください。例外を使用します。

このパターンは、エラーやその他の「例外条件」を処理するときに最も頻繁に発生しますが、他の状況でこのような定型コードが表示されるようになった場合、例外を使用するのは非常に適切です。状況と実装に応じて、状態マシンで例外が有効に使用されていることがわかります。2つの直交制御フローがあります。1つは状態を変更し、もう1つは状態内で発生するイベント用です。

ただし、そのような状況はまれであり、ルールに例外(しゃれを意図したもの)を作成する場合は、他のソリューションに対する優位性を示す準備ができていなければなりません。このような正当化のない逸脱は、当然アンチパターンと呼ばれます。


2
if文が「例外的な」状況でのみ失敗する限り、最新のCPUの分岐予測ロジックはコストを無視できます。これはマクロを実際に助けることができる場所の1つです。ただし、注意を払い、マクロで多くのことをしようとしない限りです。
ジェームズ

5

Pythonでは、ジェネレーターと反復の終了に例外が使用されます。Pythonには非常に効率的なtry / exceptブロックがありますが、実際に例外を発生させるにはオーバーヘッドがあります。

Pythonにはマルチレベルブレークやgotoステートメントがないため、時々例外を使用しました。

class GOTO(Exception):
  pass

try:
  # Do lots of stuff
  # in here with multiple exit points
  # each exit point does a "raise GOTO()"
except GOTO:
  pass
except Exception as e:
  #display error

6
深くネストされたステートメントのどこかで計算を終了し、いくつかの一般的な継続コードに移動する必要がある場合、この特定の実行パスはreturn、の代わりに関数として除外することができgotoます。
9000

3
@ 9000まったく同じことをコメントしようとしていました。try:ブロックを1行または2行にしてくださいtry: # Do lots of stuff
WIM

1
@ 9000、場合によっては、確かに。しかし、プロセスが一貫した線形プロセスである場合、ローカル変数にアクセスできなくなり、コードを別の場所に移動します。
ガフーア

4
@gahooa:私はかつてあなたのように考えていました。これは、私のコードの構造が悪いことの兆候でした。さらに考えてみると、ローカルコンテキストを解き、パラメーター全体、コード行数、および非常に正確な意味を持たない短い関数にすることができます。振り返ることはありません。
9000

4

そのような例外の使用法をスケッチしましょう:

アルゴリズムは、何かが見つかるまで再帰的に検索します。そのため、再帰から戻ってくるには、結果が見つかったかどうかを確認し、それから戻るか、そうでなければ続行する必要があります。そして、それは再帰の深さから繰り返し戻ってきます。

追加のブール値found(クラスにパックされる場合、そうでなければintのみが返される可能性がある)が必要であることに加えて、再帰の深さについても同じポストリュードが発生します。

このような呼び出しスタックの巻き戻しは、まさに例外の目的です。だから、私には、後藤のような、より直接的で適切なコーディングの手段のように思えます。不要、まれな使用法、スタイルの悪さかもしれませんが、要点です。Prologカット操作と同等。


うーん、本当に反対の立場、または不十分または短い議論に対するダウン投票ですか?私は本当に興味があります。
ジョープエッゲン14年

私はまさにこの苦境に直面している、私はあなたが私の次の質問についてどう思うか見ることができることを望んでいた?再帰的に例外を使用して、最上位の例外codereview.stackexchange.com/questions/107862/…
CL22

@Jodesおもしろいテクニックを読んだばかりですが、今はかなり忙しいです。他の誰かが問題に彼女/彼の光を当てることを願っています。
ジョープ

4

プログラミングは仕事に関するものです

これに答える最も簡単な方法は、OOPが長年にわたって行ってきた進歩を理解することだと思います。OOPで行われたすべて(および、ほとんどのプログラミングパラダイム)は、必要な作業を中心にモデル化さます。

メソッドが呼び出されるたびに、呼び出し元は「この作業を行う方法がわかりませんが、方法は知っているので、私のためにそれを行います」と言っています。

これは難しさを示しました:呼び出されたメソッドが作業を行う方法を一般的に知っているとどうなりますか?「あなたを助けたいと思っていましたが、本当にやりましたが、それはできません」と伝える方法が必要でした

これを伝える初期の方法論は、単に「ゴミ」値を返すことでした。たぶん、あなたは正の整数を期待しているので、呼び出されたメソッドは負の数を返します。これを実現する別の方法は、エラー値をどこかに設定することでした。残念ながら、どちらの方法でも、定型文のlet-me-check-over-here-to-make-sure-sure-everything's-kosherコードになりました。事態が複雑になるにつれて、このシステムは崩壊します。

例外的なアナロジー

大工、配管工、電気技師がいるとしましょう。あなたはあなたの流しを修理するために配管工したいので、彼はそれを見てみましょう。「申し訳ありませんが、修正できません。壊れています。」と彼が言っただけではあまり役に立ちません。地獄、彼がそれを修正することができなかったと言って手紙を見て、去って、そしてあなたに送るならば、それはさらに悪いです。今、あなたは彼があなたの望んだことをしなかったことを知る前にあなたのメールをチェックしなければなりません。

あなたが好むのは、「あなたのポンプが機能していないように見えるので、私はそれを修正できませんでした」と彼に言わせることです。

この情報を使用して、電気技師に問題を見てもらいたいと結論付けることができます。おそらく、電気技師は大工に関連する何かを見つけるでしょう、そしてあなたは大工にそれを修理してもらう必要があります。

ヘック、あなたも、あなたは知らないかもしれない、あなたは電気技師を必要と知っていない可能性があります誰があなたが必要です。あなたは家の修理ビジネスのちょうど中間管理者であり、あなたの焦点は配管工事です。あなたはあなたが問題について上司だと言い、それからは電気技師にそれを修正するように言います。

これがモデリングの例外です:切り離された方法での複雑な故障モード。配管工は電気技師について知る必要はありません。チェーンの上の誰かが問題を解決できることを知る必要さえありません。彼は、発生した問題について報告するだけです。

だから...アンチパターン?

それでは、例外のポイントを理解することが最初のステップです。次に、アンチパターンとは何かを理解することです。

アンチパターンとしての資格を得るには、次のことが必要です。

  • 問題を解く
  • 決定的に否定的な結果をもたらす

最初のポイントは簡単に満たされます-システムは動作しましたよね?

2番目のポイントはより粘着性です。通常の制御フローとして例外を使用する主な理由は、それが目的ではないためです。プログラム内の特定の機能には比較的明確な目的が必要であり、その目的を採用すると不必要な混乱が生じます。

しかし、それは決定的な害ではありません。それは物事を行うには貧弱な方法であり、奇妙ですが、アンチパターンですか?いいえ。ただ...奇妙です。


少なくとも、完全なスタックトレースを提供する言語では、1つの悪い結果があります。コードは多くのインライン化で大幅に最適化されているため、実際のスタックトレースと開発者が見たいスタックトレースは大きく異なるため、スタックトレースの生成にはコストがかかります。このような言語(Java、C#)のパフォーマンスにとって、例外を使いすぎると非常に悪い結果になります。
-maaartinus

「それは物事を行うには貧弱な方法です」-それはアンチパターンとして分類するのに十分ではないでしょうか?
Maybe_Factor

@Maybe_Factor ant-patternの定義により、いいえ。
MirroredFate
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.