web-dev-qa-db-ja.com

なぜ例外処理メカニズムなしで現代の言語を設計するのですか?

多くの最新言語 豊富な例外処理を提供 機能ですが、AppleのSwiftプログラミング言語 例外処理メカニズムを提供していません

私は例外的に染み込んでいますが、これが何を意味するのか気になりません。 Swiftにはアサーションがあり、もちろん戻り値がありますが、例外ベースの考え方が例外のない世界にどのようにマップされるか(そして、その点については なぜそのような世界が望ましいのですか )。Swift例外で実行できること)のような言語で私ができないことはありますか?例外を失うことで何かを得ますか?

たとえば、どのように表現すればよいですか 次のようなもの

try:
    operation_that_can_throw_ioerror()
except IOError:
    handle_the_exception_somehow()
else:
     # we don't want to catch the IOError if it's raised
    another_operation_that_can_throw_ioerror()
finally:
    something_we_always_need_to_do()

例外処理が不足している言語(Swiftなど)で?

48
orome

組み込みプログラミングでは、リアルタイムのパフォーマンスを維持しようとするときに、実行する必要のあるスタックの巻き戻しのオーバーヘッドが許容できない変動と見なされたため、従来は例外が許可されていませんでした。スマートフォンは技術的にはリアルタイムプラットフォームと見なすことができますが、組み込みシステムの古い制限が実際には適用されなくなった現在、十分に強力です。念のために育ててみました。

例外は関数型プログラミング言語でサポートされていることがよくありますが、あまり使用されないため、サポートされない場合があります。 1つの理由は遅延評価です。これは、デフォルトで遅延ではない言語でも時々行われます。実行するためにキューに入れられた場所とは異なるスタックで実行される関数があると、例外ハンドラーを配置する場所を決定することが困難になります。

もう1つの理由は、ファーストクラスの関数がオプションやフューチャーなどの構造を可能にし、より柔軟に例外の構文上の利点を提供することです。言い換えれば、言語の残りの部分は十分に表現力豊かであり、例外があなたに何も買わせないのです。

私はSwiftに精通していませんが、エラー処理について少し読んだだけでは、エラー処理がより機能的なスタイルのパターンに従うように意図されていることを示唆しています。 successブロックとfailureブロックを使用したコード例を見てきたが、これはフューチャーに非常によく似ている。

this Scala tutorialFutureを使用した例を次に示します。

val f: Future[List[String]] = future {
  session.getRecentPosts
}
f onFailure {
  case t => println("An error has occured: " + t.getMessage)
}
f onSuccess {
  case posts => for (post <- posts) println(post)
}

例外を使用した例とほぼ同じ構造であることがわかります。 futureブロックはtryに似ています。 onFailureブロックは例外ハンドラのようなものです。 Scalaでは、ほとんどの関数型言語と同様に、Futureは言語自体を使用して完全に実装されています。例外のように特別な構文は必要ありません。つまり、独自の同様の構成を定義できます。たとえば、timeoutブロックを追加したり、ログ機能を追加したりできます。

さらに、futureを渡したり、関数から返したり、データ構造に保存したりすることができます。ファーストクラスの価値です。スタックに直接伝播する必要がある例外のように制限されていません。

オプションは、少し異なる方法でエラー処理の問題を解決します。これは、一部のユースケースでより効果的に機能します。 1つの方法だけで行き詰まっているわけではありません。

これらは、「例外を失うことで得られる」種類のことです。

34
Karl Bielefeldt

例外はコードを推論することをより困難にする可能性があります。それらはgotoほど強力ではありませんが、非ローカルな性質のために同じ問題の多くを引き起こす可能性があります。たとえば、次のような命令コードがあるとします。

cleanMug();
brewCoffee();
pourCoffee();
drinkCoffee();

これらのプロシージャのいずれかが例外をスローできるかどうかは一目でわかりません。それを理解するには、これらの各手順のドキュメントを読む必要があります。 (一部の言語では、この情報を型シグネチャに追加することで、これが少し簡単になります。)上記のコードは、プロシージャがスローするかどうかに関係なく正常にコンパイルされ、例外の処理を忘れることが非常に簡単になります。

さらに、意図が例外を呼び出し元に伝播することである場合でも、一貫性のない状態のままにされないようにコードを追加する必要があることがよくあります(たとえば、コーヒーメーカーが壊れた場合でも、混乱をクリーンアップして返す必要があります)マグカップ!)したがって、多くの場合、例外を使用するコードは、余分なクリーンアップが必要なために、そうでないコードと同じくらい複雑に見えます。

例外は十分に強力な型システムでエミュレートできます。例外を回避する言語の多くは、戻り値を使用して同じ動作を実現します。これはCで行われる方法と似ていますが、最新の型システムでは通常、よりエレガントになり、エラー条件の処理を忘れることも難しくなります。それらは、物事をより扱いにくくするための構文糖を提供することもあり、例外の場合と同じくらいクリーンな場合もあります。

特に、個別の機能として実装するのではなく、エラー処理を型システムに埋め込むことにより、「例外」を、エラーにさえ関連していない他のものに使用することができます。 (例外処理が実際にはモナドのプロパティであることはよく知られています。)

21
Rufflewind

ここにいくつかの素晴らしい答えがありますが、1つの重要な理由が十分に強調されていないと思います:例外が発生すると、オブジェクトが無効な状態のままになる可能性があります。例外を "キャッチ"できる場合、例外ハンドラコードはこれらの無効なオブジェクトにアクセスして操作できます。これらのオブジェクトのコードが完全に記述されていない限り、それはひどく間違って行われます。これは非常に非常に困難です。

たとえば、Vectorの実装を想像してみてください。誰かが一連のオブジェクトを使用してベクターをインスタンス化したが、初期化中に例外が発生した場合(おそらく、オブジェクトを新しく割り当てられたメモリにコピーしているときなど)、ベクターの実装を正しく実装しないようにして、メモリがリークしています。 Stroustroupによるこの短い論文はベクターの例をカバーしています

そして、それは氷山の一角にすぎません。たとえば、すべての要素ではなく一部の要素をコピーした場合はどうなりますか? Vectorのようなコンテナーを正しく実装するには、取るすべてのアクションをリバーシブルにする必要があるため、操作全体が(データベーストランザクションのように)アトミックです。これは複雑であり、ほとんどのアプリケーションはそれを誤解しています。そして、それが正しく行われたとしても、コンテナを実装するプロセスは非常に複雑になります。

だからいくつかの現代の言語はそれは価値がないと決定しました。たとえば、Rustには例外がありますが、それらを「キャッチ」することはできないため、コードが無効な状態のオブジェクトと対話する方法はありません。

14
Charlie Flowers

私の意見では、例外は実行時にコードエラーを検出するための不可欠なツールです。テストと本番の両方で。メッセージを十分に詳細にして、スタックトレースと組み合わせて、ログから何が起こったかを理解できるようにします。

例外は主に開発ツールであり、予期しない状況で本番環境から妥当なエラーレポートを取得する方法です。

懸念の分離(予期されるエラーのみのハッピーパスと、予期しないエラーの汎用ハンドラーに到達するまでの通過)は別として、コードをより読みやすく保守しやすくすることは別として、可能な限りすべてのコードを準備することは実際には不可能です予期せぬケース、エラー処理コードでそれを肥大化して読みにくくすることでさえ。

それが「意外」の意味です。

ところで、期待されることとそうでないことは、呼び出しサイトでのみ行うことができる決定です。 Javaでチェックされた例外がうまくいかなかったのはそのためです-何が期待されているか予期されていないかがまったく明確でない場合、決定はAPIの開発時に行われます。

簡単な例:ハッシュマップのAPIは2つのメソッドを持つことができます:

Value get(Key)

そして

Option<Value> getOption(key)

見つからない場合は最初に例外をスローし、後者はオプションの値を提供します。場合によっては後者の方が理にかなっていますが、コードによっては、特定のキーに値が存在することを期待する必要があるため、値がない場合は、このコードで修正できないエラーです。仮定は失敗しました。この場合、コードパスから外れ、呼び出しが失敗した場合にいくつかの汎用ハンドラーに落ちるのが実際に望ましい動作です。

コードは、失敗した基本的な仮定に対処しようとするべきではありません。

もちろん、それらをチェックして、読みやすい例外をスローする場合を除きます。

例外を投げることは悪いことではありませんが、例外をキャッチすることは悪いことではありません。予期しないエラーを修正しようとしないでください。いくつかのループまたは操作を続行したいいくつかの場所で例外をキャッチし、それらをログに記録し、おそらく不明なエラーを報告します。それだけです。

あらゆる場所でブロックをキャッチすることは非常に悪い考えです。

意図を簡単に表現できるようにAPIを設計します。つまり、キーが見つからないなど、特定のケースが予想されるかどうかを宣言します。 APIのユーザーは、本当に予期しない場合にのみ、スローする呼び出しを選択できます。

エラー処理を自動化するためのこの重要なツールを省略し、新しい言語から懸念をより適切に分離することで、人々が例外に憤慨し、行き過ぎた理由は、悪い経験だと思います。

それと、彼らが実際に何が良いのかについてのいくつかの誤解。

モナディックバインディングですべてを実行してそれらをシミュレートすると、コードの可読性と保守性が低下し、スタックトレースがなくなるため、このアプローチはさらに悪化します。

関数型のエラー処理は、予想されるエラーの場合に最適です。

例外処理が残りのすべてを自動的に処理するようにします。それが目的です:)

6
yeoman

Rust言語で最初に驚いたのは、catch例外をサポートしていないことです。throw例外ですが、タスク(スレッドとは限りませんが、常に別のOSスレッドではない)が終了したときに、ランタイムのみが例外をキャッチできます。タスクを自分で開始する場合、タスクが正常に終了したかどうか、またはfail!()かどうかを確認できますed。

そのため、failを頻繁に使用することは慣用的ではありません。これが発生するいくつかのケースは、たとえば、テストハーネス(ユーザーコードがどのようなものかを知らない)、コンパイラーの最上位(ほとんどのコンパイラーはforkである)、またはコールバックを呼び出すときなどです。ユーザー入力時。

代わりに、一般的なイディオムは Resultテンプレート を使用して、処理する必要のあるエラーを明示的に渡すことです。これは the try!マクロ 。これは、Resultを生成し、存在する場合は成功したアームを生成する式、または関数から早期に戻る式を囲むことができます。

use std::io::IoResult;
use std::io::File;

fn load_file(name: &Path) -> IoResult<String>
{
    let mut file = try!(File::open(name));
    let s = try!(file.read_to_string());
    return Ok(s);
}

fn main()
{
    print!("{}", load_file(&Path::new("/tmp/hello")).unwrap());
}
6
o11c

Swiftは、ここではObjective-Cと同じ原則を使用しています。 Objective-Cでは、例外はプログラミングエラーを示します。クラッシュレポートツール以外では処理されません。 「例外処理」は、コードを修正することによって行われます。 (いくつかのahem例外があります。たとえば、プロセス間通信などです。しかし、それは非常にまれであり、多くの人がそれに遭遇することはありません。そして、Objective-Cは実際に試行/キャッチ/最後に/スローしますが、ほとんど使用しません)。 Swift例外をキャッチする可能性を削除しました。

Swiftにはのように見える例外処理という機能がありますが、強制的にエラー処理が行われます。歴史的に、Objective-Cには非常に広範なエラー処理パターンがありました。メソッドはBOOL(成功の場合はYES)またはオブジェクト参照(失敗の場合はnil、成功の場合はnil)を返し、パラメーター「NSError *へのポインター」を持っています。 NSError参照を格納するために使用されます。 Swiftは、このようなメソッドへの呼び出しを自動的に例外処理のようなものに変換します。

一般に、Swift関数は、関数が正常に機能した場合の結果や失敗した場合のエラーなど、代替を簡単に返すことができます。これにより、エラー処理がはるかに容易になります。しかし、元の質問に対する答えは次のとおりです。 Swift設計者は、安全な言語を作成し、そのような言語で成功するコードを作成する方が、言語に例外がなければより簡単であると感じました。

3
gnasher729

チャーリーの答えに加えて:

多くのマニュアルや本で見られる宣言された例外処理のこれらの例は、非常に小さな例でのみ非常にスマートに見えます。

無効なオブジェクトの状態についての議論を別にしても、大きなアプリを扱う場合は常に非常に大きな苦痛をもたらします。

たとえば、いくつかの暗号化を使用してIOを処理する必要がある場合、50のメソッドからスローされる20種類の例外がある場合があります。必要な例外処理コードの量を想像してみてください。例外処理には、コード自体の数倍のコードが必要です。

実際には、例外が表示されない場合があり、それほど多くの例外処理を記述する必要がないため、いくつかの回避策を使用して宣言された例外を無視します。私の実践では、宣言された例外の約5%だけが、信頼できるアプリを作成するためにコードで処理する必要があります。

2
konmik
 int result;
 if((result = operation_that_can_throw_ioerror()) == IOError)
 {
  handle_the_exception_somehow();
 }
 else
 {
   # we don't want to catch the IOError if it's raised
   result = another_operation_that_can_throw_ioerror();
 }
 result |= something_we_always_need_to_do();
 return result;

Cでは、上記のような結果になります。

Swiftで例外的にできることはありますか?

いいえ、何もありません。例外ではなく結果コードを処理するだけです。
例外を使用すると、エラー処理がハッピーパスコードと分離されるようにコードを再編成できますが、それはそれだけです。

2
stonemetal