魔法の値を返す、例外をスローする、または失敗時にfalseを返す?


83

クラスライブラリのメソッドまたはプロパティを記述する必要が生じることがありますが、実際の答えがなくても失敗することは例外ではありません。何かを判断できない、利用できない、見つからない、現在不可能、または利用可能なデータがもうない。

このような比較的例外的ではない状況で、C#4の失敗を示す3つの解決策があると思います。

  • そうでなければ意味のないマジック値を返します(nullandなど-1)。
  • 例外をスローします(例KeyNotFoundException)。
  • パラメータでfalse実際の戻り値を返し、提供しoutます(などDictionary<,>.TryGetValue)。

質問は次のとおりです。どの例外ではない状況で例外をスローする必要がありますか?そして、私が投げてはいけない場合:パラメータ付きのメソッドを実装する上で与えられた魔法の値を返すのはいつTry*outですか?(私にはoutパラメーターが汚れているようで、適切に使用するのはより手間がかかります。)

設計ガイドライン(Try*メソッドについては何も知りません)、使いやすさ(クラスライブラリを求めて)、BCLとの整合性、読みやすさなどの事実に関する答えを探しています。


.NET Framework基本クラスライブラリでは、3つのメソッドすべてが使用されます。

HashtableC#にジェネリックがなかったときに作成されたように、それが使用され、objectしたがってnullマジック値として返されることに注意してください。しかし、ジェネリックでは、で例外が使用されてDictionary<,>おり、当初はありませんでしたTryGetValue。どうやら洞察が変わります。

明らかに、Item- TryGetValueParse-のTryParse二重性は理由があるので、非例外的な障害に対して例外をスローすることはC#4で行われていないと思います。ただし、Try*メソッドが存在した場合でも、常に存在するとは限りませんでしDictionary<,>.Itemた。


6
「例外はバグを意味します」。辞書に存在しないアイテムを要求するのはバグです。ストリームを使用するたびに、EOFのときにデータを読み取るようにストリームに要求します。(これは私が提出する機会を得られなかった長い美しく整形された答えを要約します:))
KutuluMike

6
あなたの質問が広すぎると思うのではなく、建設的な質問ではないのです。答えはすでに示されているので、標準的な答えはありません。
ジョージストッカー

2
@GeorgeStocker答えが単純明快だったら、質問はしなかったでしょう。特定の選択があらゆる観点(パフォーマンス、可読性、使いやすさ、保守性、設計ガイドライン、一貫性など)から望ましい理由を人々が主張できるという事実は、本質的に非標準的でありながら私の満足度に答えることができます。すべての質問は、ある程度主観的に答えることができます。どうやら、あなたはそれが良い質問であるために、質問がほとんど同様の答えを持っていると期待しています。
ダニエルAA Pelsmaeker

3
@Virtlink:ジョージは、コミュニティで選出されたモデレーターであり、Stack Overflowを維持するために多くの時間をボランティアで提供しています。彼はなぜ質問を閉じたのかを述べ、FAQが彼を裏付けています。
エリックJ.

15
質問はここに属します主観的なものではなく、概念的なものです。 経験則:IDEコーディングの前に座っている場合は、Stack Overflowで聞いてください。ホワイトボードのブレーンストーミングの前に立っている場合は、ここでプログラマーに聞いてください。
ロバートハーベイ

回答:


56

あなたの例は本当に同等だとは思いません。3つの異なるグループがあり、それぞれがその動作の独自の根拠を持っています。

  1. マジック値は、StreamReader.Read有効な回答とはならない単純な値(「-1」IndexOf)など、「まで」の条件がある場合に適したオプションです。
  2. 関数のセマンティクスが、呼び出し元が機能することを確信しているということである場合、例外をスローします。この場合、存在しないキーまたは不適切なdouble形式は本当に例外的です。
  3. セマンティクスが操作が可能かどうかを調べることである場合、outパラメーターを使用してブール値を返します。

提供する例は、ケース2および3について完全に明確です。魔法の値については、これがすべての場合において適切な設計決定であるかどうかを議論することができます。

NaN返されたがMath.Sqrt、特殊なケースである-それは、浮動小数点標準に従っています。


10
数値1に同意しない。マジック値は、次のコーダーがマジック値の重要性を知る必要があるため、決して良い選択肢ではありません。また、読みやすさが損なわれます。マジック値の使用がTryパターンよりも優れている例は考えられません。
0b101010

1
しかし、Either<TLeft, TRight>モナドはどうですか?
サラ

2
@ 0b101010:ちょうどstreamreader.readが安全に返すことができるか調べるいくつかの時間を費やし-1 ...
jmoreno

33

APIのユーザーに彼らがしなければならないことを伝えようとしています。例外をスローした場合、それらを強制的にキャッチすることはありません。ドキュメントを読むだけで、すべての可能性が何であるかを知ることができます。個人的には、特定のメソッドがスローする可能性のあるすべての例外を見つけるためにドキュメントを掘り下げるのは遅くて面倒です(インテリセンスであっても、手動でコピーする必要があります)。

マジック値では、ドキュメントを読む必要があり、場合によってはconstテーブルを参照して値をデコードする必要があります。少なくとも、例外的でない発生と呼ばれる例外のオーバーヘッドはありません。

outパラメータが時々眉をひそめているのに、私はそのTry...構文と構文を好む理由です。これは標準的な.NETおよびC#構文です。APIのユーザーに、結果を使用する前に戻り値を確認する必要があることを伝えています。out有用なエラーメッセージを含む2番目のパラメーターを含めることもできます。これは、デバッグに役立ちます。だからこそTry...outパラメーター付きソリューションに投票します。

別のオプションは、特別な「結果」オブジェクトを返すことですが、これは非常に面倒です。

interface IMyResult
{
    bool Success { get; }
    // Only access this if Success is true
    MyOtherClass Result { get; }
    // Only access this if Success is false
    string ErrorMessage { get; }
}

入力パラメーターのみを持ち、1つのもののみを返すため、関数正しく見えます。それが返すのは、タプルのようなものだけです。

実際、そのようなことに興味がある場合はTuple<>、.NET 4で導入された新しいクラスを使用できます。Item1そしてItem2便利な名前。


3
個人的に、私はしばしばあなたのIMyResult原因のような結果コンテナを使用します。それは単にtruefalseまたは結果値よりも複雑な結果を伝えることが可能です。Try*()文字列からintへの変換のような単純なものにのみ有用です。
this.myself

1
良い投稿。私にとっては、「戻り」値と「出力」パラメーターを分割するのではなく、上で概説した結果構造のイディオムを好みます。整頓された状態に保ちます。
オーシャンエアドロップ

2
2番目の出力パラメーターの問題は、複雑なプログラムの50の関数の深さである場合、そのエラーメッセージをユーザーにどのように伝えますか?例外のスローは、チェックエラーのレイヤーを持つよりもはるかに簡単です。あなたが1つを手に入れるとき、それをただ投げてください、そして、それはあなたがどれほど深くてもかまいません。
ロール

@rolls-出力オブジェクトを使用する場合、即時の呼び出し元は出力で必要なことを行うと仮定します:ローカルで処理する、無視する、または例外にラップしてスローすることでバブルアップします。両方の言葉の長所です-呼び出し元は、可能なすべての出力(列挙など)を明確に確認でき、エラーの処理方法を決定でき、各呼び出しでキャッチする必要はありません。結果をすぐに処理するか無視することを主に期待している場合、オブジェクトを返すのは簡単です。すべてのエラーを上位層にスローしたい場合、例外は簡単です。
-drizin

2
これは、Javaのチェック例外よりも退屈な方法で、C#のチェック例外を再発明しただけです。

17

あなたの例がすでに示しているように、そのようなケースはそれぞれ個別に評価する必要があり、「例外的状況」と「フロー制御」の間にはかなりのスペクトルがあります。元々設計されていたよりも。特に「例外」を使用してそれを実装する可能性についてすぐに議論する場合は、「非例外」の意味について私たち全員が同意することを期待しないでください。

また、どのような設計がコードを読みやすく、保守しやすくするかについても同意しないかもしれませんが、ライブラリ設計者はそれについて明確な個人的なビジョンを持ち、関係する他の考慮事項とのバランスを取るだけでよいと思います。

簡潔な答え

非常に高速なメソッドを設計していて、予期せぬ再利用の可能性を期待している場合を除き、あなたの腸の感覚に従ってください。

長い答え

将来の各呼び出し元は、両方向での希望に応じて、エラーコードと例外を自由に変換できます。これにより、パフォーマンス、デバッガーの使いやすさ、および制限された相互運用性コンテキストを除き、2つの設計アプローチがほぼ同等になります。これは通常パフォーマンスに帰着するので、それに焦点を合わせましょう。

  • 経験則として、例外のスローは通常のリターンよりも200倍遅いことを期待してください(実際には、大きな違いがあります)。

  • 別の経験則として、例外をスローすると、最も粗雑な魔法の値と比較してコードがはるかにきれいになる場合があります。一貫した適切な方法でそれを処理するのに十分なコンテキストがあるポイント。(特別な場合:すべてではないが一部の種類の欠陥の場合nullに自動的にそれ自身を変換する傾向があるため、他の魔法の値よりもここでうまくいく傾向がありますNullReferenceException。通常、常にではありませんが、欠陥の原因に非常に近いです。 )

それでは、レッスンは何ですか?

アプリケーションの有効期間中に数回だけ呼び出される関数(アプリの初期化など)の場合は、よりクリーンで理解しやすいコードを提供するものを使用します。パフォーマンスは問題になりません。

スローアウェイ関数には、よりクリーンなコードを提供するものを使用してください。次に、何らかのプロファイリング(必要な場合)を行い、測定値またはプログラム全体の構造に基づいてトップボトルネックが疑われる場合は、例外をリターンコードに変更します。

高価な再利用可能な関数の場合は、よりクリーンなコードを提供するものを使用してください。基本的に常にネットワークラウンドトリップを実行するか、ディスク上のXMLファイルを解析する必要がある場合、例外をスローするオーバーヘッドはおそらく無視できます。「非例外的障害」から迅速に復帰するよりも、障害の詳細を誤って失わずに失わないことが重要です。

リーンで再利用可能な機能には、より多くの思考が必要です。例外を使用することにより、関数の本体が非常に高速に実行される場合、(多くの)呼び出しの半分で例外が発生する呼び出し元に100倍のスローダウンのようなものを強制します。例外は依然として設計オプションですが、これを買う余裕のない発信者には、オーバーヘッドの少ない代替手段を提供する必要があります。例を見てみましょう。

Dictionary<,>.Item大まかな例を挙げますと、大まかに言って、null値を返すことからKeyNotFoundException.NET 1.1と.NET 2.0の間でスローするように変更されました(Hashtable.Item実際の非ジェネリックな先駆者であると考えている場合のみ)。この「変更」の理由は、ここでは関心がないわけではありません。値タイプのパフォーマンスの最適化(ボクシングなし)により、元のマジック値(null)は非オプションになりました。outパラメータは、パフォーマンスコストの一部を取り戻すだけです。後者のパフォーマンスに関する考慮事項は、をスローするオーバーヘッドと比較して完全に無視できますKeyNotFoundExceptionが、例外設計はここでも優れています。どうして?

  • ref / outパラメーターは、「失敗」の場合だけでなく、毎回コストが発生します
  • 気にする人は誰でもContainsインデクサーを呼び出す前に呼び出すことができ、このパターンは完全に自然に読み取れます。開発者がを呼び出したいが、呼び出すのを忘れた場合、Containsパフォーマンスの問題が忍び寄ることはありません。KeyNotFoundException気づいて修正するのに十分な大きさです。

200xは例外のパフォーマンスについて楽観的だと思います... コメントの直前のblogs.msdn.com/b/cbrumme/archive/2003/10/01/51524.aspxの「パフォーマンスとトレンド」セクションをご覧ください。
gbjbaanb

@gbjbaanb-まあ、多分。この記事では、1%の失敗率を使用して、まったく異なることではないトピックについて説明しています。私自身の考えと漠然と記憶された測定は、テーブル実装されたC ++のコンテキストからのものです(このレポートのセクション5.4.1.2を参照してください.1つの問題は、その種の最初の例外はページフォールト(劇的で可変。。しかし)1時間のオーバーヘッド償却しかし、私がやると.NET 4での実験を投稿し、おそらくこの球場値を微調整します私はすでに差異を強調している。
Jirka Hanika

ref / outパラメーターのコストは高いですか?どうして?またContains、インデクサーの呼び出しの前に呼び出すと、競合状態が発生する可能性がありますが、競合状態は存在しませんTryGetValue
ダニエルAA Pelsmaeker

@gbjbaanb-実験が終了しました。私は怠け者で、Linuxではモノを使用していました。例外により、3秒で〜563000スローされました。 返品は3秒で〜10900000の返品をくれました。それは1:200でさえ1:20です。より現実的なコードには1:100+を考えることをお勧めします。(私が予測したように、パラメータのバリアントは無視できるコストでした
-Jirka Hanika

@Virtlink-一般にスレッドセーフに同意しましたが、特に.NET 4に言及していることを考えると、最適なパフォーマンスを得るConcurrentDictionaryためにマルチスレッドアクセスとDictionaryシングルスレッドアクセスに使用してください。つまり、「Contains」を使用しないと、この特定のDictionaryクラスでコードスレッドが安全になりません。
ジルカハニカ

10

このような比較的例外的ではない状況で、失敗を示すために最善のことは何ですか?その理由は何ですか?

失敗を許すべきではありません。

私は知っています、それは手で波打つ理想主義的ですが、聞いてください。設計を行う際、障害モードのないバージョンを優先する機会がある場合がいくつかあります。失敗する 'FindAll'を使用する代わりに、LINQは単純に空の列挙型を返すwhere句を使用します。使用する前に初期化する必要があるオブジェクトを使用する代わりに、コンストラクターにオブジェクトを初期化させます(または、初期化されていないものが検出されたときに初期化します)。重要なのは、コンシューマコードの障害ブランチを削除することです。これが問題ですので、注意してください。

このためのもう1つの戦略はKeyNotFoundシナリオです。3.0以降に取り組んできたほぼすべてのコードベースには、この拡張メソッドのようなものが存在します。

public static class DictionaryExtensions {
    public static V GetValue<K, V>(this IDictionary<K, V> arg, K key, Func<K,V> ifNotFound) {
        if (!arg.ContainsKey(key)) {
            return ifNotFound(key);
        }

        return arg[key];
    }
}

これには実際の障害モードはありません。ConcurrentDictionary同様のGetOrAddビルトインがあります。

そうは言っても、常に避けられない場合が常にあります。3つすべてにそれぞれの場所がありますが、最初のオプションを優先します。すべてがヌルの危険でできているにもかかわらず、「例外ではない」セットを構成する多くの「アイテムが見つからない」または「結果が適用されない」シナリオによく当てはまります。特に、null許容値型を作成している場合、「これは失敗する可能性があります」の重要性はコード内で非常に明示的であり、忘れがちです。

2番目のオプションは、ユーザーが何かおかしなことをするときに十分です。間違った形式の文字列を提供し、日付を12月42日に設定しようとします。テスト中にすばやく異常に爆発させて、不良コードを特定して修正します。

最後の選択肢は、私がますます嫌うものです。出力パラメーターは扱いにくいため、メソッドを作成する際に、1つのことに焦点を合わせて副作用を持たないなど、いくつかのベストプラクティスに違反する傾向があります。さらに、outパラメーターは通常、成功中にのみ意味があります。そうは言っても、それらは通常、並行性の問題やパフォーマンスの考慮事項(たとえば、DBに2回目のアクセスをしたくない場合)によって制約される特定の操作に不可欠です。

戻り値と出力パラメータが重要な場合、結果オブジェクトに関するScott Whitlockの提案が推奨されます(RegexのMatchクラスと同様)。


7
ここでの問題:outパラメーターは副作用の問題と完全に直交しています。refパラメーターの変更は副作用であり、入力パラメーターを介して渡すオブジェクトの状態の変更は副作用ですが、outパラメーターは関数が複数の値を返すようにする厄介な方法です。 副作用はなく、複数の戻り値があります。
スコットホイットロック

人々がそれらを使用する方法のため、私は傾向があると言いました。あなたが言うように、それらは単に複数の戻り値です。
テラスティン

しかし、フォーマットが間違っているときにパラメーターを嫌い、例外を使用して劇的に物事を爆破する場合...あなたのフォーマットがユーザー入力であるケースをどのように処理しますか?その後、ユーザーが物事を爆破するか、例外をスローしてキャッチすることでパフォーマンスが低下します。右?
ダニエルAA Pelsmaeker

個別の検証メソッドを使用して@virtlink。とにかく、UIが送信する前に適切なメッセージをUIに提供する必要があります。
テラスティン

1
outパラメータには正当なパターンがあり、それは異なる型を返すオーバーロードを持つ関数です。オーバーロード解決は戻り型では機能しませんが、出力パラメーターでは機能します。
ロバートハーベイ

2

常に例外をスローすることを好みます。失敗する可能性のあるすべての機能の中で統一されたインターフェースを備えており、可能な限りうるさい失敗を示しています。これは非常に望ましい特性です。

ParseTryParseは、故障モードを除いて実際には同じものではないことに注意してください。TryParse値を返すこともできるという事実は、やや直交しています。たとえば、何らかの入力を検証している状況を考えてみましょう。値が有効である限り、実際に値が何であるかは気にしません。そして、一種のIsValidFor(arguments)機能を提供することに何の問題もありません。ただし、それが主な動作モードになることはありません。


4
メソッドの多数の呼び出しを処理している場合、例外はパフォーマンスに大きな悪影響を与える可能性があります。例外は例外的な条件のために予約する必要があり、フォーム入力の検証には完全に受け入れられますが、大きなファイルの数値の解析には受け入れられません。
ロバートハーベイ

1
それは一般的なケースではなく、より専門的なニーズです。
-DeadMG

2
だからあなたは言います。しかし、あなたは「常に」という言葉を使いました。:)
ロバートハーベイ

@ DeadMG、RobertHarveyに同意しましたが、「ほとんどの時間」を反映するように修正され、頻繁に使用される高いパフォーマンスに関する一般的な場合の例外(しゃれなし)を指摘すると、答えが過度に投票されたと思います他のオプションを検討するための呼び出し。
ジェラルドデイビス14

例外は高くありません。深くスローされた例外をキャッチすると、システムが最も近いクリティカルポイントまでスタックをほどく必要があるため、コストが高くなる可能性があります。しかし、その「高価な」ものは比較的小さく、ネストされたタイトなループであっても恐れられるべきではありません。
マシューホワイトニング

2

他の人が指摘したように、魔法の値(ブール値の戻り値を含む)は、「範囲の終わり」マーカーを除いてそれほど素晴らしい解決策ではありません。理由:オブジェクトのメソッドを調べても、セマンティクスは明示的ではありません。オブジェクト全体の完全なドキュメントを実際に読む必要があります。「-42が返されたら、bla bla bla」という意味です。

このソリューションは、歴史的な理由またはパフォーマンス上の理由で使用される場合がありますが、それ以外の場合は使用しないでください。

これにより、プローブまたは例外という2つの一般的なケースが残ります。

ここでの経験則は、プログラムが/ unintentionally /何らかの条件に違反したときに処理する場合を除いて、プログラムが例外に反応してはならないということです。これが発生しないことを確認するには、プローブを使用する必要があります。したがって、例外は、関連するプロービングが事前に実行されなかったか、まったく予期しないことが発生したことを意味します。

例:

指定されたパスからファイルを作成します。

Fileオブジェクトを使用して、このパスがファイルの作成または書き込みに有効かどうかを事前に評価する必要があります。

プログラムが何らかの理由で違法または書き込み不可能なパスに書き込もうとする場合、免責を得る必要があります。これは競合状態が原因で発生する可能性があります(他のユーザーがproblした後、ディレクトリを削除したか、読み取り専用にしました)

予期しない失敗(例外によって通知される)を処理し、条件が操作に適しているかどうかを事前に確認(調査)するタスクは、通常、構造が異なるため、異なるメカニズムを使用する必要があります。


0

Tryコードは単に何が起こったかを示す場合、パターンが最良の選択だと思います。私はparamを嫌い、nullableオブジェクトが好きです。私は次のクラスを作成しました

public sealed class Bag<TValue>
{
    public Bag(TValue value, bool hasValue = true)
    {
        HasValue = hasValue;
        Value = value;
    }

    public static Bag<TValue> Empty
    {
        get { return new Bag<TValue>(default(TValue), false); }
    }

    public bool HasValue { get; private set; }
    public TValue Value { get; private set; }
}

だから私は次のコードを書くことができます

    public static Bag<XElement> GetXElement(this XElement element, string elementName)
    {
        try
        {
            XElement result = element.Element(elementName);
            return result == null
                       ? Bag<XElement>.Empty
                       : new Bag<XElement>(result);
        }
        catch (Exception)
        {
            return Bag<XElement>.Empty;
        }
    }

nullableのように見えますが、値の型だけではありません

もう一つの例

    public static Bag<string> TryParseString(this XElement element, string attributeName)
    {
        Bag<string> attributeResult = GetString(element, attributeName);
        if (attributeResult.HasValue)
        {
            return new Bag<string>(attributeResult.Value);
        }
        return Bag<string>.Empty;
    }

    private static Bag<string> GetString(XElement element, string attributeName)
    {
        try
        {
            string result = element.GetAttribute(attributeName).Value;
            return new Bag<string>(result);
        }
        catch (Exception)
        {
            return Bag<string>.Empty;
        }
    }

3
try catchあなたがGetXElement()何度も電話して失敗した場合、あなたのパフォーマンスに大混乱をもたらします。
ロバートハーヴェイ

いつかは関係ありません。Bag callをご覧ください。ご観察いただきありがとうございます

あなたのバッグ<T> CLASが可能System.Nullable <T>別名「NULL可能オブジェクト」とほぼ同じです
aeroson

はい、ほぼpublic struct Nullable<T> where T : struct制約の主な違いです。ところで最新バージョンは、ここでgithub.com/Nelibur/Nelibur/blob/master/Source/Nelibur.Sword/...
GSerjo

0

「マジックバリュー」ルートに興味がある場合、これを解決するもう1つの方法は、Lazyクラスの目的をオーバーロードすることです。Lazyはインスタンス化を延期することを目的としていますが、MaybeやOptionのような使用を妨げるものは何もありません。例えば:

    public static Lazy<TValue> GetValue<TValue, TKey>(
        this IDictionary<TKey, TValue> dictionary,
        TKey key)
    {
        TValue retVal;
        if (dictionary.TryGetValue(key, out retVal))
        {
            var retValRef = retVal;
            var lazy = new Lazy<TValue>(() => retValRef);
            retVal = lazy.Value;
            return lazy;
        }

        return new Lazy<TValue>(() => default(TValue));
    }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.