For details on the overload resolution process, see §12.6.4 of the specification. Right at the bottom of the general description of the process in §12.7.6.1, you can see:
Otherwise, an attempt is made to process E.I
as an extension method invocation (§12.7.8.3). If this fails, E.I
is an invalid member reference, and a binding-time error occurs.
If we take a look at §12.7.8.3:
if the normal processing of the invocation finds no applicable methods, an attempt is made to process the construct as an extension method invocation
This is pretty clear that an attempt is made to bind an extension method only if the overload resolution process fails to find an applicable instance method.
This is a deliberate decision. If this were not the case, adding a single using
statement to the top of a file could change how methods are bound further down in the file -- spooky action at a distance, which the spec generally tries to avoid.
However, since .NET Core 2.1, Enum.HasFlag
has been a JIT intrinsic (it was the poster-child for which the JIT intrinsics mechanism was introduced). This means that although the IL may say to box and call the Enum.HasFlag
method, in reality the JIT knows that it can replace this with a single bitwise test.
For example, the code:
public void Native(StringSplitOptions o) {
if (o.HasFlag(StringSplitOptions.RemoveEmptyEntries))
{
Console.WriteLine("Noo");
}
}
Is jitted to this assembly in Release:
C.Native(System.StringSplitOptions)
L0000: test dl, 1
L0003: je short L0017
L0005: mov rcx, 0x1ac4adebda0
L000f: mov rcx, [rcx]
L0012: jmp 0x00007ffb2f6ff7f8
L0017: ret
No sign of any method calls there (apart from the final Console.WriteLine
at the end)!
The same code using your extension method is significantly worse:
public void Worse(StringSplitOptions o) {
if (o.HasFlag<StringSplitOptions>(StringSplitOptions.RemoveEmptyEntries))
{
Console.WriteLine("Noo");
}
}
Gives:
C.Worse(System.StringSplitOptions)
L0000: sub rsp, 0x28
L0004: mov [rsp+0x24], edx
L0008: mov dword ptr [rsp+0x20], 1
L0010: mov ecx, [rsp+0x24]
L0014: and ecx, [rsp+0x20]
L0018: cmp ecx, [rsp+0x20]
L001c: sete cl
L001f: movzx ecx, cl
L0022: test ecx, ecx
L0024: je short L0038
L0026: mov rcx, 0x1ac4adebda0
L0030: mov rcx, [rcx]
L0033: call 0x00007ffb2f6ff7f8
L0038: nop
L0039: add rsp, 0x28
L003d: ret
We can see that this has inlined the content of your HasFlag
method, which is:
Extensions.HasFlag[[System.StringSplitOptions, System.Private.CoreLib]](System.StringSplitOptions, System.StringSplitOptions)
L0000: mov [rsp+8], ecx
L0004: mov [rsp+0x10], edx
L0008: mov eax, [rsp+8]
L000c: and eax, [rsp+0x10]
L0010: cmp eax, [rsp+0x10]
L0014: sete al
L0017: movzx eax, al
L001a: ret
Since your question is tagged [.net-6.0]
, the best advice is to throw away your extension method and use the built-in Enum.HasFlag
, as it's significantly faster than what you've written.
See this all on SharpLab.