例外オブジェクトをスローする代わりに返す正当な理由はありますか?


45

この質問は、例外処理をサポートするオブジェクト指向プログラミング言語に適用されることを意図しています。説明目的でのみC#を使用しています。

通常、例外は、コードがすぐに処理できない問題が発生したときに発生catchし、別の場所(通常は外部スタックフレーム)の句でキャッチされることを目的としています。

Q:例外がスローおよびキャッチされず、メソッドから単純に返され、エラーオブジェクトとして渡される正当な状況はありますか?

この質問は、.NET 4のSystem.IObserver<T>.OnErrorメソッドが次のことを示唆しているために思いつきました。例外はエラーオブジェクトとして渡されます。

別のシナリオ、検証を見てみましょう。私が従来の知恵に従っているとしましょう。したがって、エラーオブジェクトタイプIValidationErrorと、ValidationException予期しないエラーの報告に使用される別の例外タイプを区別しています。

partial interface IValidationError { }

abstract partial class ValidationException : System.Exception
{
    public abstract IValidationError[] ValidationErrors { get; }
}

System.Component.DataAnnotations名前空間は非常に似たようなことをします。)

これらのタイプは次のように使用できます。

partial interface IFoo { }  // an immutable type

partial interface IFooBuilder  // mutable counterpart to prepare instances of above type
{
    bool IsValid(out IValidationError[] validationErrors);  // true if no validation error occurs
    IFoo Build();  // throws ValidationException if !IsValid(…)
}

今、私はこれを上記に単純化できないのではないかと思っています。

partial class ValidationError : System.Exception { }  // = IValidationError + ValidationException

partial interface IFoo { }  // (unchanged)

partial interface IFooBuilder
{
    bool IsValid(out ValidationError[] validationErrors);
    IFoo Build();  // may throw ValidationError or sth. like AggregateException<ValidationError>
}

Q:これら2つの異なるアプローチの利点と欠点は何ですか?


2つの個別の回答を探している場合は、2つの個別の質問をする必要があります。それらを組み合わせると、答えるのがずっと難しくなります。
ボブソン

2
@Bobson:私はこれら2つの質問を補完するものと考えています:前者の質問は問題の一般的な文脈を広義に定義することになっていますが、後者は読者が自分の結論を引き出すことができる特定の情報を求めます。答えは、両方の質問に個別の明示的な答えを与える必要はありません。「中間」の回答も歓迎します。
stakx

5
私は何かについてはっきりしていません:あなたがそれを投げるつもりがない場合、ExceptionからValidationErrorを導出する理由は何ですか?
pdr

@pdr:AggregateException代わりに投げるという意味ですか?いい視点ね。
stakx

3
あんまり。つまり、スローする場合は例外を使用するということです。返す場合は、エラーオブジェクトを使用します。本来エラーである例外を予期している場合は、それらをキャッチしてエラーオブジェクトに入れます。どちらかが複数のエラーを許可するかどうかはまったく関係ありません。
pdr

回答:


31

例外がスローされてキャッチされず、メソッドから単に返され、エラーオブジェクトとして渡されるだけの正当な状況はありますか?

スローされない場合、例外ではありません。derived例外クラスからのオブジェクトですが、動作に従いません。この時点で例外と呼ぶのは純粋にセマンティクスですが、スローしないことに関して何も問題はありません。私の観点からは、例外をスローしないということは、内部関数が例外をキャッチして伝播しないようにすることとまったく同じです。

関数が例外オブジェクトを返すことは有効ですか?

絶対に。これが適切な例の短いリストを次に示します。

  • 例外ファクトリー。
  • すぐに使用できる例外として以前のエラーがあったかどうかを報告するコンテキストオブジェクト。
  • 以前にキャッチされた例外を保持する関数。
  • 内部型の例外を作成するサードパーティAPI。

それを悪く投げていませんか?

例外を投げるのは、「刑務所に行きます。Goを渡さないでください!」のようなものです。ボードゲームのモノポリー。ソースコードを実行せずに、すべてのソースコードをcatchまでジャンプするようコンパイラーに指示します。エラー、バグの報告または停止とは関係ありません。例外をスローすることは、関数の「スーパーリターン」ステートメントと考えるのが好きです。コードの実行を元の呼び出し元よりはるかに高いところに戻します。

ここで重要なことは、例外の真の値はtry/catchパターンにあり、例外オブジェクトのインスタンス化ではないことを理解することです。例外オブジェクトは単なる状態メッセージです。

あなたの質問では、これら2つのことの使用法を混同しているように見えます。例外のハンドラへのジャンプと、例外が表すエラー状態です。エラー状態を取得して例外にラップしたからといって、try/catchパターンまたはその利点に従っているわけではありません。


4
「スローされない場合、例外ではありExceptionません。クラスから派生したオブジェクトですが、動作に従いません。」-それがまさに問題です。Exceptionオブジェクトのプロデューサーやメソッドの呼び出しによって、オブジェクトがまったくスローされない状況で、派生オブジェクトを使用することは合法ですか。「スーパーリターン」制御フローなしで、「状態メッセージ」としてのみ機能しますか?または、私はこれを絶対に行わず、代わりに別の非タイプを使用するように、暗黙のtry/ catch(Exception)契約にひどく違反していExceptionますか?
stakx

10
@stakxプログラマーは、他のプログラマーを混乱させることはありますが、技術的には正しくありません。それがセマンティクスだと言った理由です。法的な答えはNo、契約を破らないということです。それpopular opinionはあなたがそうすべきではないということでしょう。プログラミングには、ソースコードが何も悪いことはしないが、誰もが嫌いな例がたくさんあります。ソースコードは、意図が何であるかが明確になるように、常に作成する必要があります。そのように例外を使用することで、他のプログラマを本当に助けていますか?はいの場合、そうします。そうでなければ、それが嫌われていても驚かないでください。
Reactgular

斜体のインラインコードブロックの@cgTagご利用には、私の脳の傷を作る...
Frayt

51

例外をスローする代わりに返すことは、状況を分析し、呼び出し元によってスローされる適切な例外を返すヘルパーメソッドがある場合、意味的に意味があります(これを「例外ファクトリ」と呼ぶことができます)。このエラーアナライザー関数で例外をスローすることは、分析自体の間に何か問題が発生したことを意味し、例外を返すことは、エラーの種類が正常に分析されたことを意味します。

1つの可能なユースケースは、HTTP応答コードを例外に変換する関数です。

Exception analyzeHttpError(int errorCode) {
    if (errorCode < 400) {
         throw new NotAnErrorException();
    }
    switch (errorCode) {
        case 403:
             return new ForbiddenException();
        case 404:
             return new NotFoundException();
        case 500:
             return new InternalServerErrorException();
        …
        default:
             throw new UnknownHttpErrorCodeException(errorCode);
     }
}

例外をスローすると、メソッドが誤って使用されたか、内部エラーが発生したことを意味し、例外を返すと、エラーコードが正常に識別されたことを意味します。


これは、コンパイラがコードフローを理解するのにも役立ちます。メソッドが正しく返されない場合(「望ましい」例外をスローするため)、コンパイラがそれを知らない場合はreturn、コンパイラに文句を言わないようにするために余分なステートメントが必要になることがあります。
ヨアヒムザウアー

@JoachimSauerうん。私は彼らがC#に "never"の戻り値型を追加することを望んでいます-それは関数が投げることによって終了しなければならないことを示し、その中に戻り値を入れることはエラーです。例外をきれいにするだけの仕事をしているコードがある場合があります。
ローレンペクテル

2
@LorenPechtel戻らない関数は関数ではありません。それはターミネーターです。そのような関数を呼び出すと、呼び出しスタックがクリアされます。を呼び出すことに似ていSystem.Exit()ます。関数呼び出し後のコードは実行されません。それは関数の良いデザインパターンのようには聞こえません。
-Reactgular

5
@MathewFoscarini同様の方法でエラーが発生する可能性のある複数のメソッドを持つクラスを検討してください。例外の一部としていくつかの状態情報をダンプして、ブームになったときに何を噛んでいたかを示したいとします。DRYのために、整形コードのコピーが1つ必要です。それは、整形を行う関数を呼び出してスローで結果を使用するか、きれいなテキストを構築してスローする関数を意味します。私は一般的に後者の方が優れていると思います。
ローレンペクテル

1
@LorenPechtelああ、ええ、私もそうしました。あぁ、了解。
-Reactgular

5

概念的には、例外オブジェクトが操作の期待される結果である場合、はい。しかし、私が考えることができるケースは、常にある時点でスローキャッチを伴う:

  • 「Try」パターンのバリエーション(メソッドは2番目の例外スローメソッドをカプセル化しますが、例外をキャッチし、代わりに成功を示すブール値を返します)。ブール値を返す代わりに、スローされた例外(成功した場合はnull)を返すことで、エンドユーザーはキャッチなしの成功または失敗の表示を保持しながら、より多くの情報を取得できます。

  • ワークフローエラー処理。実際には「パターンを試す」メソッドのカプセル化と同様に、コマンドのチェーンパターンで抽象化されたワークフローステップがある場合があります。チェーン内のリンクが例外をスローする場合、抽象化オブジェクトでその例外をキャッチしてから、その操作の結果としてそれを返すことが多くの場合、ワークフローエンジンとワークフローステップとして実行される実際のコードが大規模な試行を必要としません-独自のロジックをキャッチ。


「トライ」パターンのこのようなバリエーションを提唱する著名人はいないと思いますが、ブール値を返す「推奨」アプローチはかなり貧弱に思えます。プロパティ「成功」、メソッド、およびメソッド(null以外の場合は例外をスローする)を備えた抽象OperationResultクラスを用意する方が良いと思います。持つことは、プロパティがずっと言って便利がなかった場合の静的不変のエラーオブジェクトの使用を可能にするのではなくメソッドです。boolGetExceptionIfAnyAssertSuccessfulGetExceptionIfAnyGetExceptionIfAny
supercat 14

5

スレッドプールで実行されるタスクをエンキューしたとしましょう。このタスクが例外をスローする場合、それは別のスレッドであるため、キャッチしません。実行されたスレッドはただ死にます。

ここで、何か(そのタスクのコード、またはスレッドプールの実装)がその例外をキャッチし、タスクとともにそれを保存し、タスクが(不成功に)終了したと見なすことを考えてください。これで、タスクが終了したかどうかを確認できます(そして、タスクがスローされたかどうか、またはスレッドで再度スローできるかどうかを確認できます(または、より良い、オリジナルを原因とする新しい例外)。

手動で行うと、新しい例外を作成し、スローしてキャッチし、それを別のスレッドに格納して、スロー、キャッチ、および反応を取得していることに気付くことができます。したがって、throwとcatchをスキップして、単に保存してタスクを終了し、例外があるかどうかを尋ねて、それに反応するのは理にかなっています。ただし、同じ場所で実際に例外がスローされる可能性がある場合、これはより複雑なコードにつながる可能性があります。

PS:これは、例外の作成時にスタックトレース情報が作成されるJavaの経験で書かれています(スロー時に作成されるC#とは異なります)。そのため、Javaのスローされない例外アプローチは、C#の場合よりも遅くなります(事前に作成されて再利用されない限り)が、スタックトレース情報を利用できます。

一般に、例外を作成せず、例外をスローすることはありません(プロファイリング後のパフォーマンスの最適化がボトルネックとして指摘しない限り)。少なくともJavaでは、例外の作成に費用がかかります(スタックトレース)。C#では可能性はありますが、IMOは驚くべきものであるため、避ける必要があります。


これは、多くの場合、エラーリスナーオブジェクトでも発生します。キューはタスクを実行し、例外をキャッチし、その例外をエラーリスナに渡します。この場合、ジョブについてあまり知らないタスクスレッドを強制終了することは通常意味がありません。アイデアのこの種は、一つのスレッドが別の例外を渡されることができますのJava APIに組み込まれています: docs.oracle.com/javase/1.5.0/docs/api/java/lang/...
ランスNanekに

1

それはあなたのデザインに依存します。通常、例外を呼び出し元に返さず、例外をスローしてキャッチし、そのままにします。通常、コードは早く失敗するように書かれています。たとえば、ファイルを開いて処理する場合を考えます(これはC#PsuedoCodeです)。

        private static void ProcessFileFailFast()
        {
            try
            {
                using (var file = new System.IO.StreamReader("c:\\test.txt"))
                {
                    string line;
                    while ((line = file.ReadLine()) != null)
                    {
                        ProcessLine(line);
                    }
                }
            }
            catch (Exception ex) 
            {
                LogException(ex);
            }
        }

        private static void ProcessLine(string line)
        {
            //TODO:  Process Line
        }

        private static void LogException(Exception ex)
        {
            //TODO:  Log Exception
        }

この場合、エラーが発生し、不良レコードが検出されるとすぐにファイルの処理を停止します。

ただし、1行または複数行にエラーがある場合でも、ファイルの処理を継続したいという要件があるとします。コードは次のようになり始めます。

    private static void ProcessFileFailAndContinue()
    {
        try
        {
            using (var file = new System.IO.StreamReader("c:\\test.txt"))
            {
                string line;
                while ((line = file.ReadLine()) != null)
                {
                    Exception ex = ProcessLineReturnException(line);
                    if (ex != null)
                    {
                        _Errors.Add(ex);
                    }
                }
            }

            //Do something with _Errors Here
        }
        //Catch errors specifically around opening the file
        catch (System.IO.FileNotFoundException fnfe) 
        { 
            LogException(fnfe);
        }    
    }

    private static Exception ProcessLineReturnException(string line)
    {
        try
        {
            //TODO:  Process Line
        }
        catch (Exception ex)
        {
            LogException(ex);
            return ex;
        }

        return null;
    }

したがって、この場合は例外を呼び出し元に返しますが、例外は返されず、例外がキャッチされて既に処理されているため、何らかのエラーオブジェクトが返されます。これは例外を返すことで害はありませんが、他の呼び出し元は例外を再スローできますが、例外オブジェクトにはそのような動作があるため、望ましくない場合があります。呼び出し元が例外を返すことができるようにする場合は、例外を返します。それ以外の場合は、例外オブジェクトから情報を取り出して、より小さな軽量オブジェクトを作成し、代わりに返します。フェイルファーストは通常​​、よりクリーンなコードです。

検証の例では、検証エラーは非常に一般的である可能性があるため、例外クラスから継承したり例外をスローしたりすることはおそらくないでしょう。ユーザーの50%が最初の試行でフォームに正しく入力できなかった場合、例外をスローするよりもオブジェクトを返すほうがオーバーヘッドが少なくなります。


1

Q:例外がスローおよびキャッチされず、メソッドから単純に返され、エラーオブジェクトとして渡される正当な状況はありますか?

はい。たとえば、最近、ASMX Webサービスでスローされた例外に、結果のSOAPメッセージに要素が含まれていないという状況に遭遇したため、生成する必要がありました。

説明コード:

Public Sub SomeWebMethod()
    Try
        ...
    Catch ex As Exception
        Dim soapex As SoapException = Me.GenerateSoapFault(ex)
        Throw soapex
    End Try
End Sub

Private Function GenerateSoapFault(ex As Exception) As SoapException
    Dim document As XmlDocument = New XmlDocument()
    Dim faultDetails As XmlNode = document.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace)
    faultDetails.InnerText = ex.Message
    Dim exceptionType As String = ex.GetType().ToString()
    Dim soapex As SoapException = New SoapException("SoapException", SoapException.ClientFaultCode, Context.Request.Url.ToString, faultDetails)
    Return soapex
End Function

1

ほとんどの言語(afaik)では、例外にはいくつかの追加の利点があります。ほとんどの場合、現在のスタックトレースのスナップショットがオブジェクトに保存されます。これはデバッグには適していますが、メモリへの影響もあります。

@ThinkingMediaが既に述べたように、あなたは本当に例外をエラーオブジェクトとして使用しています。

コード例では、主にコードの再利用と構成の回避のためにこれを行っているようです。個人的には、これがこれを行う正当な理由だとは思いません。

もう1つの考えられる理由は、スタックトレースでエラーオブジェクトを提供するように言語をだますことです。これにより、エラー処理コードに追加のコンテキスト情報が提供されます。

一方、スタックトレースを保持することにはメモリコストがあると想定できます。たとえば、これらのオブジェクトをどこかで収集し始めると、メモリに不快な影響を与える可能性があります。もちろん、これはそれぞれの言語/エンジンでの例外スタックトレースの実装方法に大きく依存します。

だから、それは良いアイデアですか?

これまでのところ、使用されたり推奨されたりするのを見たことはありません。しかし、これはあまり意味がありません。

問題は、スタックトレースがエラー処理に本当に役立つかどうかです。またはより一般的に、開発者または管理者にどの情報を提供したいですか?

一般的なthrow / try / catchパターンでは、スタックトレースが必要になります。そうでない場合、どこから来たのかわからないからです。返されるオブジェクトの場合、常に呼び出された関数から取得されます。スタックトレースには、開発者が必要とするすべての情報が含まれている場合がありますが、より軽くて具体的なもので十分かもしれません。


-4

答えはイエスです。http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptionsを参照し、例外に関するGoogleのC ++スタイルガイドの矢印を開きます。そこに彼らが反対を決定するために使用した引数を見ることができますが、彼らが再びそれをやり直さなければならなかった場合、彼らはおそらく決定したでしょうと言う。

ただし、Go言語でも同様の理由で、例外を慣用的に使用しないことに注意してください。


1
申し訳ありませんが、これらのガイドラインが質問への回答にどのように役立つかはわかりません。例外処理を完全に回避することを推奨しているだけです。これは私が尋ねていることではありません。私の質問は、結果の例外オブジェクトがスローされない状況でエラー条件を記述するために例外タイプを使用することが合法かどうかです。これらのガイドラインには、それについて何も記載されていないようです。
stakx
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.