When does a using-statement box its argument, when it's a struct?

一笑奈何 提交于 2019-11-29 04:40:13

It appears as though any value type that gets put in a using statement will not be boxed. This appears to be a C# optimization as boxing is only omitted when a value type that implements IDisposable is in a using statement, not in any other context.

For more info please see The Using Statement And Disposable Value Types:

A while ago Ian Griffiths wrote about an improvement to his TimedLock class in which he changed it from a class to a struct. This change resulted in a value type that implements IDisposable. I had a nagging question in the back of my mind at the time that I quickly forgot about. The question is wouldn’t instances of that type get boxed when calling Dispose?

And also Oh No! Not the TimedLock Again!:

John Sands points out a flaw in the code I showed in a recent blog for using timeouts on locks without abandoning most of the convenience of C#'s lock keyword .

Eric Lippert

This is a duplicate of If my struct implements IDisposable will it be boxed when used in a using statement?

UPDATE: This question was the subject of my blog in March of 2011. Thanks for the great question!

Andrew Hare's answer is correct; I just wanted to add an interesting extra note. The optimization we emit -- of using constrained callvirt to skip the boxing when possible -- is actually strictly speaking a violation of the C# specification. The spec states that the finally block we generate for a value type resource is:

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

which clearly is a boxing conversion on a value type. It is possible to construct contrived scenarios in which the lack of boxing in the implementation is visible.

(Thanks are due to Vladimir Reshetnikov for pointing out this spec violation to me.)

Instance methods on a value type take a this parameter as their first argument similar to how instance methods on reference types do. However, the parameter in this case is a managed pointer to the object's data, not a reference to a boxed object. You might find it laid out in memory like 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 is the same in both of these cases¹.

Instance methods on a value type expect the managed pointer to data specifically so boxing the objects is not required. As you see above, the constrained opcode is used before the call. It tells the runtime that the following callvirt instruction is receiving a managed pointer to a ConsoleApplication2.Disposable struct instead of the object reference it normally receives. In doing so, the JIT can resolve the sealed overload of Dispose() implemented by the struct and call it directly without boxing the object. Without the constrained prefix, the object passed to the callvirt instruction would have to be an object reference, because the standard virtual call dynamic resolution procedure is based on the fact that the GC/Object header is always in the expected location - and yes, this would force boxing for value types.

¹ We'll go ahead and ignore Nullable<T> for now.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!