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


47

多くの現代言語は豊富な例外処理機能を提供していますが、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)で


11
panicまったく同じではないものを無視する場合は、リストGoを追加できます。それに加えて、例外はGOTO、明確な理由で誰もそのように呼び出しませんが、例外を実行する洗練された(しかし快適な)方法以上のものではありません。
JensG

3
あなたの質問に対するソートの答えは、それらを書くために例外のための言語サポートが必要であるということです。通常、言語サポートにはメモリ管理が含まれます。例外はどこでもスローおよびキャッチできるため、制御フローに依存しないオブジェクトを破棄する方法が必要です。
ロバートハーベイ14年

1
@Robert、私はあなたをフォローしていません。C ++は、ガベージコレクションなしで例外をサポートします。
カールビーレフェルト

3
@KarlBielefeldt:私が理解していることから、多大な費用がかかります。その後、再び、そこにあるもの、少なくとも努力の中で、大きな費用をかけずにC ++で行われ、ドメイン知識が必要なの?
ロバートハーベイ14年

2
@RobertHarvey:いいですね。私はこれらのことについて十分に考えていない人々の一人です。ARCはGCであると考えるようになりましたが、もちろんそうではありません。したがって、基本的に(大まかに把握している場合)、オブジェクトの処理が制御フローに依存している言語では、例外は厄介なもの(C ++にもかかわらず)です。
オロム14年

回答:


33

組み込みプログラミングでは、例外は許可されていませんでした。なぜなら、あなたがしなければならないスタックのアンワインドのオーバーヘッドは、リアルタイムのパフォーマンスを維持しようとすると許容できない変動とみなされたためです。スマートフォンは技術的にはリアルタイムプラットフォームと見なすことができますが、組み込みシステムの古い制限が実際に適用されなくなった今では十分に強力です。私は徹底のためにそれを持ち出します。

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

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

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

ここで使用した例ですFutureから、このScalaのチュートリアルは

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ブロックは次のようですtryonFailureブロックは、例外ハンドラのようなものです。Scalaでは、ほとんどの機能言語と同様にFuture、言語自体を使用して完全に実装されます。例外のような特別な構文は必要ありません。つまり、独自の同様の構成体を定義できます。timeoutたとえば、ブロックを追加したり、ログ機能を追加したりできます。

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

オプションは、エラー処理の問題をわずかに異なる方法で解決します。これは、一部のユースケースに適しています。1つの方法だけにとどまりません。

これらは、「例外をなくすことで得られる」種類のものです。


1
だから、Future基本的にそれを待つために停止することなく、関数から返された値を調べる方法です。Swiftと同様に、戻り値ベースですが、Swiftとは異なり、戻り値への応答は後で発生する可能性があります(例外に少し似ています)。右?
オロム14年

1
あなたはFuture正しく理解していますが、私はあなたがSwiftの特性を誤っているかもしれないと思います。たとえば、このstackoverflow answerの最初の部分を参照してください。
カールビーレフェルト14年

うーん、私はSwiftが初めてなので、その答えを解析するのは少し難しいです。しかし、私が間違えなければ、それは本質的に、後で呼び出すことができるハンドラーを渡します。右?
オロム14年

はい。基本的に、エラーが発生したときにコールバックを作成しています。
カールビーレフェルト


19

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

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

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

さらに、意図が呼び出し元に例外を伝播する場合でも、多くの場合、追加のコードを追加して、物事が矛盾​​した状態のままにならないようにする必要がありますマグカップ!)。したがって、多くの場合、例外を使用するコードは、追加のクリーンアップが必要なためにそうしなかったコードと同じくらい複雑に見えます。

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

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


オプションを含むSwiftが持っているそのようなタイプシステムは、これを達成する一種の「強力なタイプシステム」ですか?
オロム14年

2
はい、オプションであり、より一般的には、Sumt / Rustでは「enum」と呼ばれる合計タイプがこれを実現できます。しかし、それらを快適に使用するにはいくつかの追加作業が必要です。Swiftでは、これはオプションのチェーン構文で実現されます。Haskellでは、これは単項記法で実現されます。
Rufflewind 14年

「十分に強力な型システムは、」それはかなり無用だない場合は、スタックトレースを与えることができます
パヴェルPrażakに

1
例外により制御の流れが不明瞭になることを指摘したことに対して+1。これは、より例外は実際にはもっと悪ではないかどうかを理由に立って:ちょうどauxilaryメモとしてgotogotoかなり狭い範囲提供される機能ですが、本当に小さいです、例外はより多くのいくつかのクロスのように動作し、単一の機能に制限されているgotocome from(SEE en.wikipedia.org/wiki/INTERCAL ;-))。コードの任意の2つの部分を接続でき、場合によってはコードをスキップして3番目の機能を実行できます。それができない唯一のこと、できることは、goto戻ることです。
cmaster

2
@PawełPrażak多数の高階関数を扱う場合、スタックトレースはそれほど価値がありません。入力と出力についての強力な保証と副作用の回避は、この間接性が混乱を招くバグを引き起こすのを防ぎます。
ジャック

11

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

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

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

そのため、一部の現代言語では価値がないと判断されています。たとえば、Rustには例外がありますが、「キャッチ」することはできないため、コードが無効な状態のオブジェクトと対話する方法はありません。


1
キャッチの目的は、エラーが発生した後、オブジェクトに一貫性を持たせる(または不可能な場合は終了する)ことです。
-JoulinRouge

@JoulinRouge知ってるよ。しかし、一部の言語では、その機会を与えず、代わりにプロセス全体をクラッシュさせることにしました。これらの言語設計者は、あなたがしたい種類のクリーンアップを知っていますが、それをあなたに与えるにはトリッキーすぎると結論しました、そして、そうすることに伴うトレードオフはそれの価値がないでしょう。私はあなたが彼らの選択に同意しないかもしれないことを理解しています...
チャーリーフラワーズ

6

Rust言語について最初に驚いたことの1つは、Rust言語がキャッチ例外をサポートしていないことです。例外をスローすることはできますが、タスク(スレッドを考えてください。ただし、必ずしも個別のOSスレッドとは限りません)が終了したときに例外をキャッチできます。自分でタスクを開始する場合、タスクが正常に終了したかどうか、または終了したかどうかを尋ねることができますfail!()

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

代わりに、テンプレートを使用してResult処理する必要があるエラーを明示的に渡すのが一般的なイディオムです。これはマクロによって非常に簡単になります。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());
}

1
だから、それはあまりにもこの(ゴーのアプローチのような)がありスウィフト、に似ていると言うことは公正であるassert、ありませんかcatch
オロム14年

1
Swiftで試してください!意味:はい
-gnasher729

6

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

例外はほとんどが開発ツールであり、予期しない場合に本番から妥当なエラーレポートを取得する方法です。

懸念の分離(予期されるエラーのみに対する一般的なハンドラーに到達するまでのフォールスルーと予期されるエラーのみのハッピーパス)は良いことであり、コードをより読みやすく保守しやすくすることは別として、実際に可能な限りすべてのコードを準備することは不可能です予期しないケース。エラー処理コードで膨らませて読みにくくします。

それは実際には「予期しない」の意味です。

ところで、予想されることとそうでないことは、コールサイトでのみ決定できます。そのため、Javaのチェック例外は機能しませんでした-決定はAPIの開発時に行われますが、何が期待されるかまたは予期しないかがまったく明確ではありません。

簡単な例:ハッシュマップのAPIには2つのメソッドがあります。

Value get(Key)

そして

Option<Value> getOption(key)

最初の例外が見つからない場合は例外をスローし、後者はオプションの値を提供します。後者の方が理にかなっている場合もありますが、コードでは特定のキーに値があることを予期しなければならないため、キーがない場合、基本的な仮定が失敗しました。この場合、実際には、呼び出しが失敗した場合にコードパスから抜け出し、一般的なハンドラーに到達することが望ましい動作です。

コードは、失敗した基本的な仮定に対処しようとしてはなりません。

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

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

あちこちでブロックをキャッチするのは非常に悪い考えです。

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

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

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

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

機能的なスタイルのエラー処理は、予想されるエラーの場合に最適です。

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


3

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

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

一般に、Swift関数は、関数が正常に機能した場合の結果や失敗した場合のエラーなど、代替を簡単に返すことができます。エラー処理がはるかに簡単になります。しかし、元の質問への答え:Swiftの設計者は、言語に例外がない場合、安全な言語を作成し、そのような言語で成功するコードを書くことは簡単だと明らかに感じました。


Swiftの場合、これはIMOの正解です。Swiftは既存のObjective-Cシステムフレームワークとの互換性を維持する必要があるため、内部では従来の例外はありません。:私はブログにObjCは、エラー処理のためにどのように機能するかにしばらく前に掲示書いorangejuiceliberationfront.com/...を
uliwitness

2
 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でできないことはありますか?例外がありますか?

いいえ、何もありません。例外の代わりに結果コードを処理することになります。
例外を使用すると、エラー処理がハッピーパスコードとは別になるようにコードを再編成できますが、それはそれに関するものです。


また、同様に、...throw_ioerror()例外をスローするのではなく、エラーを返すための呼び出しですか?
オロム14年

1
@raxacoricofallapatorius例外が存在しないと主張している場合、プログラムは失敗時にエラーコードを返し、成功時に0を返すという通常のパターンに従うと思います。
ストーンメタル14年

1
@stonemetal RustやHaskellなどの一部の言語は、型システムを使用して、例外のように隠れた出口点を追加することなく、エラーコードよりも意味のあるものを返します。錆機能は、例えば、返すことがあります。Result<T, E>のいずれかになります列挙型、Ok<T>またはErr<E>で、Tもしあれば、あなたが望んでいたタイプであること、およびEエラーを表すタイプであること。パターンマッチングといくつかの特定の方法により、成功と失敗の両方の処理が簡素化されます。要するに、例外の欠如が自動的にエラーコードを意味すると仮定しないでください。
8ビットツリー

1

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

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

無効なオブジェクトの状態に関する議論を脇に置いたとしても、大きなアプリを扱うときは常に大きな痛みをもたらします。

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

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


まあ、実際には、これらの例外は多くの場合1か所で処理できます。たとえば、SSLが失敗した場合、DNSが解決できない場合、またはWebサーバーが404を返した場合の「データ更新のダウンロード」機能では、問題ではありません。
ザンリンクス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.