为什么在using语句之后会重置struct的字段?

问题描述 投票:1回答:1

我注意到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
c# struct idisposable
1个回答
4
投票

对于值类型,变量data将在using语句的开头复制到另一个未命名的临时文件中。根据规范,这个副本的行为就像盒装到IDisposable和Dispose一样(注意C#编译器实际上并没有打包值,更多内容在帖子的末尾)。这在C# specification中有记录:

表格的使用声明

using (ResourceType resource = expression) statement

对应于三种可能的扩展之一。当ResourceType是不可为空的值类型时,扩展为

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

请注意,您的using语句不是声明,而只是表达式。该规范还包括:

表格的使用声明

using (expression) statement

有三个可能的扩展。在这种情况下,ResourceType是隐式表达式的编译时类型(如果有的话)。否则,接口IDisposable本身将用作ResourceType。资源变量在嵌入语句中不可访问且不可访问。

因此,在Dispose中看不到你对data的修改,因为副本已经完成了。相对较新版本的C#编译器(随VS 2019一起提供)将针对此案例发出警告。

这个值实际上是盒装的吗?

没有。尽管在规范中出现了强制转换,甚至对C#进行了一些转换。允许编译器,实际上不会封装该值。 Eric Lippert的article(也在评论中链接)包含了一些额外的细节。要了解实际发生的情况,让我们看一下最后的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#规范的更新,澄清了拳击的缺失,这可能是这一点:

允许实现以不同方式实现给定的using语句,例如,出于性能原因,只要行为与上述扩展一致即可。

© www.soinside.com 2019 - 2024. All rights reserved.