web-dev-qa-db-ja.com

例外、エラーコード、および差別化された労働組合

最近C#プログラミングの仕事を始めましたが、Haskellにはかなりの経歴があります。

しかし、C#はオブジェクト指向言語であることを理解しています。丸いペグを四角い穴に押し込みたくありません。

マイクロソフトからの記事 Exception Throwing を読みます:

DO NOTエラーコードを返します。

しかし、Haskellに慣れているので、C#データ型 OneOf を使用して、結果を「正しい」値として返すか、エラー(ほとんどの場合は列挙型)を「左」の値。

これはHaskellでの Either の規則によく似ています。

私には、これは例外よりも安全に思えます。 C#では、例外を無視してもコンパイルエラーは発生せず、例外がキャッチされない場合、バブルアップしてプログラムがクラッシュするだけです。これはおそらくエラーコードを無視して未定義の動作を生成するよりも優れていますが、クライアントのソフトウェアをクラッシュさせることは、特に他の多くの重要なビジネスタスクをバックグラウンドで実行している場合、まだ良いことではありません。

OneOf を使用すると、展開して戻り値とエラーコードを処理することを非常に明確にする必要があります。そして、コールスタックのその段階でそれを処理する方法がわからない場合は、現在の関数の戻り値に入れる必要があるため、呼び出し元はエラーが発生する可能性があることを認識しています。

しかし、これはマイクロソフトが提案するアプローチではないようです。

「通常の」例外(ファイルが見つからないなど)を処理するために例外の代わりに OneOf を使用することは合理的なアプローチですか、それともひどい習慣ですか?


制御フローとしての例外は深刻なアンチパターンと見なされます であることを聞いたことは注目に値します。したがって、「例外」がプログラムを終了せずに通常処理するものである場合、それは「制御フロー」ではありません「ある意味で?ここに少し灰色の領域があることを理解しています。

「メモリ不足」のようなものに OneOf を使用していないことに注意してください。回復できないと予想される状態でも例外がスローされます。しかし、解析しないユーザー入力は本質的に「制御フロー」であり、おそらく例外をスローすべきではないなど、かなり合理的な問題のように感じます。


その後の考え:

この議論から私が現在取り除いていることは次のとおりです:

  1. 即時の呼び出し元がcatchを期待し、ほとんどの場合例外を処理して、おそらく別のパスを介して処理を続行する場合は、おそらく戻り型の一部である必要があります。 OptionalまたはOneOfはここで役立ちます。
  2. 直接の呼び出し側がほとんどの場合に例外をキャッチしないと予想される場合は、例外をスローして、手動で例外をスタックに渡す手間を省きます。
  3. 直接の呼び出し元が何をするかわからない場合は、ParseTryParseのように両方を指定してください。
82
Clinton

クライアントのソフトウェアをクラッシュさせることはまだ良いことではありません

それは確かに良いことです。

未定義のシステムはデータの破損、ハードドライブのフォーマット、大統領脅迫メールの送信などの厄介なことを行うため、システムを未定義の状態のままにしてシステムを停止させます。システムを回復して定義済みの状態に戻すことができない場合は、クラッシュが原因です。それが、私たちが静かに自分自身を引き裂くのではなく、クラッシュするシステムを構築する理由です。確かに、クラッシュしない安定したシステムが必要ですが、システムが定義された予測可能な安全な状態にある場合にのみ、本当にそれが必要です。

制御フローとしての例外は深刻なアンチパターンと見なされていると聞きました

それは絶対に本当ですが、それはしばしば誤解されています。彼らが例外システムを発明したとき、彼らは構造化プログラミングを壊していたのではないかと恐れていました。構造化プログラミングこそが​​、必要なときにすべてのことを行うためにforwhileuntilbreak、およびcontinueがある理由ですそれはgotoです。

Dijkstra gotoを非公式に使用する(つまり、好きなところにジャンプする)と、コードを読むのが悪夢になることがわかりました。彼らが私たちに例外システムを与えたとき、彼らは後藤を再発明しているのではないかと恐れました。それで彼らは私たちが理解することを望んで「フロー制御のためにそれを使用しない」ように私たちに言いました。残念ながら、私たちの多くはそうしませんでした。

奇妙なことに、gotoのようにスパゲッティコードを作成するために例外を悪用することはあまりありません。アドバイス自体がさらにトラブルを引き起こしたようです。

基本的に例外は、仮定を拒否することです。ファイルを保存するように要求すると、ファイルは保存可能であり、今後も保存されると見なされます。名前が違法である、HDがいっぱいである、またはデータケーブルを介してネズミがかじったことが原因で、例外が発生する場合があります。これらのエラーはすべて別の方法で処理したり、まったく同じ方法で処理したり、システムを停止させたりすることができます。コードには、想定が満たされなければならないハッピーパスがあります。何らかの方法で例外を設けることで、その幸運な道から外れます。厳密に言えば、ええ、それは一種の「フロー制御」ですが、彼らがあなたに警告していたものではありません。彼らは話していた このようなナンセンス

enter image description here

「例外は例外的でなければならない」。この小さなトートロジーは、例外システムの設計者がスタックトレースを構築する時間を必要とするために生まれました。ジャンプするのに比べて遅いです。 CPU時間を消費します。しかし、システムをログに記録して停止しようとしている場合、または少なくとも次の処理を開始する前に現在の時間を集中的に使用する処理を停止する場合は、ある程度の余裕があります。人々が「フロー制御のための」例外の使用を開始した場合、時間に関するそれらの仮定はすべて窓から出て行きます。したがって、「例外は例外的である必要があります」は、パフォーマンスの考慮事項として実際に提供されました。

それよりはるかに重要なことは、私たちを混乱させないことです。上記のコードで無限ループを見つけるのにどれくらい時間がかかりましたか?

DO NOTエラーコードを返します。

...通常、エラーコードを使用しないコードベースを使用している場合のアドバイスです。どうして?戻り値を保存してエラーコードを確認するのを忘れる人はいないからです。 C言語を使用している場合でも、これは適切な規則です。

OneOf

さらに別の規則を使用しています。コンベンションを設定し、単に別のコンベンションと戦うのではない限り、それは問題ありません。同じコードベースに2つのエラー規則があると混乱します。どういうわけか、他の規則を使用するすべてのコードを取り除くことができたら、次に進みます。

私は大会が好きです。私が見つけたそれの最も良い説明の1つ ここ*

enter image description here

しかし、私が好きなように、私はそれを他の慣習と混合するつもりはありません。 1つを選んで、それに固執してください。1

1:つまり、同時に複数の会議について考えさせないでください。


その後の考え:

この議論から私が現在取り除いていることは次のとおりです:

  1. ほとんどの場合、直接の呼び出し側が例外をキャッチして処理し、おそらく別のパスを介してその処理を続行すると予想される場合は、おそらく戻り型の一部である必要があります。ここでは、OptionalまたはOneOfが役立ちます。
  2. 直接の呼び出し側がほとんどの場合に例外をキャッチしないと予想される場合は、例外をスローして、手動で例外をスタックに渡す手間を省きます。
  3. 直接の呼び出し元が何をするかわからない場合は、おそらくParseとTryParseのように両方を提供します。

それは本当にこれほど単純ではありません。理解する必要がある基本的なことの1つは、ゼロとは何かです。

5月の残り日数は? 0(5月ではないため。既に6月です)。

例外は仮定を拒否する方法ですが、それらが唯一の方法ではありません。例外を使用して仮定を拒否する場合は、ハッピーパスを離れます。しかし、想定したほど単純ではないことを示すハッピーパスを送信する値を選択した場合、それらの値を処理できる限り、そのパスに留まることができます。時々、0はすでに何かを意味するために使用されているため、別の値を見つけて、仮定を拒否する考えをマッピングする必要があります。このアイデアは、古き良き時代の 代数 での使用から認識できます。モナドはそれを助けることができますが、常にモナドである必要はありません。

例えば2

IList<int> ParseAllTheInts(String s) { ... }

これが意図的に何かを投げるように設計されなければならない理由があると思いますか? intを解析できないときに何が得られると思いますか?言う必要すらありません。

それは良い名前のしるしです。申し訳ありませんが TryParse は良い名前だとは思いません。

答えが同時に複数のものになる可能性がある場合、何も得られない場合に例外をスローしないことがよくありますが、何らかの理由で、答えが1つのものであるか何でもない場合は、1つのものを与えるか、またはスローするように主張することに夢中になります。

IList<Point> Intersection(Line a, Line b) { ... }

平行線は本当にここで例外を引き起こす必要がありますか?このリストに複数のポイントが含まれることがないのは本当に悪いことですか?

たぶん意味論的にはそれを受け入れることができない。もしそうなら、それは残念です。しかし、多分モナドは、Listのように任意のサイズがないため、気分が良くなるでしょう。

Maybe<Point> Intersection(Line a, Line b) { ... }

モナドは、それらをテストする必要を回避する特定の方法で使用されることを意図した小さな特別な目的のコレクションです。何が含まれているかに関係なく、それらに対処する方法を見つけることになっています。そうすれば、ハッピーパスはシンプルなままです。触って開いてすべてのモナドをテストすると、それらを間違って使用していることになります。

私は知っています、それは奇妙です。しかし、それは新しいツールです(まあ、私たちにとって)。少し時間をかけてください。ハンマーは、ネジでの使用をやめた方が理にかなっています。


私にふけるなら、私はこのコメントに取り組みたいと思います:

Eitherモナドがエラーコードではなく、OneOfでもないという回答がどれもないのはなぜですか?それらは根本的に異なり、その結果、問題は誤解に基づいているようです。 (ただし、変更された形式でも有効な質問です。)– Konrad RudolphJun 4 `18 at 14:08

これは絶対に本当です。モナドは、例外、フラグ、エラーコードよりもコレクションにはるかに近いです。彼らは賢明に使用されたときにそのようなもののための良い容器を作ります。

131
candied_orange

C#はHaskellではありません。C#コミュニティの専門家の合意に従う必要があります。その代わりに、C#プロジェクトでHaskellのプラクティスを実行しようとすると、チームの他のメンバー全員が疎外され、最終的にはC#コミュニティが異なる方法で行う理由を発見するでしょう。大きな理由の1つは、C#が差別化された共用体を適切にサポートしていないことです。

制御フローとしての例外は深刻なアンチパターンと見なされていると聞きましたが、

これは広く受け入れられている真実ではありません。例外をサポートするすべての言語での選択は、例外をスローする(呼び出し元が自由に処理できない)か、複合値を返す(呼び出し元が処理する必要がある)かのいずれかです。

コールスタックを介してエラー条件を上方に伝播するには、すべてのレベルで条件が必要であり、これらのメソッドの循環的複雑度が2倍になり、その結果、ユニットテストケースの数が2倍になります。典型的なビジネスアプリケーションでは、多くの例外が回復を超えており、最高レベル(Webアプリケーションのサービスエントリポイントなど)に伝播するように任せることができます。

35
kevin cline

あなたは興味深い時にC#に来ました。比較的最近まで、言語は命令型プログラミングスペースにしっかりと存在していました。例外を使用してエラーを通知することは間違いなく標準でした。リターンコードの使用は、言語サポートの欠如に悩まされていました(たとえば、差別化された共用体やパターンマッチングなど)。かなり合理的に、Microsoftの公式ガイダンスは、リターンコードを回避し、例外を使用することでした。

ただし、これには常に例外があり、TryXXXメソッドの形式で、boolean成功結果を返し、outパラメータを介して2番目の値の結果を提供します。これらは関数型プログラミングの「試行」パターンと非常によく似ていますが、Maybe<T>の戻り値ではなく、出力パラメーターを介して結果が返される点が異なります。

しかし、状況は変化しています。関数型プログラミングはますます一般的になり、C#などの言語が対応しています。 C#7.0では、いくつかの基本的なパターンマッチング機能が言語に導入されました。 C#8では、スイッチ式や再帰パターンなど、さらに多くのものが導入されます。これに伴い、C-#の 「関数ライブラリ」、たとえば自分のSuccinc <T>ライブラリ 、これは、差別された組合にも支援を提供します。

これら2つを組み合わせると、次のようなコードの人気が徐々に高まっています。

static Maybe<int> TryParseInt(string source) =>
    int.TryParse(source, out var result) ? new Some<int>(result) : none; 

public int GetNumberFromUser()
{
    Console.WriteLine("Please enter a number");
    while (true)
    {
        var userInput = Console.ReadLine();
        if (TryParseInt(userInput) is int value)
        {
            return value;
        }
        Console.WriteLine("That's not a valid number. Please try again");
    }
}

現時点ではかなりニッチですが、過去2年間でさえ、C#開発者の間の一般的な敵意からそのようなコードへのそのような手法の使用への関心の高まりへの著しい変化に気づきました。

DO NOTreturn error code。」と言うことはまだできていません。時代遅れであり、廃止する必要があります。しかし、その目標に向かって道を下る行進は順調に進んでいます。だからあなたの選択をしてください:それがあなたが物事をやりたい方法であるならば、ゲイの放棄で例外を投げる古い方法に固執してください。または、関数型言語の規約がC#でより一般的になりつつある新しい世界を探求し始めます。

26
David Arno

エラーを返すためにDUを使用することは完全に合理的だと思いますが、OneOfを書いたのでそれを言うでしょう:)とにかく、それはF#などの一般的な慣習なので(たとえば、他の人が述べたResultタイプ) )。

私が通常返すエラーは例外的な状況ではなく、通常どおりですが、処理する必要があるかどうかにかかわらず、モデル化する必要のあるまれな状況が発生することはありません。

これがプロジェクトページの例です。

public OneOf<User, InvalidName, NameTaken> CreateUser(string username)
{
    if (!IsValid(username)) return new InvalidName();
    var user = _repo.FindByUsername(username);
    if(user != null) return new NameTaken();
    var user = new User(username);
    _repo.Save(user);
    return user;
}

これらのエラーに対して例外をスローすることはやりすぎのようです。メソッドのシグネチャで明示的でない、または完全なマッチングを提供するというデメリットは別として、パフォーマンスのオーバーヘッドがあります。

OneOfがc#でどの程度慣用的であるかは、別の質問です。構文は有効であり、かなり直感的です。 DUとレール指向プログラミングはよく知られている概念です。

可能であれば、何かが壊れていることを示す例外を保存します。

5
mcintyre321

「通常の」例外(File Not Foundなど)を処理するために、例外の代わりにOneOfを使用することは妥当なアプローチですか、それともひどい習慣ですか?

制御フローとしての例外は深刻なアンチパターンと見なされると聞いたのは注目に値します。「例外」がプログラムを終了せずに通常処理するものである場合、その「制御フロー」はある意味ではないでしょうか。

エラーにはいくつかの側面があります。私は3つの側面を識別します。それらは防止できますか、それらが発生する頻度、およびそれらから回復できるかどうかです。一方、エラーシグナリングには主に1つの側面があります。つまり、呼び出し側をどの程度叩いて、エラーを強制的に処理させるか、または例外としてエラーを「静かに」バブルアップさせるかです。

予防不可能な、頻繁で、回復可能なエラーは、コールサイトで実際に処理する必要があるエラーです。彼らは、発信者に対抗するように強制することを最も正当化します。 Javaでは、チェックされた例外を作成し、Try*メソッドにするか、区別された共用体を返すことで、同様の効果が得られます。識別された共用体は、関数に戻り値がある場合に特に便利です。これらはnullを返すより洗練されたバージョンです。 try/catchブロックの構文は適切ではなく(括弧が多すぎる)、代替案の見栄えがよくなります。また、例外はスタックトレースを記録するため、少し遅くなります。

防止可能なエラー(プログラマーのエラー/過失のために防止されなかった)と回復不可能なエラーは、通常の例外と同じように機能します。プログラマが予期する努力に値しないことがよくあるため、まれに発生するエラーも通常の例外として機能します(これはプログラムの目的によって異なります)。

重要な点は、メソッドのエラーモードがこれらの次元にどのように適合するかを決定するのは、多くの場合、使用サイトであるということです。これは、以下を検討するときに留意する必要があります。

私の経験では、エラーが防止できず、頻繁で、回復可能な場合、発信者にエラー条件に直面させることは問題ありませんが、発信者がフープをジャンプするように強いられると、非常に迷惑になります必要としない、またはしたくない。これは、呼び出し元がエラーが発生しないことを知っているか、コードを復元できるようにしたくない(まだ)ためです。 Javaでは、これはチェック例外の頻繁な使用とOptionalを返すことに対する不安を説明します(これはMaybeに似ており、最近多くの論争の中で導入されました)。疑わしい場合は、例外をスローし、呼び出し側に処理方法を決定させます。

最後に、エラー状態についての最も重要なことは、それがどのように通知されるかなどの他の考慮事項よりもはるかに重要であることを覚えておいてください徹底的に文書化する

4

OneOfは、Javaのチェック例外とほぼ同等です。いくつかの違いがあります:

  • OneOfは、副作用のために呼び出されるメソッドを生成するメソッドでは機能しません(それらは意味的にが呼び出され、結果が単に無視される可能性があるため)。明らかに、すべての人が純粋な関数を使用しようとする必要がありますが、これは常に可能であるとは限りません。

  • OneOfは、問題が発生した場所に関する情報を提供しません。これは、パフォーマンスを節約し(Javaでスローされる例外はスタックトレースを埋めるためにコストがかかり、C#でも同じです)、エラーで十分な情報を提供するように強制するので、良いですOneOf自体のメンバーです。この情報では不十分な場合があり、問題の発生元を見つけるのに苦労する可能性があるため、これも悪いことです。

OneOfとチェック例外には、共通の重要な「機能」が1つあります。

  • どちらも、コールスタックのあらゆる場所でそれらを処理するように強制します

これはあなたの恐怖を防ぎます

C#では、例外を無視してもコンパイルエラーは発生せず、例外がキャッチされない場合、バブルアップしてプログラムがクラッシュするだけです。

しかし、すでに述べたように、この恐怖は不合理です。基本的に、通常はプログラム内の1つの場所にすべてをキャッチする必要があります(おそらく、すでにフレームワークを使用しているはずです)。あなたとあなたのチームは通常数週間または数年プログラムに取り組んでいるので、あなたはそれを忘れないでしょうか?

ignorable例外の大きな利点は、スタックトレースのすべての場所ではなく、処理したい場所であればどこでも処理できることです。これにより、大量のボイラープレートが排除され(Javaを宣言するコードで「スロー....」を宣言したり、例外をラップしたりするだけです)、コードのバグが少なくなります。メソッドがスローする可能性があるため、 幸いなことに、適切なアクションは通常何も実行していない、つまり、適切に処理できる場所までバブルアップさせます。

4
maaartinus

他の回答では、例外とエラーコードについて詳細に説明しているため、質問に固有の別の視点を追加したいと思います。

OneOfはエラーコードではなく、モナドのようなものです

使用方法、OneOf<Value, Error1, Error2>実際の結果または何らかのエラー状態を表すコンテナです。これはOptionalに似ていますが、値が存在しない場合に、その理由を詳しく説明できる点が異なります。

エラーコードの主な問題は、それらをチェックするのを忘れていることです。しかし、ここでは、文字通りエラーをチェックしないと結果にアクセスできません。

唯一の問題は、結果を気にしない場合です。 OneOfのドキュメントの例は次のとおりです。

public OneOf<User, InvalidName, NameTaken> CreateUser(string username) { ... }

...そして、結果ごとに異なるHTTP応答を作成します。これは、例外を使用するよりもはるかに優れています。しかし、このようなメソッドを呼び出すと。

public void CreateUserButtonClick(String username) {
    UserManager.CreateUser(string username)
}

次に、doに問題があり、例外を選択することをお勧めします。 Railの save と比較 save!

2
Cephalopod

C#のコードは一般に純粋ではないということを考慮する必要があります。副作用に満ちたコードベースで、いくつかの関数の戻り値をどうするかについて気にしないコンパイラでは、あなたのアプローチはあなたにかなりの悲しみを引き起こす可能性があります。たとえば、ファイルを削除するメソッドがある場合、「ハッピーパス上の」エラーコードを確認する理由はありませんが、メソッドが失敗したときにアプリケーションが通知することを確認する必要があります。これは、使用していない戻り値を明示的に無視する必要がある関数型言語とはかなり異なります。 C#では、戻り値は常に暗黙的に無視されます(唯一の例外はプロパティゲッターIIRCです)。

MSDNが「エラーコードを返さない」と指示している理由はまさにこれです-誰もあなたにエラーコードを読み取ることを強制しません。コンパイラーはまったく役に立ちません。ただし、if関数には副作用がないため、安全にcanできますEitherのようなものを使用します。つまり、エラーの結果(ある場合)を無視しても、「適切な結果」を使用しない場合にのみそれを実行できます。これを達成する方法はいくつかあります。たとえば、成功を処理するためのデリゲートを渡すことによってのみ値の「読み取り」を許可する場合がありますおよびエラーの場合、それを Railway Oriented Programming のようなアプローチと簡単に組み合わせることができます(C#では問題なく動作します)。

int.TryParseは途中にあります。それは純粋です。 2つの結果の値が常に何であるかを定義します。つまり、戻り値がfalseの場合、出力パラメーターは0に設定されます。それでも、戻り値を確認せずに出力パラメーターを使用することを妨げるものではありませんが、少なくとも、関数が失敗した場合でも結果が保証されます。

しかし、ここで絶対に重要なのはconsistencyです。誰かがその関数を副作用を持つように変更しても、コンパイラーはあなたを救いません。または、例外をスローします。したがって、このアプローチはC#では完全に問題ありません(私はそれを使用しています)が、チームの全員がそれを理解して使用することを確認する必要があります。関数型プログラミングがうまく機能するために必要な不変条件の多くは、C#コンパイラーによって強制されないため、それらが自分でフォローされていることを確認する必要があります。 Haskellがデフォルトで適用する規則に従わない場合、違反する点に注意する必要があります。これは、コードに導入する関数パラダイムについて覚えておくべきことです。

1
Luaan

正しい記述は例外にはエラーコードを使用しないでください!そして例外以外には例外を使用しないでください。

コードから回復できない(つまり、機能させる)ことができる場合、それは例外です。エラーコードを直接処理してログに記録したり、何らかの方法でユーザーにコードを提供したりする以外は、すぐにコードが行うことはありません。ただし、これはまさに例外です-それらはすべてのハンドリングを処理し、実際に何が起こったかを分析するのに役立ちます。

ただし、自動的に再フォーマットするか、データを修正するようにユーザーに要求する(そしてそのケースを考えた)ことによって処理できる無効な入力のような何かが発生した場合、それはそもそも例外ではありませんそれを期待する。エラーコードやその他のアーキテクチャでこれらのケースを自由に処理できます。例外ではありません。

(私はC#の経験があまりないことに注意してください。ただし、例外とは何かという概念的なレベルに基づいて、私の答えは一般的なケースで成り立つはずです。)

0
Frank Hopkins