例外をスローするか、コードを失敗させる


52

このスタイルに賛否両論があるかどうか疑問に思っています。

private void LoadMaterial(string name)
{
    if (_Materials.ContainsKey(name))
    {
        throw new ArgumentException("The material named " + name + " has already been loaded.");
    }

    _Materials.Add(
        name,
        Resources.Load(string.Format("Materials/{0}", name)) as Material
    );
}

そのメソッドは、それぞれnameに対して1回だけ実行する必要があります。_Materials.Add()同じに対して複数回呼び出されると、例外がスローされますname。その結果、私のガードは完全に冗長なのでしょうか、それともそれほど明白でない利点がありますか?

誰かが興味を持っているなら、それはC#、Unityです。


3
ガードなしでマテリアルを2回ロードするとどうなりますか?ない_Materials.Add例外をスロー?
user253751

2
実際、私は既にスローで例外を言及しています:P-
非同期

9
サイドノート:1)string.Format例外メッセージを構築するために、文字列連結を使用することを検討してください。2)asキャストが失敗すると予想される場合にのみ使用し、結果を確認しnullます。常にaを取得するMaterial場合は、のようなキャストを使用し(Material)Resources.Load(...)ます。明確なキャスト例外は、後で発生するnull参照例外よりもデバッグが簡単です。
CodesInChaos

3
この特定のケースでは、呼び出し元がコンパニオンLoadMaterialIfNotLoadedまたはReloadMaterial(この名前は改善を使用できる)メソッドが役立つこともあります。
jpmc26

1
別の注意事項:このパターンは、リストにアイテムを追加する場合には問題ありません。ただし、大規模なリストではパフォーマンスの問題が発生するO(n ^ 2)状況に陥るので、リスト全体を埋めるためにこのパターンを使用しないでください。100エントリでは気付かないでしょうが、1000エントリでは確かに、そして10.000エントリでは大きなボトルネックになります。
ピーターB

回答:


109

利点は、「カスタム」例外に、この関数を実装する方法を知らずにこの関数呼び出す人にとって意味のあるエラーメッセージがあることです(将来的にはあなたになるかもしれません!)。

確かに、この場合、彼らはおそらく「標準」例外の意味を推測できますが、コードの奇妙なバグに遭遇するのではなく、契約に違反していることを明確にしています。


3
同意する。ほとんどの場合、前提条件違反に対して例外をスローすることをお勧めします。
フランクヒルマン

22
「利点は、「カスタム」例外に、この関数を実装する方法を知らずにこの関数を呼び出す人にとって意味のあるエラーメッセージがあることです(将来的にはあなたになるかもしれません!)。トップアドバイス。
非同期

2
「明示的は暗黙的よりも優れています。」- パイソンの禅
jpmc26

4
スローされ_Materials.Addたエラーが、呼び出し側に送信したいエラーではない場合でも、このエラーのラベル付けの方法は非効率的だと思います。(通常の操作)の呼び出しが成功するたびにLoadMaterial、同じ健全性テストを2回LoadMaterialもう一度繰り返し_Materials.Addます。それぞれが同じスタイルを採用しているより多くのレイヤーがラップされている場合、同じテストを何度も受けることができます。
マークヴァンレーウェン

15
...代わりに_Materials.Add、無条件に突入することを検討してから、潜在的なエラーをキャッチし、ハンドラーで別のエラーをスローします。余分な作業は、エラーが発生した場合にのみ行われるようになりました。これは、効率をまったく気にしない例外的な実行パスです。
マークヴァンレーウェン

100

Ixrecの答えに同意します。ただし、3番目の選択肢、つまり関数をべき等にすることを検討することをお勧めします。つまり、をスローする代わりに早く戻りArgumentExceptionます。これは、LoadMaterial毎回呼び出す前に既にロードされているかどうかを確認する必要がある場合によくあります。前提条件が少ないほど、プログラマーの認知的負荷が少なくなります。

本当にプログラマーのミスであれば、例外をスローすることをお勧めします。これは、呼び出し前に実行時に確認することなく、素材が既にロードされている場合、コンパイル時に明らかで知られているはずです。


2
一方、機能がサイレントに失敗すると、予期しない動作が発生し、後で発生するバグを見つけにくくなります。
フィリップ

30
@Philipp関数は黙って失敗しません。べき等であるということは、何度も実行することは、一度実行することと同じ効果があることを意味します。つまり、マテリアルが既にロードされている場合、仕様(「マテリアルをロードする必要があります」)はすでに満たされているため、何もする必要はありません。ただ戻ることができます。
ウォーボ

8
実際、私はこれがもっと好きです。もちろん、アプリケーションの要件によって与えられた制約によっては、これは間違っているかもしれません。繰り返しますが、私はべき等メソッドの絶対的なファンです。
FP

6
@Philippについて考えるとLoadMaterial、次の制約があります:「呼び出した後、材料は常に読み込まれ、読み込まれた材料の数は減りませんでした」。「マテリアルを2回追加することはできません」という3番目の制約に例外をスローすると、愚かで直感に反するように思えます。関数をべき等にすることで、コードの実行順序とアプリケーションの状態への依存を減らすことができます。私にとっては二重の勝利のようです。
ベンジャミングリュンバウム

7
私はこのアイデアが好きですが、1つの小さな変更を提案します:何かがロードされたかどうかを反映するブール値を返します。呼び出し元がアプリケーションロジックに本当に関心がある場合、呼び出し元はそれを確認して例外をスローできます。
user949300

8

あなたが尋ねなければならない基本的な質問は次のとおりです。あなたの機能のためのインターフェースは何にしたいですか?特に、_Materials.ContainsKey(name)条件は関数の前提条件ですか?

前提条件でない場合、関数はのすべての可能な値に対して明確に定義された結果を提供する必要がありますname。この場合、スローされた例外nameがの一部ではない場合_Materials、関数のインターフェイスの一部になります。つまり、インターフェイスのドキュメントの一部である必要があり、将来その例外を変更することにした場合、それは重大なインターフェイスの変更になります。

より興味深い質問は、それが前提条件である場合に何が起こるかです。その場合、前提条件自体が関数インターフェースの一部になりますが、この条件に違反した場合の関数の動作必ずしもインターフェースの一部ではありません

この場合、投稿したアプローチ、前提条件違反のチェックおよびエラーの報告は、防御プログラミングとして知られています。防御的プログラミングは、ユーザーが間違えたときにユーザーに通知し、偽の引数で関数を呼び出したという点で優れています。ユーザーコードは、関数が特定の方法で前提条件違反を処理することに依存する可能性があるため、保守の負担が大幅に増加するという点で悪いです。特に、実行時チェックが将来パフォーマンスボトルネックになることが判明した場合(このような単純なケースとは異なり、より複雑な前提条件では非常に一般的です)、それを削除できない可能性があります。

これらの不利な点は非常に重要であり、特定のサークルでは防御的なプログラミングが悪い評判になることが判明しています。ただし、最初の目標はまだ有効です。関数のユーザーが間違えたときに早く気づくようにします。

そのため、最近では多くの開発者がこの種の問題に対してわずかに異なるアプローチを提案しています。例外をスローする代わりに、前提条件をチェックするためにアサートのようなメカニズムを使用します。つまり、デバッグビルドで前提条件をチェックして、ユーザーが早い段階でミスをキャッチできるようにしますが、それらは関数インターフェイスの一部ではありません。違いは一見微妙に見えるかもしれませんが、実際には大きな違いを生む可能性があります。

技術的には、前提条件に違反した関数の呼び出しは未定義の動作です。しかし、実装はそれらのケースを検出し、それが発生した場合はすぐにユーザーに通知することを決定する場合があります。残念ながら、例外はこれを実装するのに適したツールではありません。ユーザーコードは例外に反応する可能性があるため、例外の存在に依存し始める可能性があるためです。

古典的な防御アプローチの問題の詳細な説明、およびアサートスタイルの前提条件チェックの可能な実装については、John Lakosの講演CppCon 2014のDefensive Programming Done Rightを参照してください(スライドビデオ)。


4

ここにはすでにいくつかの答えがありますが、Unity3Dを考慮に入れた答えは次のとおりです(答えはUnity3Dに非常に固有であり、ほとんどのコンテキストでこれを非常に異なる方法で行います)。

一般に、Unity3Dは従来、例外を使用しません。Unity3Dで例外をスローする場合、平均的な.NETアプリケーションとは異なります。つまり、プログラムを停止しません。ほとんどの場合、エディターを一時停止するように構成できます。記録されるだけです。これにより、ゲームが簡単に無効な状態になり、エラーを追跡するのが難しくなるカスケード効果が作成されます。したがって、Unityの場合Add、例外をスローさせることは特に望ましくないオプションです。

しかし、一部のプラットフォーム上のUnityでMonoがどのように機能するかにより、例外の速度を調べることは、場合によっては時期尚早な最適化のケースではありません。実際、iOS上のUnity3Dはいくつかの高度なスクリプト最適化をサポートしており、無効化された例外*はそれらの1つの副作用です。これらの最適化は多くのユーザーにとって非常に有益であることが証明されており、Unity3Dでの例外の使用を制限することを検討するための現実的なケースを示しているため、これは本当に考慮すべきものです。(*コードではなく、エンジンからの管理例外)

Unityでは、より専門的なアプローチを取りたいと思うかもしれません。皮肉なことに、これを書いている時点で非常にダウン投票答え、私はショー一つの方法かもしれません。このような何かを実装し、具体的Unity3Dの文脈で(このような他の場所で何かが本当に受け入れられない、とさえユニティで、それはかなり洗練です)。

私が検討する別のアプローチは、実際には呼び出し側に関する限りエラーを示すのではなく、Debug.LogXX関数を使用することです。そうすることで、何かを奇妙な状態にする危険を冒すことなく、未処理の例外をスローするのと同じ動作が得られます(Unity3Dがそれらを処理する方法のため)。また、これが本当にエラーであるかどうかも考慮してください(同じマテリアルを2回ロードしようとすると、必ずエラーになりますか?それとも、Debug.LogWarningより適切なケースかもしれません)。

また、Debug.LogXX例外の代わりに関数などを使用することに関しては、値を返すもの(GetMaterialなど)から例外がスローされるとどうなるかを考慮する必要があります。エラーをログに記録するとともにnullを渡すことでこれにアプローチする傾向があります(これもUnityのみです)。次に、MonoBehaviorsでnullチェックを使用して、マテリアルなどの依存関係がnull値でないことを確認し、MonoBehaviorが無効な場合は無効にします。いくつかの依存関係を必要とする単純な動作の例は、次のようなものです。

    public void Awake()
    {
        _inputParameters = GetComponent<VehicleInputParameters>();
        _rigidbody = GetComponent<Rigidbody>();
        _rigidbodyTransform = _rigidbody.transform;
        _raycastStrategySelector = GetComponent<RaycastStrategySelectionBehavior>();

        _trackParameters =
            SceneManager.InstanceOf.CurrentSceneData.GetValue<TrackParameters>();

        this.DisableIfNull(() => _rigidbody);
        this.DisableIfNull(() => _raycastStrategySelector);
        this.DisableIfNull(() => _inputParameters);
        this.DisableIfNull(() => _trackParameters);
    }

SceneData.GetValue<>例外をスローする辞書の関数を呼び出すという点で、あなたの例に似ています。ただし、例外をスローする代わりにDebug.LogError、通常の例外と同様にスタックトレースを提供し、nullを返します。続くチェック*は、無効な状態で存在し続けるのではなく、動作を無効にします。

*チェックは、ゲームオブジェクトを無効にするときにフォーマットされたメッセージを出力する小さなヘルパーを使用しているため、そのように見えます**。ifここでの作業を伴う単純なnullチェック(**ヘルパーのチェックは、デバッグビルド(アサートなど)でのみコンパイルされます。Unityでラムダとそのような式を使用すると、パフォーマンスが低下します)


1
真に新しい情報をパイルに追加するための+1。私はそれを期待していませんでした。
Ixrec

3

私は2つの主な答えが好きですが、関数名を改善できることを提案したいと思います。私はJavaに慣れているので、YMMVですが、「add」メソッドは、アイテムが既に存在する場合、IMOが例外をスローするべきではありません。アイテムを再度追加するか、宛先がSetの場合は何もしません。これはMaterials.Addの動作ではないため、TryPut、AddOnce、AddOrThrowなどの名前に変更する必要があります。

同様に、LoadMaterialの名前をLoadIfAbsent、Put、TryLoad、またはLoadOrThrow(回答#1と#2のどちらを使用するかによって異なります)などに変更する必要があります。

C#Unityの命名規則に従ってください。

これは、同じものを2回ロードできる他のAddFooおよびLoadBar関数がある場合に特に役立ちます。明確な名前がなければ、開発者はイライラするでしょう。


C#Dictionary.Add()セマンティクス(msdn.microsoft.com/en-us/library/k7z0zy8k%28v=vs.110%29.aspxを参照)とJava Collection.add()セマンティクス(ドキュメントを参照)には違いがあります。 oracle.com/javase/8/docs/api/java/util/…)。C#はvoidを返し、例外をスローします。一方、Javaはboolを返し、以前に保存された値を新しい値で置き換えるか、新しい要素を追加できない場合に例外をスローします。
キャスパーヴァンデンバーグ

キャスパー-ありがとう、面白い。C#辞書の値をどのように置き換えますか?(Java put()と同等)。そのページに組み込みのメソッドは表示されません。
user949300

良い質問は、一つはDictionary.Removeは、()(参照行うことができますmsdn.microsoft.com/en-us/library/bb356469%28v=vs.110%29.aspx)Dictionary.Add(続く)(参照 msdn.microsoftを.com / en-us / library / bb338565%28v = vs.110%29.aspx)、しかしこれは少し厄介に見えます。
キャスパーヴァンデンバーグ

それがすでに存在する場合は、@ user949300辞書[キー] =値エントリを置き換える
Rupe

@Rupeは、そうでない場合はおそらく何かをスローします。すべては私にとって非常に厄介なようです。エントリはwheteher辞書を設定するほとんどのクライアントは気にしませんでしたが、彼らはちょうど彼らのセットアップコードの後に、それは、という気に、であるが。Java Collections API(IMHO)で1つ獲得してください。PHPも命令に不便で、C#がその前例に従うのは間違いでした。
user949300

3

すべての答えは貴重なアイデアを追加します、私はそれらを結合したいと思います:

LoadMaterial()操作の意図および予想されるセマンティクスを決定します。少なくとも次のオプションがあります。

  • 上の前提条件nameLoadedMaterials:→

    前提条件に違反した場合の効果LoadMaterial()は指定されていませんComicSansMSによる回答のように)。これはの実装と将来の変化のほとんどの自由を許しますLoadMaterial()。または、

  • 呼び出しの影響LoadMaterial(name)nameLoadedMaterials指定されています。どちらか:

    • 仕様には、例外がスローされることが記載されています。または
    • 結果は、(同様に冪等であることを指定状態回答 によってカールBielefeldt

セマンティクスを決定したら、実装を選択する必要があります。提案されている場合、次のオプションと考慮事項:

  • (としてのカスタム例外を投げる提案によりIxrec)→

    利点は、「カスタム」例外に、この関数を呼び出すすべての人にとって意味のあるエラーメッセージがあることです。IxrecUser16547

    • 繰り返しチェックのコストを避けるためにnameLoadedMaterialsをあなたが従うことができますマーク・バン・リーウウェンのアドバイスを:

      ...代わりに、_Materials.Addに無条件で突入し、潜在的なエラーをキャッチし、ハンドラーで別のエラーをスローすることを検討してください。

  • してみましょうDictionay.Addが例外をスロー→

    例外を投げるコードは冗長です。— ジョン・レイナー

    ただし、ほとんどの有権者はIxrecに賛成しています

    この実装を選択する追加の理由は次のとおりです。

    • 呼び出し元がすでにArgumentException例外を処理できること、および
    • スタック情報が失われないようにします。

    ただし、これら2つの理由が重要な場合は、カスタム例外から派生した例外をArgumentExceptionチェーン例外として使用することもできます。

  • 作りLoadMaterial()として冪等をanwserによりカールBielefeldtと最も多く(75回)upvoted。

    この動作の実装オプション:

    • Dictionary.ContainsKey()で確認してください

    • 常にDictionary.Add()を呼び出してArgumentException、挿入するキーがすでに存在する場合にスローするものをキャッチし、その例外を無視します。例外を無視することが、あなたが何をするつもりであり、なぜであるかを文書化します。

      • ときはLoadMaterials()(ほぼ)常にそれぞれのために一度呼ばれname、これを繰り返しチェックするコスト回避nameLoadedMaterials参照 マークヴァンレーベン。しかしながら、
      • ときLoadedMaterials()、多くの場合、同じのために複数回呼び出され name、この招き投げの(高価な)コスト ArgumentExceptionとスタックアンワインド。
    • Dictionary.Addへの失敗した呼び出しの高価な例外のスローとスタックの巻き戻しを回避できるTryGet()にTryAdd類似した-method が存在すると考えました。

      しかし、このTryAdd-methodは存在しないようです。


1
最も包括的な回答であるため+1。これはおそらく、OPが最初に求めていたものをはるかに超えていますが、すべてのオプションの有用な要約です。
Ixrec

1

例外を投げるコードは冗長です。

あなたが電話した場合:

_Materials.Add(name、Resources.Load(string.Format( "Materials / {0}"、name))as Material

同じキーでそれがスローされます

System.ArgumentException

メッセージは、「同じキーを持つアイテムが既に追加されています。」です。

ContainsKey本質的に同じ行動がそれなしで達成されているのでチェックは冗長です。異なる唯一の項目は、例外の実際のメッセージです。カスタムエラーメッセージを表示することで真のメリットがある場合、ガードコードにはメリットがあります。

それ以外の場合、おそらくこの場合のガードをリファクタリングします。


0

これはコーディング規約の質問というよりも、API設計に関する質問だと思います。

呼び出しの期待される結果(契約)は何ですか:

LoadMaterial("wood");

呼び出し側がこのメソッドを呼び出した後にマテリアル「wood」がロードされることを期待/保証する場合、そのマテリアルがすでにロードされているときに例外をスローする理由は見当たりません。

マテリアルのロード中にエラーが発生した場合、たとえばデータベース接続を開くことができなかった場合、またはリポジトリにマテリアル「wood」がない場合は、例外をスローして呼び出し元にその問題を通知するのが適切です。


-2

メソッドを変更して、問題が追加された場合は「true」を返し、そうでない場合は「false」を返します。

private bool LoadMaterial(string name)
{
   if (_Materials.ContainsKey(name))

    {

        return false; //already present
    }

    _Materials.Add(
        name,
        Resources.Load(string.Format("Materials/{0}", name)) as Material
    );

    return true;

}

15
エラー値を返す関数は、例外によって廃止されるはずのアンチパターンです。
フィリップ

これは常にそうですか?OPコードサンプルでは、​​システムにマテリアルを追加しなくても問題はないため、セミプレディケート(en.wikipedia.org/wiki/Semipredicate_problem)の場合のみだと思いました。このメソッドは、実行時にマテリアルがシステム内にあることを確認することを目的としており、それがまさにこのメソッドが行うことです。(失敗した場合でも)返されるブール値は、メソッドがすでにこの問題を追加しようとしたかどうかを示します。ps:同じ名前の複数の事項が存在する可能性があることを考慮していません。
MrIveck

1
私は自分で解決策を見つけたと思います:en.wikipedia.org/wiki / ...これは、Ixrecの答えが最も正しいことを示しています
-MrIveck

@Philippコーダー/仕様が、2回ロードしようとしてもエラーがないと判断した場合。その意味で、戻り値はエラー値ではなく情報値です。
user949300
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.