Search code examples
c#structidisposable

Why does a struct's field gets reset after the using statement?


I noticed some weird behavior with IDisposable structs. The dispose method seems to be called on a new instance with the fields set to default values.

public static class Example
{
    public static void Main()
    {
        var data = new MyStruct();

        using (data)
        {
            data.Foo = "some string";
            Console.WriteLine(data.Foo); //some string
        }

        Console.WriteLine(data.Foo); //some string
    }

}

public struct MyStruct : IDisposable
{
    public string Foo;


    public void Dispose()
    {
        Console.WriteLine(Foo);//null!
        Foo = "some string";
    }
}

I'm assuming it happens because the object is cast to IDisposable in the finally block, and since I have a value type here, a new instance is created. What I don't understand is why the field isn't copied to the new instance? When I box a struct the fields are copied:

    var s = new MyStruct();
    s.Foo = "1";

    var s2 = (MyStruct)(object)s;
    Console.WriteLine(s.Foo);//1
    Console.WriteLine(s2.Foo);//1

Solution

  • For value types, the variable data is copied into another unnamed temporary at the start of the using statement. According to the specification, this copy will behave as if boxed to IDisposable and Dispose called (note however the C# compiler does not actually box the value, more on this at the end of the post). This is documented in the C# specification:

    A using statement of the form

    using (ResourceType resource = expression) statement
    

    corresponds to one of three possible expansions. When ResourceType is a non-nullable value type, the expansion is

    {
        ResourceType resource = expression;
        try {
            statement;
        }
        finally {
            ((IDisposable)resource).Dispose();
        }
    }
    

    Note that your using statement is not a declaration, but simply an expression. The specification also covers this:

    A using statement of the form

    using (expression) statement
    

    has the same three possible expansions. In this case ResourceType is implicitly the compile-time type of the expression, if it has one. Otherwise the interface IDisposable itself is used as the ResourceType. The resource variable is inaccessible in, and invisible to, the embedded statement.

    So your modification of data is not seen inside Dispose because the copy has already been made. Relatively recent versions of the C# compiler (shipping with VS 2019) will issue a warning for this case.

    Is the value actually boxed?

    No. Despite the appearance of cast in the specification and even some decompilations to C#. The compiler is permitted and in fact does not box the value. Eric Lippert's article (linked in the comments as well) contains some addition details on this. To see what is actually going on, let's look at the IL in the finally:

    IL_0023: ldloca.s 1
    IL_0025: constrained. MyStruct
    IL_002b: callvirt instance void [mscorlib]System.IDisposable::Dispose()
    IL_0030: endfinally
    

    First the unnamed temporary is loaded back onto the evaluation stack. This is the unmodified copy mentioned previously. Next the magic happens via the constrained opcode. This is a special instruction that informs the JIT that a call is being made directly on the type and if it is a value type which implements the method, then it need not box to make a virtual call via the interface.

    Eric's article mentions an update to the C# specification clarifying the elision of boxing, which is probably this point:

    An implementation is permitted to implement a given using-statement differently, e.g. for performance reasons, as long as the behavior is consistent with the above expansion.