如果我的结构体实现了IDisposable接口,在使用using语句时它会被装箱吗?

38

如果我的结构体实现了IDisposable接口,那么在使用using语句时它会被装箱吗?

谢谢

编辑:这个TimeLock是一个结构体并实现了IDisposable接口。 http://www.interact-sw.co.uk/iangblog/2004/04/26/yetmoretimedlocking

编辑2:根据IL指令查看,如果你的结构体公开了Dispose()方法,当结构体的实例超出范围时编译器会自动调用Dispose()方法,如果你忘记调用Dispose()方法(例如,没有使用“using”语句)。


7
为什么你有一个实现IDisposable接口的结构体? - Yuriy Faktorovich
5
听起来非常危险。由于它的赋值语义是复制,很难确定你有多少个结构体的副本。 - Jonathan Allen
2
@Jonathan Allen - 在 using 语句的上下文中,结构体的副本数量并不重要。重要的是,由结构体指向的非托管资源必须得到适当的处理和释放。在赋值时进行复制的语义中,跟踪该引用应该不难。 - Jeffrey L Whitledge
1
我担心的是有人会滥用你的结构体,导致资源被重复释放。根据这个资源的类型,可能会出现从异常到完全的内存损坏等各种情况。 - Jonathan Allen
1
我想不出在结构体中实现IDisposable的好理由。为什么不使用类呢? - Ed S.
4个回答

39

根据Eric Lippert的说法:

对结构体调用IDisposable.Dispose会生成一个受限虚拟调用,大多数情况下不会将值装箱。

对值类型进行受限虚拟调用只有在该虚拟方法未被该类型实现时才会将值装箱。唯一可能出现值类型未实现虚拟方法的情况是当该方法由基类System.ValueType实现,例如ToString。

有关详细信息,请参见CLI文档的第III部分的第2.1节。


但是 using 不仅仅调用 Dispose(),它还将其作为接口引用存储。 - H H
@Henk Holterman,你确定这是一个接口引用吗?当你说using (SQLConnection = new SQLConnection())时,我们已经声明了完整的类型。里面没有接口,也没有理由认为using会添加一个接口。 - Samuel Neff
Sam,看一下官方编译器的重写方式:using(x) {...} -> try {...} finally { if (x != null) x.Dispose(); } - H H
1
@Henk Holterman - 你提供的扩展仅适用于引用类型。值类型有单独的扩展。 - Jeffrey L Whitledge
13
我已经创建了一个测试应用程序来查看会发生什么,然后在ildasm中查看了它。没有装箱操作。正如这个回答所指出的那样,情况确实是这样的。 - Jeffrey L Whitledge
@Henk Holterman,在您的编译器重写中,何时更改声明以使用“IDisposable”?它没有。 - Samuel Neff

30

这是“当使用语句为结构体时,何时对其进行装箱?”问题的副本。

更新:此问题在2011年3月成为我的博客主题,感谢这个好问题。

以下是一些要点:

  • 正如其他人正确指出的那样,实现IDisposable的值类型在控制离开using语句时被处理成可释放资源时不会被装箱。
  • 从技术上讲,这是违反C#规范的。规范说明finally块应该具有((IDisposable)resource).Dispose(); 语义,其中显然包括了装箱转换。但我们实际上并不生成装箱转换。由于大多数情况下这正是您所希望的,因此我们不会为此而失眠。
  • 可释放的值类型似乎是一个潜在的坏主意。它太容易意外地创建值类型的副本;毕竟,它们按值复制。
  • 你到底为什么关心这个是否进行了装箱?我希望你不是因为想让dispose方法改变包含值类型的变量而问这个问题。那将是一个非常糟糕的主意。可变值类型是邪恶的。

22
你不想把它打包的最明显原因是出于性能考虑(例如,你想在C#中模拟低成本的C++ RAII)。 - Qwertie
“由于大多数情况下这正是您想要的。” — 除了略有不同的内存使用特性外,我想不出还有什么可观察到的差异,它在什么情况下 是我想要的呢? - Timwi
@Timwi 如果这是C#,你可能想调用一个私有接口实现而不是同名的公共非接口方法。当然,IL实际上确实调用了接口方法,但没有C#所需的装箱。另一个可观察到的区别是Dispose很可能会改变内部状态 - 如果你想观察到这种变化,那么你需要访问已释放的对象,而不是其副本的源。 - Eamon Nerbonne
我认为如果结构体是只读的,并且正在引用类型上进行处理,那么它应该是安全的。如果结构体显式实现Dispose,则会被装箱。 - M.kazem Akhgary
7
我正在运用 using(new SomeScopedBehavior()) 的技巧,以确保我的 End 方法在每个 Begin 方法中被调用,即使在有多个返回或者出现异常情况下也能如此。同时我也在使用Unity,所以我试图避免为了这一个调用而每秒钟创建3k+的堆垃圾。你认为我应该使用 try { ... } finally { ... } 以增加可读性,还是说在这里使用带结构体的 using 技巧更好?我 IDisposable 对象仅包装了静态方法,所以它对我来说没有任何额外开销。 - Merlyn Morgan-Graham
@MerlynMorgan-Graham 我也是因为Unity才发现了这个问题。似乎using(new SomeScopedBehavior())是一个很好的惯用法。Unity的开发人员在他们的引擎中也是这样做的,例如在检查器绘制循环中使用了一个名为EditorGUI.DisabledScope的结构体,在其中绘制禁用控件。 - dmitry1100

19

不会发生装箱。

using 不是一个方法调用,它只是编译器将其转换为大致如下的语法糖:

MyClass m = new MyClass()
try
{
    // ...
}
finally
{
    if (m != null) {
        m.Dispose();
    }
}

它在声明中从不使用 IDisposable,也从不将实例传递给其他任何东西。对于结构体,编译器实际上生成的内容更小:

MyStruct m = new MyStruct()
try
{
    // ...
}
finally
{
    m.Dispose();
}

由于结构体不能为null。

现在,为了确保它永远不会装箱,请查看IL。

尝试这个示例代码:

class StructBox
{
    public static void Test()
    {
        using(MyStruct m = new MyStruct())
        {

        }


        MyStruct m2 = new MyStruct();
        DisposeSomething(m2);
    }

    public static void DisposeSomething(IDisposable disposable)
    {
        if (disposable != null)
        {
            disposable.Dispose();
        }
    }

    private struct MyStruct : IDisposable
    {           
        public void Dispose()
        {
            // just kidding
        }
    }
}

然后查看IL:

.method public hidebysig static void Test() cil managed
{
    .maxstack 1
    .locals init (
        [0] valuetype ConsoleApplication1.StructBox/MyStruct m,
        [1] valuetype ConsoleApplication1.StructBox/MyStruct m2)
    L_0000: ldloca.s m
    L_0002: initobj ConsoleApplication1.StructBox/MyStruct
    L_0008: leave.s L_0018
    L_000a: ldloca.s m
    L_000c: constrained ConsoleApplication1.StructBox/MyStruct
    L_0012: callvirt instance void [mscorlib]System.IDisposable::Dispose()
    L_0017: endfinally 
    L_0018: ldloca.s m2
    L_001a: initobj ConsoleApplication1.StructBox/MyStruct
    L_0020: ldloc.1 
    L_0021: box ConsoleApplication1.StructBox/MyStruct
    L_0026: call void ConsoleApplication1.StructBox::DisposeSomething(class [mscorlib]System.IDisposable)
    L_002b: ret 
    .try L_0008 to L_000a finally handler L_000a to L_0018
}

代码行 L_0000 到 L_0017 代表了 m 的声明和 using。这里没有装箱。

代码行 L_0018 到 L_0026 代表了 m2 的声明和对 DisposeSomething 的调用。请参见第 L_0021 行的 box


1
哇——你知道你比我快了一秒钟吗?打字快确实有好处! - JMarsch

9
这将不会执行装箱操作(我很惊讶)。我认为bnkdev的解释已经包含了这一点。以下是我的证明过程:
编写了下面的快捷控制台应用程序(请注意,我包括了BoxTest()函数,我知道会执行装箱操作,以便我可以进行比较)。
然后我使用Reflector来反汇编编译输出为IL代码(您也可以使用ILDASM)。
请注意BoxTest的IL - 第L_000a行上的box指令(星号强调我的部分)
现在看看Main(我们使用IDisposable结构的using语句)
请注意第 L_000f 行的 constrained 关键字。我找不到确切含义的参考资料,但如果您阅读 bnkdev 的帖子,我认为这是他所描述的受限虚拟调用。

我找到了“constrained”关键字的定义。绝对不会有装箱 - 就像bnkdev所说的那样:http://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.constrained.aspx - JMarsch
+1 很棒!那绝对是确保的一种方式。 - Patrick Karcher

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接