例外をスローするか、nullを返すかを制御するパラメーター-良い習慣ですか?


24

失敗時に例外がスローされるか、nullが返されるかを制御する追加のブール型パラメーターを持つメソッド/関数によく遭遇します。

どちらの場合がより良い選択であるかについてはすでに議論がありますので、ここではこれに焦点を合わせません。たとえば、マジック値を返す、例外をスローする、失敗した場合にfalseを返すなどを参照してください

代わりに、両方の方法をサポートしたい理由があると仮定しましょう。

個人的には、そのようなメソッドはむしろ2つに分割する必要があると思います。1つは失敗時に例外をスローし、もう1つは失敗時にnullを返します。

それで、どちらが良いですか?

A:$exception_on_failureパラメータ付きの1つのメソッド。

/**
 * @param int $id
 * @param bool $exception_on_failure
 *
 * @return Item|null
 *   The item, or null if not found and $exception_on_failure is false.
 * @throws NoSuchItemException
 *   Thrown if item not found, and $exception_on_failure is true.
 */
function loadItem(int $id, bool $exception_on_failure): ?Item;

B:2つの異なる方法。

/**
 * @param int $id
 *
 * @return Item|null
 *   The item, or null if not found.
 */
function loadItemOrNull(int $id): ?Item;

/**
 * @param int $id
 *
 * @return Item
 *   The item, if found (exception otherwise).
 *
 * @throws NoSuchItemException
 *   Thrown if item not found.
 */
function loadItem(int $id): Item;

編集:C:他に何か?

多くの人が他のオプションを提案したり、AとBの両方に欠陥があると主張しています。そのような提案や意見は歓迎され、関連性があり有用です。完全な回答には、このような追加情報を含めることができますが、署名/動作を変更するパラメーターが良いアイデアであるかどうかという主要な質問にも対処します。

ノート

誰かが疑問に思っている場合:例はPHPにあります。しかし、PHPまたはJavaにある程度似ている限り、この質問は言語全体に当てはまると思います。


5
私はPHPで作業しなければならず、非常に熟練していますが、それは単に悪い言語であり、その標準ライブラリはいたるところにあります。読むeev.ee/blog/2012/04/09/php-a-fractal-of-bad-designの参考のため。そのため、一部のPHP開発者が悪い習慣を採用するのは驚くことではありません。しかし、この特定のデザインの匂いはまだ見ていません。
ポリグノーム

与えられた答えの本質的なポイントを見逃しています:いつどちらを使うか。見つからないアイテムが予期せず、エラー(パニックになるまでの時間)と見なされる場合に、例外メソッドを使用します。不足しているアイテムがありそうでなく、間違いなくエラーではなく、通常のフロー制御に含める可能性がある場合に、Try ...メソッドを使用します。
マーティンマート


1
@Polygnomeその通りです。標準ライブラリ関数はいたるところにあり、多くの貧弱なコードが存在し、リクエストごとに1プロセスの問題があります。しかし、バージョンごとに改善されています。型とオブジェクトモデルは、そこから期待できるすべてまたはほとんどのことを行います。ジェネリック、共分散、反分散、署名を指定するコールバックタイプなどを見たいと思っています。これの多くは議論されているか、実装の途中です。標準的なライブラリの癖は簡単になくなるわけではありませんが、全体的な方向性は良いと思います。
ドンキホーテ

4
1つの提案:ではなく、あなたにとって意味があるloadItemOrNull(id)かどうかloadItemOr(id, defaultItem)を検討してください。アイテムが文字列または数値の場合、多くの場合そうです。
user949300

回答:


35

あなたは正しい:いくつかの理由で、2つの方法がはるかに優れています。

  1. Javaでは、潜在的に例外をスローするメソッドシグネチャにこの例外が含まれます。他の方法ではできません。これにより、一方と他方に何が期待されるかが特に明確になります。

  2. メソッドのシグネチャが例外について何も語らないC#などの言語では、パブリックメソッドはまだ文書化される必要があり、そのような文書には例外が含まれています。単一のメソッドを文書化するのは簡単ではありません。

    あなたの例は完璧です。コードの2番目の部分のコメントは非常に明確に見え、「見つかった場合はアイテム(そうでない場合は例外)」から「アイテム」に短縮します。あなたがそれに与えた説明は自明です。

  3. 単一のメソッドの場合、実行時ブール値パラメーターの値を切り替えたい場合はほとんどありません。その場合、呼び出し側は両方のケース(null応答例外)を処理する必要があります)、コードを必要以上に難しくします。選択は実行時ではなくコードの作成時に行われるため、2つの方法が最適です。

  4. .NET Frameworkなどの一部のフレームワークでは、このような状況の規則確立されており、提案されたように2つの方法で解決されます。唯一の違いは、Ewan の回答で説明されているパターンを使用しているためint.Parse(string): int、例外をスローする一方で使用int.TryParse(string, out int): boolしないことです。この命名規則は.NETのコミュニティで非常に強力であり、記述した状況にコードが一致する場合は常に従う必要があります。


1
ありがとう、これは私が探していた答えです!目的は、Drupal CMS(drupal.org/project/drupal/issues/2932772)についてこれに関する議論を開始したことであり、これは個人的な好みに関するものではなく、他の人からのフィードバックでバックアップしたいものです。
ドンキホーテ

4
少なくとも.NETの実際の長年の伝統は、2つの方法を使用せず、代わりに適切なタスクに適切な選択をすることです。例外は例外的な状況であり、予想される制御フローを処理することではありません。文字列を整数に解析できませんか?非常に予期しない、または例外的な状況ではありません。int.Parse()は本当に悪い設計上の決定と見なされTryParseます。それがまさに.NETチームが先に進んで実装した理由です。
VOO

1
どのようなユースケースを扱っているかを考える代わりに2つのメソッドを提供するのが簡単な方法です。あなたが何をしているのかを考えずに、代わりにAPIのすべてのユーザーに責任を負わせます。確かに機能しますが、それは優れたAPIデザインの特徴ではありません(または問題のデザインです。たとえば、Joelのこれに対する見解を読んでください。)
Voo

@Voo有効なポイント。実際、2つのメソッドを提供すると、呼び出し元に選択肢が与えられます。おそらく、一部のパラメーター値ではアイテムが存在すると予想されますが、他のパラメーター値では存在しないアイテムは通常のケースと見なされます。おそらく、コンポーネントは、アイテムが常に存在することが期待される1つのアプリケーションと、存在しないアイテムが正常と見なされる別のアプリケーションで使用されます。おそらく、呼び出し元はを呼び出す->itemExists($id)前に呼び出し->loadItem($id)ます。または、過去に他の人によって行われた単なる選択であり、私たちはそれを当たり前のことと考えなければなりません。
ドンキホーテ

1
理由1b:一部の言語(Haskell、Swift)では、署名にnullabilityが存在する必要があります。特に、Haskellには、null許容値(data Maybe a = Just a | Nothing)に対してまったく異なる型があります。
ジョンドヴォルザーク

9

興味深いバリエーションは、Swift言語です。Swiftでは、関数を「スロー」として宣言します。つまり、エラーをスローすることを許可します(Javaの例外と似ていますが、まったく同じではありません)。呼び出し元は、次の4つの方法でこの関数を呼び出すことができます。

a。try / catchステートメントで、例外をキャッチして処理します。

b。呼び出し元自体がスローとして宣言されている場合は、単に呼び出してください。スローされたエラーは、呼び出し元によってスローされたエラーに変わります。

c。関数呼び出しにマークを付けると、try!「この呼び出しはスローされないはずです」という意味になります。呼び出された関数スローする場合、アプリケーションはクラッシュすることが保証されています。

d。関数呼び出しにマークを付けると、try?「関数はスローできることはわかっていますが、どのエラーがスローされるかは気にしません」という意味になります。これにより、関数の戻り値がタイプ " T"からタイプ " optional<T>"に変更され、スローされた例外はnilを返すようになります。

(a)と(d)はあなたが尋ねているケースです。関数がnil 返し例外をスローできる場合はどうなりますか?その場合、戻り値の型はoptional<R>いくつかの型Rに対して" "になり、(d)はこれを " optional<optional<R>>"に変更します。これは少し不可解で理解しにくいが、完全に問題ありません。また、Swift関数はを返すことができるoptional<Void>ため、(d)はVoidを返す関数をスローするために使用できます。


2

bool TryMethodName(out returnValue)アプローチが好きです。

メソッドが成功したかどうかの最終的な指示を提供し、try catchブロックで例外をスローするメソッドをラップする名前が一致します。

nullを返すだけの場合、失敗したか、戻り値が正当なnullであったかはわかりません。

例えば:

//throws exceptions when error occurs
Item LoadItem(int id);

//returns false when error occurs
bool TryLoadItem(int id, out Item item)

使用例(outは初期化されていない参照渡しを意味します)

Item i;
if(TryLoadItem(3, i))
{
    Print(i.Name);
}
else
{
    Print("unable to load item :(");
}

申し訳ありませんが、ここで私を失いました。bool TryMethodName(out returnValue)元の例では、「アプローチ」はどのように見えますか?
ドンキホーテ

例を追加するために編集
Ewan

1
だから、戻り値の代わりに参照パラメータを使用するので、ブール値の成功のために戻り値が自由になりますか?この「out」キーワードをサポートするプログラミング言語はありますか?
ドンキホーテ

2
はい、申し訳ありませんが、 'out'はc#に固有であることに気づきません
Ewan

1
常にノーとは限りません。しかし、アイテム3 ヌルの場合どうでしょうか?エラーがあったかどうかわからない
ユアン

2

通常、booleanパラメーターは、関数を2つに分割できることを示します。通常、booleanを渡すのではなく、常に分割する必要があります。


1
資格やニュアンスを追加することなく、これをルールとして除外しません。引数としてブール値を削除すると、他の場所でダムif / elseを書くことを余儀なくされる可能性があるため、変数ではなく、記述されたコードでデータをエンコードします。
ジェラルド

@Gerard、例を挙げてください。
最大

@Maxブール変数がある場合b:呼び出す代わりに、他の引数、または言語に三項演算子とファーストクラスの関数/メソッドがある場合などfoo(…, b);を記述if (b) then foo_x(…) else foo_y(…);して繰り返す必要があり((b) ? foo_x : foo_y)(…)ます。そして、これはプログラムのいくつかの異なる場所から繰り返されることさえあります。
ブラックジャック

@BlackJackええ、私はあなたに同意するかもしれません。この例を考えてみましたが、if (myGrandmaMadeCookies) eatThem() else makeFood()このような別の関数でラップするかもしれませんcheckIfShouldCook(myGrandmaMadeCookies)。あなたが言及したニュアンスは、元の質問のように関数の動作方法にブール値を渡すかどうかに関係するのか、それともモデルに関する情報を渡すのか、私がちょうど与えたおばあちゃんの例。
マックス

1
私のコメントで言及されているニュアンスは、「あなたがいつもするべきです」という意味です。「a、b、cが該当する場合[...]」と言い換えることもできます。あなたが言う「ルール」はすばらしいパラダイムですが、ユースケースの非常に多様なものです。
ジェラルド

1

Clean Codeは、ブールフラグ引数の意味は呼び出しサイトでは不透明なので、避けることをお勧めします。

loadItem(id, true) // does this throw or not? We need to check the docs ...

一方、別の方法では表現力豊かな名前を使用できます。

loadItemOrNull(id); // meaning is obvious; no need to refer to docs

1

AまたはBのいずれかを使用する必要がある場合、Arseni Mourzenkosの回答に記載されている理由により、2つの別々の方法であるBを使用します

しかし、別の方法1Optionalがあります。Javaではが呼び出されますが、言語が同様のクラスをまだ提供していない場合、独自の型を定義できる他の言語に同じ概念を適用できます。

次のようにメソッドを定義します。

Optional<Item> loadItem(int id);

メソッドではOptional.of(item)、アイテムが見つかった場合に戻るか、見つからOptional.empty()ない場合に戻ります。あなたは2を投げたり、戻ったりしませんnull

メソッドのクライアントにとっては、のようなものですがnull、アイテムが欠落している場合について考えることを強いられるという大きな違いがあります。ユーザーは、結果がない可能性があるという事実を単に無視することはできません。Optional明示的なアクションからアイテムを取得するには、次が必要です。

  • loadItem(1).get()NoSuchElementException見つかったアイテムをスローまたは返します。
  • loadItem(1).orElseThrow(() -> new MyException("No item 1"))返される値Optionalが空の場合、カスタム例外を使用することもできます。
  • loadItem(1).orElse(defaultItem)例外をスローせずに、見つかったアイテムまたは渡されたアイテムdefaultItem(いずれか)を返しますnull
  • loadItem(1).map(Item::getName)存在する場合は、再びOptionalアイテム名を返します。
  • さらにいくつかのメソッドがあります。JavadocをOptional参照してください。

利点は、アイテムがなくても単一のメソッド提供する必要がある場合にどうなるかは、クライアントコード次第であるということです。


1私はそれのようなものだと思いtry?におけるgnasher729s答えが、私はスウィフトを知らないとして、それは私には完全には明らかではないが、それはスウィフトに固有であるように、言語機能のようです。

2データベースとの通信に問題があったためにアイテムがあるかどうかわからない場合など、他の理由で例外をスローできます。


0

2つの方法の使用に同意する別のポイントとして、これは非常に簡単に実装できます。PHPの構文がわからないので、C#で例を作成します。

public Widget GetWidgetOrNull(int id) {
    // All the lines to get your item, returning null if not found
}

public Widget GetWidget(int id) {
    var widget = GetWidgetOrNull(id);

    if (widget == null) {
        throw new WidgetNotFoundException();
    }

    return widget;
}

0

この方法では、値を返すだけでなく、どの値を正確に返すかを教えてくれます。

public int GetValue(); // If you can't get the value, you'll throw me an exception
public int GetValueOrDefault(); // If you can't get the value, you'll return me 0, since it's the default for ints

TryDoSomething()も悪いパターンではありません。

public bool TryParse(string value, out int parsedValue); // If you return me false, I won't bother checking parsedValue.

前者はデータベースインタラクションで、後者は構文解析でより多く使用されています。

他の人が既に言ったように、フラグパラメータは通常コードのにおいです(コードのにおいは、あなたが何か間違ったことをしていることを意味するものではありません。正確に名前を付けますが、そのフラグの使用方法を理解するのが難しくなる微妙な違いがあり、メソッド本体の内部を見ることになります。


-1

他の人が別の方法で提案する一方で、エラーの処理方法を示すパラメーターを持つメソッドを持つことは、2つの使用シナリオに別々のメソッドを使用するよりも優れていることをお勧めします。「エラースロー」と「エラーリターンエラー表示」の使用方法に個別のエントリポイントを設定することが望ましい場合ありますが、両方の使用パターンに対応できるエントリポイントを用意することで、ラッパーメソッドでのコードの重複を回避できます。簡単な例として、機能がある場合:

Thing getThing(string x);
Thing tryGetThing (string x);

また、同様に動作するが、角括弧で囲まれたxを持つ関数が必要な場合は、2組のラッパー関数を記述する必要があります。

Thing getThingWithBrackets(string x)
{
  getThing("["+x+"]");
}
Thing tryGetThingWithBrackets (string x);
{
  return tryGetThing("["+x+"]");
}

エラーの処理方法に関する引数がある場合、必要なラッパーは1つだけです。

Thing getThingWithBrackets(string x, bool returnNullIfFailure)
{
  return getThing("["+x+"]", returnNullIfFailure);
}

ラッパーが十分に単純であれば、コードの複製はそれほど悪くないかもしれませんが、変更が必要になる可能性がある場合、コードを複製するには変更を複製する必要があります。追加の引数を使用しないエントリポイントを追加することを選択した場合でも:

Thing getThingWithBrackets(string x)
{
   return getThingWithBrackets(x, false);
}
Thing tryGetThingWithBrackets(string x)
{
   return tryGetThingWithBrackets(x, true);
}

すべての「ビジネスロジック」を1つのメソッドに保持できます。これは、失敗した場合の対処方法を示す引数を受け入れるメソッドです。


あなたは正しい:デコレータとラッパー、さらには通常の実装でも、2つのメソッドを使用するとコードが重複します。
ドンキホーテ

例外を投げるバージョンと投げないバージョンを別々のパッケージに入れ、ラッパーを汎用にするのは確かですか?
ウィルフォードクロフォード

-1

例外がスローされるかどうかを制御するフラグを使用すべきではないことを説明するすべての答えは良いと思います。彼らのアドバイスは、その質問をする必要を感じている人への私のアドバイスです。

ただし、このルールには意味のある例外があることを指摘したいと思いました。他の何百人もが使用するAPIの開発を開始するのに十分なレベルに達したら、そのようなフラグを提供したい場合があります。APIを書いているとき、あなたは純粋主義者に書いているだけではありません。あなたは本当の欲求を持っている本当の顧客に書いています。あなたの仕事の一部は、彼らをあなたのAPIに満足させることです。それはプログラミングの問題ではなく、社会的な問題になり、時には社会的な問題が、それ自体ではプログラミングの解決策としては理想的ではない解決策を指示します。

APIの2人の異なるユーザーがいることがあります。1人は例外を必要とし、もう1人は不要です。これが発生する可能性のある実際の例は、開発と組み込みシステムの両方で使用されるライブラリを使用する場合です。開発環境では、ユーザーはどこでも例外を持ちたいと思うでしょう。例外は、予期しない状況を処理するために非常に一般的です。ただし、リアルタイムの制約を分析するのは難しすぎるため、多くの組み込み状況では禁止されています。あなたが実際に平均だけでなく気にしたとき関数を実行するのにかかる時間でなく、個々の楽しみにかかる時間、任意の場所でスタックを巻き戻すという考えは非常に望ましくありません。

私にとっての実例:数学ライブラリ。数学ライブラリにVector同じ方向の単位ベクトルの取得をサポートするクラスがある場合、大きさで除算できる必要があります。大きさが0の場合、ゼロ除算の状況になります。開発では、これらをキャッチしたいます。0をぶら下げて驚くべき除算を望んでいません。例外をスローすることは、このための非常に一般的なソリューションです。それはほとんど発生しません(本当に例外的な動作です)が、発生した場合は、それを知りたいと思います。

組み込みプラットフォームでは、これらの例外は必要ありません。チェックを行う方が理にかなっていますif (this->mag() == 0) return Vector(0, 0, 0);。実際、私は実際のコードでこれを見ています。

次に、ビジネスの観点から考えます。APIを使用する2つの異なる方法を教えることができます。

// Development version - exceptions        // Embeded version - return 0s
Vector right = forward.cross(up);          Vector right = forward.cross(up);
Vector localUp = right.cross(forward);     Vector localUp = right.cross(forward);
Vector forwardHat = forward.unit();        Vector forwardHat = forward.tryUnit();
Vector rightHat = right.unit();            Vector rightHat = right.tryUnit();
Vector localUpHat = localUp.unit()         Vector localUpHat = localUp.tryUnit();

これは、ここでの回答のほとんどの意見を満たしますが、会社の観点からは望ましくありません。組み込み開発者は1つのコーディングスタイルを学習する必要があり、開発開発者は別のスタイルを学習する必要があります。これはかなり厄介な場合があり、人々に1つの考え方を強いる。さらに悪い可能性:埋め込みバージョンに例外を投げるコードが含まれていないことをどのように証明しますか?投げるバージョンは簡単に溶け込み、高価なFailure Review Boardがそれを見つけるまでコードのどこかに隠れます。

一方、例外処理をオンまたはオフにするフラグがある場合は、両方のグループがまったく同じスタイルのコーディングを学習できます。実際、多くの場合、あるグループのコードを他のグループのプロジェクトで使用することさえできます。を使用したこのコードのunit()場合、0除算のケースで実際に何が起こるか気にするなら、テストを自分で書いておくべきです。したがって、組み込みシステムに移動すると、悪い結果が得られるだけで、その動作は「正気」です。今、ビジネスの観点から、私はコーダーを一度訓練し、彼らは私のビジネスのすべての部分で同じAPIと同じスタイルを使用します。

ほとんどの場合、他の人のアドバイスに従うtryGetUnit()必要があります。例外をスローしない関数に似たようなイディオムを使用し、代わりにnullなどのセンチネル値を返します。ユーザーが単一のプログラム内で例外処理と非例外処理コードの両方を並べて使用したいと思う場合は、tryGetUnit()表記法に従ってください。ただし、ビジネスの現実により旗を使用するほうが良い場合があります。あなたがその角の場合に自分自身を見つけた場合、道端で「ルール」を投げることを恐れてはいけません。それがルールの目的です!

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