web-dev-qa-db-ja.com

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

この質問は、例外処理をサポートするすべてのOOプログラミング言語に適用することを目的としています。説明目的でのみC#を使用しています。

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

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

.NET 4の System.IObserver<T>.OnError method は、それを示唆しています。例外は、エラーオブジェクトとして渡されます。

別のシナリオである検証を見てみましょう。私が従来の知識に従っているため、エラーオブジェクトタイプ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つの異なるアプローチの長所と短所は何ですか?

45
stakx

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

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

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

もちろんです。これが適切な例の短いリストを次に示します。

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

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

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

ここで重要なのは、例外の真の価値がtry/catchパターンであり、例外オブジェクトのインスタンス化ではありません。例外オブジェクトは単なる状態メッセージです。

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

31
Reactgular

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

考えられる使用例の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);
     }
}

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

51
Philipp

概念的には、例外オブジェクトが操作の予期される結果である場合は、はい。ただし、私が考えることができるケースには、いつかスローキャッチが常に含まれます。

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

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

5
KeithS

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

次に、何か(そのタスクのコード、またはスレッドプールの実装のいずれか)がその例外をキャッチし、タスクと一緒に格納し、タスクが(正常に)終了したと考えます。これで、タスクが終了したかどうかを確認できます(タスクがスローされたかどうか、またはスレッドで再びスローできるかどうかを確認できます(または、原因として元の例外がある新しい例外))。

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

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

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

5
user470365

ほとんどの言語(afaik)では、例外にいくつかの追加の特典が付いています。ほとんどの場合、現在のスタックトレースのスナップショットはオブジェクトに格納されます。これはデバッグには適していますが、メモリに影響を与える可能性もあります。

@ThinkingMediaですでに述べたように、実際には例外をエラーオブジェクトとして使用しています。

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

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

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

それで、それは良い考えですか?

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

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

典型的なスロー/トライ/キャッチパターンでは、スタックトレースが必要になる場合があります。それ以外の場合は、それがどこから来たかの手掛かりがないためです。返されたオブジェクトの場合、常に呼び出された関数から取得されます。スタックトレースには、開発者が必要とするすべての情報が含まれている可能性がありますが、それほど重くなく、より具体的なもので十分な場合があります。

1
donquixote

それはあなたのデザインに依存します、通常、私は例外を呼び出し元に返さず、むしろスローしてキャッチし、そのままにします。通常、コードは早く失敗するように記述されています。たとえば、ファイルを開いて処理する場合を考えます(これは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
Jon Raynor

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
Tom Tom