web-dev-qa-db-ja.com

「catch、when」で例外をキャッチする

特定の条件が満たされたときにキャッチハンドラーを実行できるC#のこの新しい機能に出会いました。

int i = 0;
try
{
    throw new ArgumentNullException(nameof(i));
}
catch (ArgumentNullException e)
when (i == 1)
{
    Console.WriteLine("Caught Argument Null Exception");
}

これがいつ役立つかを理解しようとしています。

1つのシナリオは次のようなものです。

try
{
    DatabaseUpdate()
}
catch (SQLException e)
when (driver == "MySQL")
{
    //MySQL specific error handling and wrapping up the exception
}
catch (SQLException e)
when (driver == "Oracle")
{
    //Oracle specific error handling and wrapping up of exception
}
..

しかし、これも同じハンドラー内で実行でき、ドライバーの種類に応じて異なるメソッドに委任できます。これにより、コードが理解しやすくなりますか?間違いなく。

私が考えることができる別のシナリオは次のようなものです:

try
{
    SomeOperation();
}
catch(SomeException e)
when (Condition == true)
{
    //some specific error handling that this layer can handle
}
catch (Exception e) //catchall
{
    throw;
}

繰り返しますが、これは私が好きなことができるものです:

try
{
    SomeOperation();
}
catch(SomeException e)
{
    if (condition == true)
    {
        //some specific error handling that this layer can handle
    }
    else
        throw;
}

'catch、when'機能を使用すると、ハンドラーがスキップされ、ハンドラー内の特定のユースケースの処理と比較してスタックの巻き戻しがはるかに早く発生するため、例外処理が高速化されますか?この機能に適した特定のユースケースはありますか?

82
MS Srikkanth

キャッチブロックを使用すると、例外のtypeでフィルタリングできます。

catch (SomeSpecificExceptionType e) {...}

when句を使用すると、このフィルターを汎用式に拡張できます。

したがって、when句は、例外のtypeが、例外をここで処理するか、または


一般的な使用例は、実際には複数の異なる種類のエラーに対するwrapperである例外タイプです。

私が実際に使用したケースは次のとおりです(VBでは、かなり長い間この機能を既に備えています)。

try
{
    SomeLegacyComOperation();
}
catch (COMException e) when (e.ErrorCode == 0x1234)
{
    // Handle the *specific* error I was expecting. 
}

SqlExceptionも同じです。これにはErrorCodeプロパティもあります。別の方法は次のようなものです。

try
{
    SomeLegacyComOperation();
}
catch (COMException e)
{
    if (e.ErrorCode == 0x1234)
    {
        // Handle error
    }
    else
    {
        throw;
    }
}

これは間違いなくエレガントではなく、 スタックトレースを少し中断します

さらに、同じtry-catch-blockで同じtypeの例外を2回言及できます。

try
{
    SomeLegacyComOperation();
}
catch (COMException e) when (e.ErrorCode == 0x1234)
{
    ...
}
catch (COMException e) when (e.ErrorCode == 0x5678)
{
    ...
}

when条件がなければ不可能です。

98
Heinzi

Roslyn's wiki (強調鉱山)から:

例外フィルターは、スタックを無傷のままにするため、キャッチして再スローするよりも望ましい方法です。例外によって後でスタックがダンプされる場合、最後に再スローされた場所ではなく、元のスタックの場所を確認できます。

また、副作用のために例外フィルターを使用することは、「乱用」の一般的で受け入れられている形式です。例えばロギング。彼らは、そのコースをインターセプトすることなく、「フライングバイ」の例外を検査できます。これらの場合、フィルターは多くの場合、副作用を実行するfalse-returningヘルパー関数の呼び出しになります。

private static bool Log(Exception e) { /* log it */ ; return false; }

… try { … } catch (Exception e) when (Log(e)) { }

最初のポイントは実証する価値があります。

static class Program
{
    static void Main(string[] args)
    {
        A(1);
    }

    private static void A(int i)
    {
        try
        {
            B(i + 1);
        }
        catch (Exception ex)
        {
            if (ex.Message != "!")
                Console.WriteLine(ex);
            else throw;
        }
    }

    private static void B(int i)
    {
        throw new Exception("!");
    }
}

例外がヒットするまでこれをWinDbgで実行し、!clrstack -i -aを使用してスタックを出力すると、Aのフレームだけが表示されます。

003eef10 00a7050d [DEFAULT] Void App.Program.A(I4)

PARAMETERS:
  + int i  = 1

LOCALS:
  + System.Exception ex @ 0x23e3178
  + (Error 0x80004005 retrieving local variable 'local_1')

ただし、プログラムを変更してwhenを使用する場合:

catch (Exception ex) when (ex.Message != "!")
{
    Console.WriteLine(ex);
}

スタックにはBのフレームも含まれていることがわかります。

001af2b4 01fb05aa [DEFAULT] Void App.Program.B(I4)

PARAMETERS:
  + int i  = 2

LOCALS: (none)

001af2c8 01fb04c1 [DEFAULT] Void App.Program.A(I4)

PARAMETERS:
  + int i  = 1

LOCALS:
  + System.Exception ex @ 0x2213178
  + (Error 0x80004005 retrieving local variable 'local_1')

その情報は、クラッシュダンプをデバッグするときに非常に役立ちます。

35
Eli Arbel

例外がスローされると、例外処理の最初のパスで、例外がキャッチされる場所が特定されますbeforeスタックの巻き戻し。 「キャッチ」ロケーションが識別されると、すべての「最終」ブロックが実行されます(例外が「最終」ブロックをエスケープすると、以前の例外の処理が中止される可能性があることに注意してください)。それが起こると、コードは「キャッチ」で実行を再開します。

「when」の一部として評価される関数内にブレークポイントがある場合、そのブレークポイントはスタックの巻き戻しが発生する前に実行を一時停止します。対照的に、「キャッチ」でのブレークポイントは、すべてのfinallyname__ハンドラーが実行された後にのみ実行を一時停止します。

最後に、fooname__の23行目と27行目がbarname__を呼び出し、23行目の呼び出しがfooname__内でキャッチされて57行目で再スローされた場合、スタックトレースは、57行目からbarname__を呼び出しているときに例外が発生したことを示唆します[場所の再スロー]、例外がline-23またはline-27呼び出しで発生したかどうかに関する情報を破棄します。そもそもwhenname__を使用して例外をキャッチしないようにすることで、このような障害を回避できます。

ところで、C#とVB.NETの両方で厄介な厄介な便利なパターンは、whenname__句内で関数呼び出しを使用して、finallyname__句内で使用できる変数を設定して、関数が正常に完了したかどうかを判断し、関数は、発生した例外を「解決」する見込みはありませんが、それに基づいてアクションを実行する必要があります。たとえば、リソースをカプセル化するオブジェクトを返すことになっているファクトリメソッド内で例外がスローされた場合、取得されたリソースはすべて解放する必要がありますが、基になる例外は呼び出し元まで浸透する必要があります。 (構文ではないが)意味的に処理する最もクリーンな方法は、例外が発生したかどうかをfinallyname__ブロックでチェックし、発生した場合は、返されないオブジェクトに代わって取得したすべてのリソースを解放することです。クリーンアップコードは、例外の原因となった条件を解決する見込みがないため、実際にはcatchname__ではなく、単に何が起こったかを知る必要があります。次のような関数を呼び出す:

bool CopySecondArgumentToFirstAndReturnFalse<T>(ref T first, T second)
{
  first = second;
  return false;
}

whenname__句内では、ファクトリ関数が何かが発生したことを知ることができます。

5
supercat