Search code examples
c#cililspy

Why does 'return x switch' generates 'if (1 == 0)' in ILSpy?


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?


Solution

  • 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.