メソッドやプロパティクラスライブラリの場合を記述しなければならないことが時々あります。そのため、実際の答えはなく、失敗は例外ではありません。判別できない、利用できない、見つからない、現在不可能である、または利用できるデータがない。
このような比較的non-exceptional状況には、C#4での失敗を示す3つの解決策があると思います。
null
や-1
など)を返します。KeyNotFoundException
);false
を返し、実際の戻り値をout
パラメータで提供します( Dictionary<,>.TryGetValue
など)。だから質問は次のとおりです:例外ではない状況で例外をスローする必要がありますか?そして、スローすべきでない場合:上記のTry*
の実装で提供される魔法の値を返すときout
パラメータを持つメソッド? (私にとっては、out
パラメータはダーティであるように見え、適切に使用するのはより困難です。)
設計ガイドライン(Try*
メソッドについては知りません)、使いやすさ(これはクラスライブラリに必要です)、BCLとの整合性、読みやすさなど、実際の答えを探しています。
.NET Framework基本クラスライブラリでは、3つのメソッドすべてが使用されます。
Collection<T>.IndexOf
は-1を返しますStreamReader.Read
は-1を返しますMath.Sqrt
はNaNを返します。Hashtable.Item
はnullを返します。Dictionary<,>.Item
はKeyNotFoundExceptionをスローし、Double.Parse
FormatExceptionをスローします。またはfalse
を返し、out
パラメータに実際の戻り値を提供します:Hashtable
はC#にジェネリックがなかったときに作成されたため、object
を使用するため、マジック値としてnull
を返すことができます。しかし、ジェネリックスでは、例外はDictionary<,>
で使用され、最初はTryGetValue
がありませんでした。どうやら洞察は変化します。
明らかに、Item
-TryGetValue
とParse
-TryParse
の双対性が理由で存在するため、例外ではないエラーに対して例外をスローすることはC#4 未完了。ただし、Try*
が存在する場合でも、Dictionary<,>.Item
メソッドが常に存在するとは限りませんでした。
私はあなたの例が本当に同等であるとは思いません。 3つの異なるグループがあり、それぞれがその動作に対する独自の論理的根拠を持っています。
StreamReader.Read
などの「まで」の条件がある場合、または有効な答えにはならない単純な値(IndexOf
の場合は-1)がある場合に適しています。提供する例は、ケース2と3で完全に明確です。魔法の値については、これがすべてのケースで適切な設計上の決定であるかどうかは議論の余地があります。
Math.Sqrt
によって返されるNaN
は特殊なケースであり、浮動小数点標準に従います。
あなたはAPIのユーザーに彼らがしなければならないことを伝えようとしています。例外をスローしても、例外をキャッチする必要はありません。ドキュメントを読むだけで、すべての可能性を知ることができます。個人的には、特定のメソッドがスローする可能性のあるすべての例外を見つけるためにドキュメントを掘り下げるのは遅くて面倒です(インテリセンスの場合でも、手動でコピーする必要があります)。
マジック値でも、ドキュメントを読み、値をデコードするためにconst
テーブルを参照する必要があります。少なくとも、例外ではないオカレンスと呼ばれるものに対する例外のオーバーヘッドはありません。
そのため、out
パラメータが不快な場合がありますが、私はTry...
構文を使用したその方法を好みます。これは、標準的な.NETおよびC#構文です。 APIのユーザーに、結果を使用する前に戻り値を確認する必要があることを伝えています。 2番目のout
パラメータを含めると、役立つエラーメッセージが表示され、デバッグに役立ちます。だから、私はout
パラメータ付きのTry...
に投票します。
別のオプションは、特別な「結果」オブジェクトを返すことですが、これはかなり面倒です。
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; }
}
次に、関数looksです。これは、入力パラメータしかなく、1つのものだけを返すためです。それが返すのはタプルのようなものだけです。
実際、そのようなことに興味がある場合は、.NET 4で導入された新しいTuple<>
クラスを使用できます。 Item1
とItem2
に役立つ名前を付けることはできません。
あなたの例がすでに示しているように、そのようなケースはそれぞれ個別に評価する必要があり、「例外的な状況」と「フロー制御」の間にはかなりのグレーのスペクトルがあります。特に、メソッドが再利用可能であり、まったく異なるパターンで使用される場合もともと設計されていたよりも。特に「例外」を使用してそれを実装する可能性についてすぐに話し合う場合は、ここにいるすべての人が「非例外」の意味に同意することを期待しないでください。
また、どのデザインがコードを最も読みやすく維持しやすいかについては同意しないかもしれませんが、ライブラリ設計者はそれについて明確な個人的なビジョンを持ち、関係する他の考慮事項とバランスを取るだけでよいと思います。
短い答え
かなり高速なメソッドを設計していて、予期せぬ再利用の可能性がある場合を除いて、直感を忘れないでください。
長い答え
将来の各呼び出し元は、エラーコードと例外を双方向で自由に変換できます。これにより、パフォーマンス、デバッガーの使いやすさ、一部の制限された相互運用性のコンテキストを除いて、2つの設計アプローチはほぼ同等になります。これは通常、パフォーマンスに要約されるので、それに焦点を当てましょう。
経験則として、例外のスローは通常のリターンより200倍遅いことを期待してください(実際には、大幅な差異があります)。
別の経験則として、例外をスローすると、大まかな魔法の値に比べてコードがよりクリーンになることがよくあります。これは、プログラマがエラーコードを別のエラーコードに変換することに依存していないためです。一貫して適切な方法でそれを処理するための十分なコンテキストがあるポイント。 (特別な場合:null
は、一部の、ただしすべてではないタイプの欠陥の場合に自動的にNullReferenceException
に自動的に変換される傾向があるため、他の魔法の値よりもうまく機能する傾向があります。通常、常にではないが、欠陥の原因に非常に近い。)
それで、教訓は何ですか?
アプリケーションの存続期間中に数回だけ呼び出される関数(アプリの初期化など)の場合は、コードをより簡潔でわかりやすいものにしてください。パフォーマンスは問題になりません。
使い捨て関数の場合は、よりクリーンなコードが得られるものを使用してください。次に、プロファイリングを行い(必要な場合)、測定値または全体的なプログラム構造に基づいて、ボトルネックの疑いがある場合は例外を戻りコードに変更します。
高価な再利用可能な関数については、よりクリーンなコードを提供するものを使用してください。基本的に常にネットワークラウンドトリップを実行するか、ディスク上のXMLファイルを解析する必要がある場合、例外をスローするオーバーヘッドはごくわずかです。 「例外的でない障害」から迅速に戻ることよりも、偶然ではなく、障害の詳細を失わないことがより重要です。
無駄のない再利用可能な機能には、さらに検討が必要です。例外を使用することで、強制呼び出し元の(多くの)呼び出しの半分で例外が発生する場合、100倍の速度が低下します。関数の本体は非常に高速に実行されます。例外は依然として設計オプションですが、これを利用できない発信者には、オーバーヘッドの少ない代替手段を提供する必要があります。例を見てみましょう。
Dictionary<,>.Item
は、大まかに言うと、.NET 1.1と.NET 2.0の間でnull
値を返すことからKeyNotFoundException
をスローするように変更されました(Hashtable.Item
は、その実用的な非ジェネリックフォアランナーになるためです)。この「変化」の理由は、ここで関心がないわけではありません。値型のパフォーマンス最適化(ボクシングは不要)により、元のマジック値(null
)はオプションではなくなりました。 out
パラメータは、パフォーマンスコストのごく一部を取り戻すだけです。この後者のパフォーマンスの考慮事項は、KeyNotFoundException
をスローするオーバーヘッドと比較して、完全に無視できるですが、例外の設計はここでも優れています。どうして?
Contains
を呼び出すことができ、このパターンは完全に自然に読み取られます。開発者がContains
の呼び出しを望んでいるのに忘れている場合、パフォーマンスの問題が発生することはありません。 KeyNotFoundException
は十分に大きく、気付かれて修正されます。失敗を示すために、このような比較的例外的でない状況で何をするのが最善であり、なぜですか?
失敗を許してはいけません。
私は知っています、それは手波で理想的ですが、私に聞いてください。設計を行う際に、失敗モードのないバージョンを優先する機会がある場合がいくつかあります。失敗する '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の危険から成っていますが、よく知られているため、「例外ではない障害」セットを構成する多くの「アイテムが見つかりません」または「結果は該当しません」シナリオに適合します。特にnull可能値型を作成する場合、「this may fail」の重要性はコードで非常に明示的であり、忘れたりねじ込んだりするのは困難です。
2番目のオプションは、ユーザーが馬鹿なことをする場合に十分です。間違った形式の文字列を与え、日付を12月42日に設定しようとします...不正なコードが特定されて修正されるように、テスト中に迅速かつ壮観に爆発させたいものです。
最後のオプションは、私がますます嫌いなものです。出力パラメータは扱いにくく、1つのことに集中して副作用がないなど、メソッドを作成するときにいくつかのベストプラクティスに違反する傾向があります。さらに、outパラメータは通常、成功時にのみ意味があります。とは言っても、それらは通常、同時実行の懸念、またはパフォーマンスの考慮事項(たとえば、DBへの2回目のアクセスを行いたくない場合)によって制約される特定の操作に不可欠です。
戻り値と出力パラメーターが重要な場合は、結果オブジェクトに関するScott Whitlockの提案(RegexのMatch
クラスなど)が推奨されます。
常に例外を投げることを好む。失敗する可能性のあるすべての関数の中で統一されたインターフェイスがあり、失敗をできるだけうるさく示します-非常に望ましいプロパティです。
Parse
とTryParse
は、失敗モードを除いて、実際には同じではないことに注意してください。 TryParse
も値を返すことができるという事実は、実際には多少直交しています。たとえば、入力を検証している状況を考えてみましょう。値が有効である限り、実際には値が何であるかは気にしません。そして、一種のIsValidFor(arguments)
関数を提供することに何の問題もありません。しかし、それがprimary操作モードになることはありません。
他の人が指摘したように、魔法の値(ブール値の戻り値を含む)は、「範囲の終わり」マーカーとしての場合を除いて、それほど優れたソリューションではありません。理由:オブジェクトのメソッドを調べても、セマンティクスは明示的ではありません。オブジェクト全体の完全なドキュメントを実際に読む必要があります。「ああ、それが-42を返した場合、それは何とか何とかなる」を意味します。
このソリューションは、歴史的な理由またはパフォーマンス上の理由で使用できますが、それ以外の場合は避けてください。
これにより、2つの一般的なケースが残ります。プローブまたは例外です。
ここでの経験則は、プログラムが/意図せず/何らかの条件に違反したときに処理する場合を除いて、プログラムは例外に反応すべきではないということです。これが起こらないようにするために、プロービングを使用する必要があります。したがって、例外は、関連するプロービングが事前に実行されなかったか、まったく予期しないことが発生したことを意味します。
例:
特定のパスからファイルを作成したい。
Fileオブジェクトを使用して、このパスがファイルの作成または書き込みに対して正当であるかどうかを事前に評価する必要があります。
プログラムが何らかの理由で違法または書き込み不可能なパスに書き込もうとする場合は、免責事項を取得する必要があります。これは競合状態が原因で発生する可能性があります(他のユーザーが問題を解決した後、ディレクトリを削除したか、読み取り専用にした)
予期しない失敗(例外によって通知される)を処理し、事前に条件が操作に適しているかどうかを確認(プローブ)するタスクは、通常、構造が異なるため、異なるメカニズムを使用する必要があります。
「マジックバリュー」ルートに興味がある場合、これを解決するもう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));
}
コードが何が起こったかを示すだけの場合、Try
パターンが最良の選択だと思います。私はparamが嫌いで、null許容オブジェクトが好きです。次のクラスを作成しました
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;
}
}
Null可能のように見えますが、値タイプだけではありません
もう一つの例
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;
}
}