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

时间:2009-08-25 19:55:12

标签: c# struct idisposable using boxing

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

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更改为使用的实际结构,避免了装箱。

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

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

3 个答案:

答案 0 :(得分:6)

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

有关详细信息,请参阅The Using Statement And Disposable Value Types

  

不久前Ian Griffiths写了一篇文章   他的TimedLock课程的改进   他把它从一个班级变成了一个班级   结构。这一变化导致了一个   实现的值类型   IDisposable接口。我有一个唠叨的问题   在我脑海里当时   我很快忘记了。该   问题不是那种情况   调用Dispose时输入的类型是什么?

还有Oh No! Not the TimedLock Again!

  约翰·桑兹指出了一个缺陷   我在最近的博客中展示的代码   在没有锁的情况下使用超时   放弃了大部分的便利   C#的lock关键字。

答案 1 :(得分:4)

这是If my struct implements IDisposable will it be boxed when used in a using statement?

的副本

更新:这个问题是the subject of my blog in March of 2011。谢谢你提出的好问题!

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

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

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

(感谢Vladimir Reshetnikov指出这违反规范的行为。)

答案 2 :(得分:3)

值类型的实例方法将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可以解析由结构实现的Dispose()的密封重载,并直接调用它而不用装箱对象。如果没有constrained前缀,则传递给callvirt指令的对象必须是对象引用,因为标准虚拟调用动态解析过程基于GC / Object头是总是在预期的位置 - 是的,这会强制拳击值类型。

¹我们现在继续忽略Nullable<T>