Visual Studio bleats about "Delegate invocation can be simplified" when you have statements such as this:
Action<int> foo = x;
if (foo != null)
foo(10);
The quick action smart tag rules want you to change this to:
Action<int> foo = x;
foo?.Invoke(10);
Does the compiler deal with this for you in a nice way and produce the same code either way? Or does the latter perform differently?
In build with optimization turned off (typically Debug builds) you will get the following two IL instruction sequences:
IL_0000: nop IL_0000: nop
IL_0001: ldnull IL_0001: ldnull
IL_0002: ldftn x IL_0002: ldftn x
IL_0008: newobj Action<int>..ctor IL_0008: newobj Action<int>..ctor
IL_000D: stloc.0 // foo IL_000D: stloc.0 // foo
IL_000E: ldloc.0 // foo IL_000E: ldloc.0 // foo
IL_000F: ldnull IL_000F: brtrue.s IL_0013
IL_0010: cgt.un IL_0011: br.s IL_001C
IL_0012: stloc.1
IL_0013: ldloc.1
IL_0014: brfalse.s IL_001F
IL_0016: ldloc.0 // foo IL_0013: ldloc.0 // foo
IL_0017: ldc.i4.s 0A IL_0014: ldc.i4.s 0A
IL_0019: callvirt Action<int>.Invoke IL_0016: callvirt Action<int>.Invoke
IL_001E: nop IL_001B: nop
IL_001F: ret IL_001C: ret
Slight differences here regarding the branch instructions, but let's build with optimizations turned on (typically Release builds):
IL_0000: ldnull IL_0000: ldnull
IL_0001: ldftn x IL_0001: ldftn x
IL_0007: newobj Action<int>..ctor IL_0007: newobj Action<int>..ctor
IL_000C: stloc.0 // foo IL_000C: dup
IL_000D: ldloc.0 // foo IL_000D: brtrue.s IL_0011
IL_000E: brfalse.s IL_0018 IL_000F: pop
IL_0010: ldloc.0 // foo IL_0010: ret
IL_0011: ldc.i4.s 0A IL_0011: ldc.i4.s 0A
IL_0013: callvirt Action<int>.Invoke IL_0013: callvirt Action<int>.Invoke
IL_0018: ret IL_0018: ret
Again, slight difference in the branch instructions. Specifically, the example using a null-coalescing operator will push a duplicate of the action delegate reference on the stack, whereas the one with the if-statement will use a temporary local variable. The JITter might put both into a register, however, this isn't conclusive that it will behave differently.
Let's try something different:
public static void Action1(Action<int> foo)
{
if (foo != null)
foo(10);
}
public static void Action2(Action<int> foo)
{
foo?.Invoke(10);
}
This gets compiled (again, with optimizations turned on) to:
IL_0000: ldarg.0 IL_0000: ldarg.0
IL_0001: brfalse.s IL_000B IL_0001: brfalse.s IL_000B
IL_0003: ldarg.0 IL_0003: ldarg.0
IL_0004: ldc.i4.s 0A IL_0004: ldc.i4.s 0A
IL_0006: callvirt Action<int>.Invoke IL_0006: callvirt Action<int>.Invoke
IL_000B: ret IL_000B: ret
The exact same code. So the differences in the above examples were different because of other things than the null-coalescing operator.
Now, to answer your specific question, will the branch sequence differences from your example impact performance? The only way to know this is to actually benchmark. However, I would be very surprised if it turned out to be something you need to take into account. Instead I would choose the style of code depending on what you find easiest to write, read, and understand.