I used ILSpy on my code from curiosity, and I noticed that if (1 == 0)
empty instructions were added around return x switch
statements.
Here is an example of this behavior:
public static string TestReturnSwitch(string name)
{
return name switch
{
"General Kenobi" => "Hello there!",
"Inigo Montoya" => "My name is Inigo Montoya [...]",
"T800" => "Sarah Connor?",
_ => "???"
};
}
The above code in ILSpy is transformed to:
public static string TestReturnSwitch(string name)
{
if (1 == 0)
{
}
string result = name switch
{
"General Kenobi" => "Hello there!",
"Inigo Montoya" => "My name is Inigo Montoya [...]",
"T800" => "Sarah Connor?",
_ => "???",
};
if (1 == 0)
{
}
return result;
}
And part of the IL (for the first if (1 == 0)
):
// if (1 == 0)
IL_0001: ldc.i4.1
IL_0002: brtrue.s IL_0005
However, if I change the return switch
statement to a classic switch case
statement, the resulting code does not have the strange if (1 == 0)
. In fact, it becomes exactly the same code as the initial example.
It's the same behavior with ILSpy v.8.2 (which uses .NET 6.0) and ILSpy v.9.0 Preview 2 (which uses .NET 8.0).
Edit: this occurs only when compiled in Debug.
Why are these instructions added for return switch
but not for the classic switch case
?
Most likely for debugging purposes generated by the C# compiler.
Best view mode to use in such situations for ILSpy for me is IL with C#. I reproduced the generated IL code as:
IL_0000: nop
// if (1 == 0)
IL_0001: ldc.i4.1
IL_0002: brtrue.s IL_0005
// (no C# code)
IL_0004: nop
// string result = name switch
// {
// "General Kenobi" => "Hello there!",
// "Inigo Montoya" => "My name is Inigo Montoya [...]",
// "T800" => "Sarah Connor?",
// _ => "???",
// };
IL_0005: ldarg.0
IL_0006: ldstr "General Kenobi"
compiled in release mode, however:
// {
IL_0000: ldarg.0
// (no C# code)
IL_0001: ldstr "General Kenobi"
IL_0006: call bool [System.Runtime]System.String::op_Equality(string, string)
// return name switch
// {
// "General Kenobi" => "Hello there!",
// "Inigo Montoya" => "My name is Inigo Montoya [...]",
// "T800" => "Sarah Connor?",
// _ => "???",
// };
IL_000b: brtrue.s IL_0029
Just as you'd expect it to be.
The regular switch statement still produces "strange" code in debug compilation, just you didn't notice this in regular C# only view.
However, if I change the return switch statement to a classic switch case statement, the resulting code does not have the strange if (1 == 0). In fact, it becomes exactly the same code as the initial example.
It's not exactly the same:
IL_0000: nop
// return name switch
// {
// "General Kenobi" => "Hello there!",
// "Inigo Montoya" => "My name is Inigo Montoya [...]",
// "T800" => "Sarah Connor?",
// _ => "???",
// };
IL_0001: ldarg.0
IL_0002: stloc.1
// (no C# code)
IL_0003: ldloc.1
IL_0004: stloc.0
IL_0005: ldloc.0
IL_0006: ldstr "General Kenobi"
This translates roughly to:
var localCompilerHelper1 = name;
var localCompilerHelper0 = localCompilerHelper1 ;
switch(localCompilerHelper0 )...
We have no real need for these dummy local variables, we can just use the argument name
as we did with the other switch variant.
In both cases however, we start the real work at IL_0005
IL_0005: ldarg.0 // this is "name" argument
IL_0006: ldstr "General Kenobi"
IL_0005: ldloc.0 // this is a local variable
IL_0006: ldstr "General Kenobi"
So to your original question
Why are these instructions added for return switch but not for the classic switch case?
5 bytes of dummy IL are added for both switch types when compiled in Debug mode. Most likely to allow for setting a break point at the {
that marks the beginning of the method.
Why the compiler preferred one set of dummy IL over the other would be a different question not many people not on the compiler team can answer.