为什么我的扩展方法重载不是首选?

Hel*_*ate 1 .net c# .net-6.0

我做了一个通用的重载来Enum.HasFlag防止拳击:

    public static unsafe bool HasFlag<T>(this T enumVal, T flag) where T : unmanaged, Enum {
        return sizeof(T) switch {
            1 => (*(byte*)&enumVal & *(byte*)&flag) == *(byte*)&flag,
            2 => (*(ushort*)&enumVal & *(ushort*)&flag) == *(ushort*)&flag,
            4 => (*(uint*)&enumVal & *(uint*)&flag) == *(uint*)&flag,
            8 => (*(ulong*)&enumVal & *(ulong*)&flag) == *(ulong*)&flag,
            _ => throw new ArgumentException("Unsupported base enum Type")
        };
    }
Run Code Online (Sandbox Code Playgroud)

然而编译器仍然想使用默认值Enum.HasFlag,我必须显式定义泛型类型以强制它使用扩展。

扩展方法在这里应该优先,因为与原始方法相比,参数具有正确的类型并且不需要隐式类型转换,那么为什么编译器仍然使用错误的类型呢?

can*_*on7 5

有关重载解析过程的详细信息,请参见规范的\xc2\xa712.6.4。在 \xc2\xa712.7.6.1 中该过程的一般描述的底部,您可以看到:

\n
\n

E.I否则,将尝试作为扩展方法调用进行处理(\xc2\xa712.7.8.3)。如果失败,E.I则成员引用无效,并且会发生绑定时错误。

\n
\n

如果我们看一下\xc2\xa712.7.8.3

\n
\n

如果调用的正常处理没有找到适用的方法,则会尝试将该构造作为扩展方法调用进行处理

\n
\n

很明显,仅当重载解析过程无法找到适用的实例方法时,才会尝试绑定扩展方法。

\n

这是一个深思熟虑的决定。如果不是这种情况,using在文件顶部添加一条语句可能会改变方法在文件中进一步绑定的方式——远距离的怪异动作,这是规范通常试图避免的。

\n
\n

然而,自 .NET Core 2.1 以来,Enum.HasFlag它一直是 JIT 内在函数(它是引入 JIT 内在函数机制的典型代表)。这意味着虽然 IL 可能会说装箱并调用该Enum.HasFlag方法,但实际上 JIT 知道它可以用单个按位测试来替换它。

\n

例如,代码:

\n
public void Native(StringSplitOptions o) {\n    if (o.HasFlag(StringSplitOptions.RemoveEmptyEntries))\n    {\n        Console.WriteLine("Noo");   \n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

在发布中被推送到此程序集:

\n
C.Native(System.StringSplitOptions)\n    L0000: test dl, 1\n    L0003: je short L0017\n    L0005: mov rcx, 0x1ac4adebda0\n    L000f: mov rcx, [rcx]\n    L0012: jmp 0x00007ffb2f6ff7f8\n    L0017: ret\n
Run Code Online (Sandbox Code Playgroud)\n

那里没有任何方法调用的迹象(除了最后的最后一个Console.WriteLine)!

\n

使用扩展方法的相同代码明显更糟:

\n
public void Worse(StringSplitOptions o) {\n    if (o.HasFlag<StringSplitOptions>(StringSplitOptions.RemoveEmptyEntries))\n    {\n        Console.WriteLine("Noo");   \n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

给出:

\n
C.Worse(System.StringSplitOptions)\n    L0000: sub rsp, 0x28\n    L0004: mov [rsp+0x24], edx\n    L0008: mov dword ptr [rsp+0x20], 1\n    L0010: mov ecx, [rsp+0x24]\n    L0014: and ecx, [rsp+0x20]\n    L0018: cmp ecx, [rsp+0x20]\n    L001c: sete cl\n    L001f: movzx ecx, cl\n    L0022: test ecx, ecx\n    L0024: je short L0038\n    L0026: mov rcx, 0x1ac4adebda0\n    L0030: mov rcx, [rcx]\n    L0033: call 0x00007ffb2f6ff7f8\n    L0038: nop\n    L0039: add rsp, 0x28\n    L003d: ret\n
Run Code Online (Sandbox Code Playgroud)\n

我们可以看到这已经内联了你的方法的内容HasFlag,即:

\n
Extensions.HasFlag[[System.StringSplitOptions, System.Private.CoreLib]](System.StringSplitOptions, System.StringSplitOptions)\n    L0000: mov [rsp+8], ecx\n    L0004: mov [rsp+0x10], edx\n    L0008: mov eax, [rsp+8]\n    L000c: and eax, [rsp+0x10]\n    L0010: cmp eax, [rsp+0x10]\n    L0014: sete al\n    L0017: movzx eax, al\n    L001a: ret\n
Run Code Online (Sandbox Code Playgroud)\n

由于您的问题被标记为[.net-6.0],最好的建议是放弃您的扩展方法并使用内置的Enum.HasFlag,因为它比您编写的要快得多。

\n

在 SharpLab 上查看这一切

\n