6

エラーのコンテキストで、Throwable/Exception を常に見てきました。Throwableしかし、再帰的なメソッド呼び出しのスタックから抜け出すためだけにa を拡張すると本当に便利な状況を思いつくことができます。たとえば、再帰的検索によってツリー内のオブジェクトを見つけて返そうとしたとします。見つけたら、いくつかに突き刺してCarrier extends Throwable投げ、再帰メソッドを呼び出すメソッドでキャッチします。

肯定的: 再帰呼び出しのリターン ロジックについて心配する必要はありません。必要なものが見つかったので、その参照をメソッド スタックに戻す方法を心配する必要はありません。

否定的: 必要のないスタック トレースがあります。また、try/catchブロックは直感に反します。

以下はばかばかしいほど単純な使用法です。

public class ThrowableDriver {
    public static void main(String[] args) {
        ThrowableTester tt = new ThrowableTester();
        try {
            tt.rec();
        } catch (TestThrowable e) {
            System.out.print("All good\n");
        }
    }
}

public class TestThrowable extends Throwable {

}

public class ThrowableTester {
    int i=0;

    void rec() throws TestThrowable {
        if(i == 10) throw new TestThrowable();
        i++;
        rec();
    }
}

問題は、同じことを達成するためのより良い方法があるかということです。また、このように物事を行うことについて本質的に悪いことはありますか?

4

6 に答える 6

8

実際、「通常の」プログラマーが例外を使用することを考えない場合には、例外を使用することをお勧めします。たとえば、「ルール」を開始し、それが機能しないことを検出するパーサーでは、例外は正しいリカバリポイントにブローバックするための非常に良い方法です。(これは、再帰から抜け出すというあなたの提案とある程度似ています。)

「例外は後藤に勝るものはない」という古典的な反対意見がありますが、これは明らかに誤りです。Javaやその他のほとんどの現代言語では、ネストされた例外ハンドラーとfinallyハンドラーを使用できるため、例外を介して制御が転送されると、適切に設計されたプログラムでクリーンアップなどを実行できます。実際、このように、例外はいくつかの点でリターンコードでは、すべてのリターンポイントにロジックを追加してリターンコードをテストしfinally、ルーチンを終了する前に正しいロジック(おそらくいくつかのネストされた部分)を見つけて実行する必要があるためです。例外ハンドラーを使用すると、ネストされた例外ハンドラーを介して、これは合理的に自動化されます。

例外には、いくつかの「手荷物」が含まれます。たとえば、Javaのスタックトレースです。ただし、Javaの例外は実際には非常に効率的であるため(少なくとも他のいくつかの言語の実装と比較して)、例外をあまり頻繁に使用しない場合、パフォーマンスは大きな問題にはなりません。

[私は40年のプログラミング経験があり、70年代後半から例外を使用していることを付け加えておきます。1980年頃に独立して「発明された」try/catch / finally(BEGIN / ABEXIT / EXITと呼ばれる)。]

「違法な」余談:

これらの議論でしばしば見落とされるのは、コンピューティングにおける最大の問題は、コストや複雑さ、標準やパフォーマンスではなく、制御であるということだと思います。

「制御」とは、「制御フロー」、「制御言語」、「オペレーター制御」、または「制御」という用語が頻繁に使用されるその他のコンテキストを意味するものではありません。私は一種の「複雑さの制御」を意味しますが、それはそれだけではありません。それは「概念的な制御」です。

私たちは皆それをやり遂げました(少なくとも約6週間以上プログラミングをしている私たちの場合)-実際の構造や標準(私たちが習慣的に使用するものを除く)のない「単純な小さなプログラム」を書き始めました。 「シンプル」で「使い捨て」なので、複雑さを気にする必要はありません。しかし、その後、コンテキストに応じて、おそらく10分の1または100分の1のケースで、「単純な小さなプログラム」は怪物に成長します。

私たちはそれに対する「概念的制御」を失います。1つのバグを修正すると、さらに2つのバグが発生します。プログラムの制御とデータフローは不透明になります。それは私たちが完全に理解できない方法で動作します。

それでも、ほとんどの標準では、この「単純な小さなプログラム」はそれほど複雑ではありません。それほど多くのコード行はありません。(私たちは熟練したプログラマーなので)「適切な」数のサブルーチンに分割されている可能性が非常に高いです。複雑さの測定アルゴリズムを実行すると(まだ比較的小さく、「サブルーチン化」されているため)、特に複雑ではないとスコアリングされます。

最終的に、概念的な制御を維持することが、事実上すべてのソフトウェアツールと言語の背後にある原動力です。はい、アセンブラーやコンパイラーのようなものは私たちをより生産的にし、生産性は主張された原動力ですが、その生産性の向上の多くは、「無関係な」詳細で忙しくする必要がなく、代わりに私たちが望む概念に集中できるためです実装する。

概念制御の大きな進歩は、「外部サブルーチン」が存在し、環境からますます独立するようになるにつれて、コンピューティングの歴史の初期に発生し、サブルーチン開発者がサブルーチンの環境について多くを知る必要がない「関心の分離」を可能にしました。サブルーチンのユーザーは、サブルーチンの内部について多くを知る必要はありませんでした。

BEGIN / ENDと「{...}」の単純な開発は、「インライン」コードでさえ「そこに」と「ここに」の間の分離から利益を得ることができるので、同様の進歩を生み出しました。

私たちが当たり前と思っているツールや言語機能の多くは存在し、これまで以上に複雑なソフトウェア構造に対する知的制御を維持するのに役立つため、便利です。そして、新しいツールや機能の有用性は、それがこの知的制御にどのように役立つかによって、かなり正確に測定できます。

残りの最大の困難な領域がリソース管理である場合の1つ。ここでの「リソース」とは、プログラムの実行中に「作成」または「割り当て」られ、その後何らかの形式の割り当て解除が必要になる可能性のあるエンティティ(オブジェクト、開いているファイル、割り当てられたヒープなど)を意味します。「自動スタック」の発明は、ここでの最初のステップでした。変数は「スタック上」に割り当てられ、「割り当てられた」サブルーチンが終了すると自動的に削除されます。(これはかつては非常に物議を醸した概念であり、多くの「当局」は、パフォーマンスに影響を与えるため、この機能の使用を推奨していませんでした。)

しかし、ほとんどの(すべての?)言語では、この問題は依然として何らかの形で存在します。明示的なヒープを使用する言語では、「新しい」ものは何でも「削除」する必要があります。開いたファイルはなんらかの方法で閉じる必要があります。ロックを解除する必要があります。これらの問題のいくつかは、(たとえば、GCヒープを使用して)細かく調整したり、ペーパーオーバー(参照カウントまたは「子育て」)したりできますが、すべてを排除または非表示にする方法はありません。そして、単純なケースでこの問題を管理するのはかなり簡単です(たとえば、newオブジェクト、それを使用するサブルーチンを呼び出してから、deleteそれ)、実際の生活はめったにそれほど単純ではありません。十数かそこらの異なる呼び出しを行い、呼び出し間でリソースをいくらかランダムに割り当て、それらのリソースの「ライフタイム」を変えるメソッドがあることは珍しいことではありません。また、呼び出しの中には、制御フローを変更する結果を返す場合があり、場合によってはサブルーチンを終了させたり、サブルーチン本体のサブセットの周りにループを引き起こしたりすることがあります。このようなシナリオでリソースを解放する方法(正しいものをすべて解放し、間違ったものを解放しない)を知ることは困難であり、サブルーチンが時間の経過とともに変更されると(複雑なすべてのコードがそうであるように)、さらに複雑になります。

メカニズムの基本的な概念try/finally(アスペクトを少し無視するcatch)は、上記の問題にかなりうまく対処します(完全にはほど遠いですが、認めます)。管理する必要のある新しいリソースまたはリソースのグループごとに、プログラマーはtry/finallyブロックを導入し、割り当て解除ロジックをfinally節に配置します。リソースが確実に解放されるという実際的な側面に加えて、このアプローチには、関連するリソースの「範囲」を明確に示し、「強制的に維持される」一種のドキュメントを提供するという利点があります。

このメカニズムがメカニズムと結合されているという事実はcatch、通常の場合にリソースを管理するために使用されるのと同じメカニズムが「例外」の場合にそれらを管理するために使用されるため、少し偶然です。「例外」は(表面上は)まれであるため、メインラインほど十分にテストされることはなく、エラーケースの「概念化」は平均的には特に難しいため、そのまれなパスのロジックの量を最小限に抑えることが常に賢明です。プログラマー。

確かに、try/finallyいくつかの問題があります。それらの最初の1つは、ブロックが非常に深くネストされる可能性があるため、プログラム構造が明確になるのではなく不明瞭になる可能性があることです。しかし、これはdoループやifステートメントに共通する問題であり、言語設計者からのインスピレーションを得た洞察を待っています。より大きな問題はtry/finallycatch(さらに悪いことに、例外的な)手荷物があることです。これは、必然的に二級市民に追いやられることを意味します。(たとえば、finally現在非推奨のJSB / RETメカニズムを超えて、Javaバイトコードの概念としてさえ存在しません。)

他のアプローチがあります。IBM iSeries(または「Systemi」または「IBMi」またはそれらが現在呼んでいるもの)には、関連するプログラムが戻った(または異常終了した)ときに実行される、呼び出しスタック内の特定の呼び出しレベルにクリーンアップ・ハンドラーを付加するという概念があります。 )。これは、現在の形式では不器用であり、Javaプログラムで必要とされる細かいレベルの制御にはあまり適していません。たとえば、潜在的な方向を示しています。

そしてもちろん、C ++言語ファミリ(Javaではない)には、リソースを表すクラスを自動変数としてインスタンス化し、オブジェクトデストラクタに変数のスコープからの終了時に「クリーンアップ」を提供させる機能があります。(このスキームは、基本的にtry / finalを使用していることに注意してください。)これは多くの点で優れたアプローチですが、一連の一般的な「クリーンアップ」クラスまたは異なるタイプごとの新しいクラスの定義が必要です。リソースの、テキスト的にかさばるが比較的無意味なクラス定義の潜在的な「クラウド」を作成します。(そして、私が言ったように、それは現在の形のJavaのオプションではありません。)

しかし、私は逸脱します。

于 2011-08-01T03:51:05.807 に答える
3

プログラム制御フローに例外を使用することはお勧めできません。

通常の運用基準外の状況については、まさにそのための例外を予約してください。

SOにはかなりの数の関連する質問があります:

于 2011-08-01T03:26:07.717 に答える
2

一般的な制御フロー用に設計されていないため、構文が不安定になります。再帰関数の設計における標準的な方法は、番兵の値または見つかった値(または、この例では機能するものは何もない)のいずれかを最後まで返すことです。

従来の知識:「例外は例外的な状況のためのものです。」お気づきのThrowableように、理論的にはより一般化された音ですが、例外とエラーを除いて、広く使用するように設計されているようには見えません。ドキュメントから:

Throwableクラスは、Java言語のすべてのエラーと例外のスーパークラスです。

多くのランタイム(VM)は、例外のスローを最適化しないように設計されています。つまり、「高価」になる可能性があります。もちろん、これができなかったわけではなく、「高価」は主観的ですが、一般的にはこれは行われず、他の人はあなたのコードでそれを見つけて驚くでしょう。

于 2011-08-01T03:27:14.670 に答える
1

問題は、同じことを達成するためのより良い方法があるかということです。また、このように物事を行うことについて本質的に悪いことはありますか?

2 番目の質問に関しては、コンパイラがどれほど効率的であるかに関係なく、例外は実行時に大きな負担を伴います。それだけで、一般的なケースでそれらを制御構造として使用することに反対するはずです。

さらに、例外は制御された goto に相当し、ロング ジャンプにほぼ相当します。はい、はい、ネストできます。Java のような言語では、素敵な 'finally' ブロックなどすべてを使用できます。それでも、それがすべてであり、そのため、典型的な制御構造を一般的に置き換えることを意図したものではありません。40年以上にわたる業界の集合的知識は、非常に正当な理由がない限り、一般的にそのようなことを避けるべきであることを教えてくれます.

そして、それが最初の質問の核心になります。はい、もっと良い方法があります (コードを例にとると)... 典型的な制御構造を使用するだけです:

// class and method names remain the same, though using 
// your typical logical control structures

public class ThrowableDriver {
    public static void main(String[] args) {
        ThrowableTester tt = new ThrowableTester();
        tt.rec();
        System.out.print("All good\n");
        }
    }
}

public class ThrowableTester {
    int i=0;

    void rec() {
        if(i == 10) return;
        i++;
        rec();
    }
}

見る?よりシンプル。コード行が少なくなります。冗長な try/catch や不要な例外スローはありません。あなたは同じことを達成します。

結局のところ、私たちの仕事は言語構造をいじることではなく、分別があり、保守性の観点から十分に単純で、仕事を完了するのに十分なステートメントだけで、他に何もないプログラムを作成することです。

したがって、あなたが提供したサンプル コードに関しては、自問する必要があります。典型的な制御構造を使用した場合には得られない、そのアプローチでは何が得られたのでしょうか?

再帰呼び出しのリターン ロジックについて心配する必要はありません。

戻りロジックを気にしない場合は、単純に戻りを無視するか、メソッドを void 型に定義してください。それを try/catch でラップすると、コードが必要以上に複雑になります。返品を気にしないなら、完成させる方法を気にするのは確かです。したがって、必要なのは単純に呼び出すことだけです (この投稿で提供したコード サンプルのように)。

必要なものが見つかったので、その参照をメソッド スタックに戻す方法を心配する必要はありません。

例外のスローに関連するすべての帳簿を保持する (エピローグを実行し、潜在的に大きなスタック トレースを埋める) よりも、メソッドの完了前にリターン (JVM のほとんどのオブジェクト参照) をスタックにプッシュする方が安価です。 JVMであろうとなかろうと、これは基本的なCS 101のものです。

そのため、コストが高くなるだけでなく、同じことをコーディングするためにさらに多くの文字を入力する必要があります。

通常の制御構造を使用して書き換えることができない、Throwable を介して終了できる再帰的なメソッドは事実上ありません。制御構造の代わりに例外を使用するには、非常に、非常に、しかし非常に正当な理由が必要です。

于 2011-08-01T16:57:38.583 に答える
0

ただ。しないでください。
参照: Joshua Bloch 著「Effective Java」、p. 243

于 2011-08-01T17:15:56.553 に答える