当一个struct是一个struct时,using语句是什么时候用它的参数?

我对以下代码有一些疑问:

using System; namespace ConsoleApplication2 { public struct Disposable : IDisposable { public void Dispose() { } } class Program { static void Main(string[] args) { using (Test()) { } } static Disposable Test() { return new Disposable(); } } } 

我的问题是:

  • Disposable结构上运行的using语句是否会从Test()框返回结构?
  • 我怎样才能找到自己的答案?

为了试图找出自己,我检查了上面代码生成的IL,这里是Main(...)方法的IL:

 .method private hidebysig static void Main(string[] args) cil managed { .entrypoint .maxstack 1 .locals init ( [0] valuetype ConsoleApplication2.Disposable CS$3$0000) L_0000: call valuetype ConsoleApplication2.Disposable ConsoleApplication2.Program::Test() L_0005: stloc.0 L_0006: leave.s L_0016 L_0008: ldloca.s CS$3$0000 L_000a: constrained ConsoleApplication2.Disposable L_0010: callvirt instance void [mscorlib]System.IDisposable::Dispose() L_0015: endfinally L_0016: ret .try L_0006 to L_0008 finally handler L_0008 to L_0016 } 

我怀疑在那里调用虚方法,在L_0010会引入装箱操作,但实际的box指令不在这里。

我问的原因是,不久前,可能是1 – 2年,我在网上看到了某人评论的使用声明的“优化”。 这种情况是使用using语句作为对象的短时间锁定的语法,其中在方法中获取了锁,并且返回了一个结构,当处理掉时,会释放锁,这样的代码:

 using (LockTheObject()) { // use the object } 

并且注释是通过将LockTheObject方法的返回类型从IDisposable更改为使用的实际结构,避免了装箱。

但我想知道这是真的还是真的。

谁能指出我正确的方向? 如果,为了看到盒子操作,我将不得不检查运行时汇编代码,请给我看一个要查找的例子,我非常精通汇编代码,所以这不是问题,但没有跳出来当我看着那个时,在我身边。

看起来好像放在using语句中的任何值类型都不会被装箱。 这似乎是一个C#优化,因为当实现IDisposable的值类型在using语句中而不在任何其他上下文中时,才会省略装箱。

有关更多信息,请参阅使用声明和一次性值类型 :

不久之前,Ian Griffiths写了一篇关于他的TimedLock类的改进,其中他将它从一个类改为一个struct。 此更改导致实现IDisposable的值类型。 当我很快忘记的时候,脑子里有一个唠叨的问题。 问题是,在调用Dispose时,该类型的实例不会被装箱吗?

而且哦不! 不是TimedLock了! :

John Sands指出我在最近的博客中展示的代码中存在一个缺陷,即在不放弃C#的lock关键字的大部分便利性的情况下使用锁定超时。

这是一个副本如果我的结构实现了IDisposable,它会在using语句中使用时被装箱吗?

更新:这个问题是我2011年3月的博客主题 。 谢谢你这个好问题!

Andrew Hare的回答是正确的; 我只想添加一个有趣的额外注释。 我们发出的优化 – 使用受约束的callvirt在可能的情况下跳过装箱 – 实际上严格来说违反了C#规范。 规范声明我们为值类型资源生成的finally块是:

  finally { ((IDisposable)resource).Dispose(); } 

这显然是对值类型的装箱转换。 可以构建设计的场景,其中实现中缺少拳击是可见的。

(感谢Vladimir Reshetnikov向我指出这个规范违规行为。)

值类型的实例方法this参数作为其第一个参数,类似于引用类型上的实例方法。 但是,在这种情况下,参数是指向对象数据的托管指针,而不是对盒装对象的引用。 您可能会发现它在内存中的布局如下:

 Unboxed object: ----------------------------------------- | DATA | ----------------------------------------- ^ managed pointer to struct Boxed object: ------------------------------------------------------------ | GC/Object header | [Boxed] DATA | ------------------------------------------------------------ ^ The 'unbox' opcode gives a managed pointer to the boxed data ^ A *reference* to any instance of a reference type or boxed object, points here 

在这两种情况下, DATA都是相同的¹。

值类型上的实例方法期望特定于托管指针指向数据,因此不需要装箱对象。 如上所述,在调用之前使用constrained操作码。 它告诉运行时以下callvirt指令正在接收指向ConsoleApplication2.Disposable结构的托管指针,而不是它通常接收的对象引用。 在这样做时,JIT可以解析由struct实现的Dispose()的密封重载并直接调用它而不用装箱对象。 如果没有constrained前缀,传递给callvirt指令的对象必须是对象引用,因为标准虚拟调用动态解析过程基于GC / Object头始终位于预期位置的事实 – 是的,这会强制拳击值类型。

¹我们现在继续并忽略Nullable