Search code examples
c#methodsreturn-valuetry-finally

Why does returning a variable in a try block not change the value of the returned thing when this variable is reset in the finally section?


I don't really understand how the instructions flow in the following code. The body of finally is guaranteed to be executed before the method returns. If so, the return value should be 0 rather than 1.

Could you explain the internal mechanism why the return value is still 1 even though the finally has reset it to 0?

class Container
{
    int data = 0;
    public int Retrieve()
    {
        try
        {
            Inc();
            return data;
        }
        finally
        {
            Reset();
            //return data;
        }
    }
    void Reset()
    {
        data = 0;
        WriteLine("Reset");
    }
    void Inc() => data++;
}

class ReturnInTry
{
    static void Main()
    {
        Clear();
        WriteLine("Start");
        WriteLine(new Container().Retrieve());
        WriteLine("End");
    }
}

Solution

  • Because when the return instruction is executed, it PUSHs in the CPU Stack the value to be returned.

    Then the finally block is executed but it does not modify the value already pushed.

    Thus after the method PROC RET, the caller POPs the value and has what it has been PUSHed, but the data itself has been reseted.

    Therefore, calling the method again will return 0.

    This means that the return statement is executed first, and the code in finally is executed after, so the result is previously stored and changing data does not change this stored in the stack result.

    try-finally (C# Reference)

    We can check that using for example ILSpy:

    .method public hidebysig 
      instance int32 Retrieve () cil managed 
    {
      // Method begins at RVA 0x4cf4
      // Code size 30 (0x1e)
      .maxstack 1
      .locals init ( [0] int32 )
    
      .try
      {
        // Inc();
        IL_0002: ldarg.0
        IL_0003: call instance void ConsoleApp.Container::Inc()
    
        // return data;
        IL_0009: ldarg.0
        IL_000a: ldfld int32 ConsoleApp.Container::data
        IL_000f: stloc.0
    
        IL_0010: leave.s IL_001c
      } // end .try
      finally
      {
        // Reset();
        IL_0013: ldarg.0
        IL_0014: call instance void ConsoleApp.Container::Reset()
        // }
        IL_001b: endfinally
      } // end handler
    
      IL_001c: ldloc.0
      IL_001d: ret
    } // end of method Container::Retrieve
    

    OpCodes.Stloc_0 Field

    OpCodes.ldloc_0 Field

    // Console.WriteLine(new Container().Retrieve());
    IL_000c: newobj instance void ConsoleApp.Container::.ctor()
    IL_0011: call instance int32 ConsoleApp.Container::Retrieve()
    IL_0016: call void [mscorlib]System.Console::WriteLine(int32)
    

    OpCodes.Call Field