为什么结构的字段在 using 语句后被重置?

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

我注意到 IDisposable 结构有一些奇怪的行为。 dispose 方法似乎是在字段设置为默认值的新实例上调用的。

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";
    }
}

我假设它发生是因为对象在 finally 块中被转换为 IDisposable,并且因为我在这里有一个值类型,所以创建了一个新实例。 我不明白的是为什么不将该字段复制到新实例? 当我装箱结构时,字段被复制:

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

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

对于值类型,变量 data 在 using 语句的开头被复制到另一个未命名的临时变量中。根据规范,此副本的行为就像装箱到 IDisposable 和调用的 Dispose(但请注意,C# 编译器实际上并不装箱该值,更多内容在 post 末尾)。这记录在 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();
    }
}

请注意,您的 using 语句不是声明,而只是一个表达式。该规范还涵盖了这一点:

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.

因此您对 data 的修改在 Dispose 中看不到,因为已经进行了复制。相对较新版本的 C# 编译器(VS 2019 附带)将针对这种情况发出警告。

Is the value actually boxed?

没有。尽管在规范中出现了强制转换,甚至对 C# 进行了一些反编译。编译器是允许的,实际上不会装箱该值。 Eric Lippert 的 article(也在评论中链接)包含一些关于此的附加详细信息。要了解实际情况,让我们看看 finally 中的 IL:

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

首先将未命名的临时文件加载回计算堆栈。这是前面提到的未修改的副本。接下来,魔法通过 constrained opcode 发生了。这是一个特殊的指令,它通知 JIT 正在直接对该类型进行调用,如果它是实现该方法的值类型,那么它不需要通过接口进行虚拟调用。

Eric 的文章提到了对 C# 规范的更新,澄清了装箱的省略,大概就是这一点:

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.