我注意到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#编译器实际上并没有打包值,更多内容在帖子的末尾)。这在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语句,例如,出于性能原因,只要行为与上述扩展一致即可。