ThrowHelper 类

ThrowHelper 类是一种帮助程序类型,可用于有效引发异常。 它旨在支持 Guard API,它主要用于开发人员需要对引发的异常类型进行精细控制,或对要包含的确切异常消息进行精细控制的情况。

平台 API:ThrowHelperGuard

Syntax

ThrowHelper 类公开了一系列 ThrowException API,这些 API 提供 1:1 映射到所有可用异常类型的构造函数。 这些方法旨在取代代码中的经典 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。 如前所述,这种情况可能发生在库需要抛出 API 中 Guard 不存在的特定异常类型时,或者需要完全控制抛出异常时所包含的确切参数。

所有可用 API 也有泛型重载,这些 API 可用于需要特定类型的返回类型的表达式(例如 switch 表达式)。 这些重载永远不会返回任何值,但返回类型声明为允许 C# 编译器在表达式中接受这些 API 的使用,这些 API 不适用于返回 void方法。 请看下面的示例:

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

在这里,ThrowHelper 被用于一个需要返回类型为int的表达式中,因此可以使用ThrowArgumentException的泛型重载功能来做到这一点。 泛型重载还适用于三元运算符(x ? a : b)、null 合并运算符(x = a ?? b)和 null 合并赋值运算符()x ??= y等模式。

方法

ThrowHelper类中,有许多不同的方法和重载。 它们提供对以下异常的访问权限(并映射其所有公共构造函数):

可以从链接的文档查看每种异常类型的可用构造函数。相应的 ThrowHelper API 提供一系列与该指定类型的现有构造函数对应的重载。 每个 API 仅命名为“Throw”+ 异常类型。

技术详细信息

以下部分介绍在代码生成方面切换到 ThrowHelper API 对标准 throw new Exception 语句的影响。 如前所述,了解这些详细信息并不需要使用这些 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 APIs 的使用。 它会检查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

可以看到生成的程序集比以前小得多。 创建异常的代码已移出方法,这也意味着你可以重复使用 API 中的 ThrowHelper 相同代码,并使用不同的参数,而不是将异常引发内联到每个方法中。 此处只有两条指令将异常类型加载到 mov,然后跳转到 ThrowHelper.ThrowInvalidOperationException 方法。 JIT 编译器能够看到该方法始终抛出异常,因此永远不会内联该方法的调用。 它确保以最佳方式重写条件分支(具体而言,知道应该执行哪个分支,而不出错)。

示例

可以在 单元测试中找到更多示例。