次の方法で共有


ThrowHelper クラス

ThrowHelper クラスは、例外を効率的にスローするために使用できるヘルパー型です。 これは Guard API をサポートすることを目的としており、主に開発者がスローされる例外の種類をきめ細かく制御する必要がある場合や、含める正確な例外メッセージを制御する必要がある場合に使用する必要があります。

プラットフォーム API:ThrowHelperGuard

構文

ThrowHelper クラスは、使用可能なすべての例外の種類のコンストラクターに 1 対 1 のマッピングを提供する一連のThrowException API を公開します。 これらのメソッドは、コード内の従来の throw new Exception(...) ステートメントの代わりに使用され、より小さく効率的なコードを作成するためのものです。 簡単に言えば、

// Replace this...
throw new InvalidOperationException("Some custom message from my library");

// ...with this
ThrowHelper.ThrowInvalidOperationException("Some custom message from my library");

この変更により、元の呼び出し元によって直接ではなく、このヘルパー メソッドによって例外がスローされるため、スタック トレースも若干異なります。 実際の結果は、例外を直接スローするのと実質的に同じです。

Guard API は、実行する必要がある多くの一般的なチェックに対して使いやすい抽象化を提供するため、最初に使用できるGuard API があるかどうかを確認し、そうでない場合はThrowHelperの使用にフォールバックすることをお勧めします。 前述のように、これは、ライブラリが Guard API に存在しない特定の例外の種類をスローする必要がある場合、またはスローされる例外に含まれる正確な引数を完全に制御する必要がある場合です。

また、使用可能なすべての API のジェネリック オーバーロードもあります。これは、特定の型の戻り値の型 (switch 式など) を必要とする式内で使用できます。 これらのオーバーロードは値を返すことはありませんが、戻り値の型は、C# コンパイラが、 voidを返すメソッドで動作しない式内でこれらの API の使用を受け入れるように宣言されています。 次の例を確認してください。

int result = text switch
{
    "cat" => 0,
    "dog" => 1,
    _ => ThrowHelper.ThrowArgumentException<int>(nameof(text))
};

ここでは、 ThrowHelperint型の戻り値の型を必要とする式内で使用されるため、 ThrowArgumentException のジェネリック オーバーロードを使用してこれを可能にすることができます。 ジェネリック オーバーロードは、三項演算子 (x ? a : b)、null 合体演算子 (x = a ?? b)、null 合体代入演算子 (x ??= y) などのパターンでも機能します。

メソッド

ThrowHelper クラスには、さまざまなメソッドとオーバーロードが多数あります。 次の例外へのアクセスを提供します (すべてのパブリック コンストラクターをマップします)。

リンクされたドキュメントから、これらの例外の種類ごとに使用可能なコンストラクターを確認できます。それぞれの ThrowHelper API は、指定した型の既存のコンストラクターに対応する一連のオーバーロードを提供します。 各 API には、単に "Throw" + 例外の種類という名前が付けられています。

技術的な詳細

次のセクションでは、codegen に関する標準のThrowHelper ステートメントに対するthrow new Exception API への切り替えの影響について説明します。 前述のように、これらの詳細を知ることはこれらの API を使用する必要はありませんが、ランタイムのしくみや実行時の JIT によるコードの処理方法を理解している開発者にとって、このセクションでは、 ThrowHelper パターンを使用するとコードがよりコンパクトで高速な結果となる理由をより明確に説明します。

まず、このような状況で JIT コンパイラが実際に生成するコードについて考えます。 この例では、.NET Core 3.1 x64 で生成されたコードを参照として使用しますが、他のランタイムやアーキテクチャにも同じ概念が適用されます。

この単純な型を考えてみましょう。

public struct ExceptionTest
{
    private int number;

    public int GetValueIfNotZeroOrThrow()
    {
        if (number == 0)
        {
            throw new InvalidOperationException("The value must be != 0");
        }

        return number;
    }
}

この型は、 ThrowHelper API の使用のみを目的としています。 number フィールドの値をチェックし、0 の場合はスローします。 生成されたアセンブリ コードを調べます。

ExceptionTest.GetValueIfNotZeroOrThrow()
    mov eax, [rcx]              ; read number
    test eax, eax               ; if (number != 0)
    je short L0011              ; jump to faulting path (if == 0)
    ret                         ; return (number is already in eax)
    mov rcx, 0x7ff95cbaca80     ; exception setup...
    call 0x00007ff9bc6178f0
    mov rsi, rax
    mov ecx, 1
    mov rdx, 0x7ff96590c080
    call 0x00007ff9bc7403e0
    mov rdx, rax
    mov rcx, rsi
    call System.InvalidOperationException..ctor(System.String)
    mov rcx, rsi
    call 0x00007ff9bc5db3a0
    int3 ; finally throw the exception

コードには、スローされる例外の作成専用の行が多数含まれていることがわかります。 これにより、命令キャッシュの効率が低下するため、これらのチェックの多くがある場合は常にコード サイズが大幅に増加する可能性があります。 一般に、メソッドのサイズをできるだけ小さくする必要があります。

ThrowHelper API を使用して書き換えた前のメソッドは、次のようになります。

public int GetValueIfNotZeroOrThrow()
{
    if (number == 0)
    {
        ThrowHelper.ThrowInvalidOperationException("The value must be != 0");
    }

    return number;
}

その結果、次のアセンブリが作成されます。

ExceptionTest.GetValueIfNotZeroOrThrow2()
    mov eax, [rcx]          ; load number
    test eax, eax           ; if (number != 0)
    je short L000f          ; faulting path (if == 0)
    ret                     ; return number (already in eax)
    mov rcx, 0x23435d85b98  ; load the exception message
    mov rcx, [rcx]
    call ThrowHelper.ThrowInvalidOperationException(System.String) ; ThrowHelper call!
    int3 ; return with fault

結果のアセンブリが以前よりもはるかに小さいことがわかります。 例外を作成するコードはメソッドから移動されます。つまり、すべてのメソッドで例外スローをインライン化する代わりに、異なる引数を使用して、 ThrowHelper API から同じコードを再利用することもできます。 ここでは、例外の種類でmovを読み込むためのstring命令が 2 つだけあり、ThrowHelper.ThrowInvalidOperationException メソッドにジャンプします。 JIT コンパイラは、そのメソッドが常にスローすることを確認できるため、その呼び出しはインライン化されません。 また、可能な限り最善の方法で条件付きブランチを書き直します (具体的には、どのブランチが実行されるはずなのかを、障害を発生させることなく把握します)。

例示

単体テストでは、その他の例を見つけることができます。