I have some questions about the following code:
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();
}
}
}
My questions are:
- Will the using-statement that operates on the
Disposable
struct, returned fromTest()
box the struct, or not? - How can I find the answer myself?
To try to find out myself, I inspected the IL produced by the above code, and here's the IL for the Main(...)
method:
.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
}
I suspect the call to the virtual method there, on L_0010
will introduce a boxing operation, but the actual box
instruction is not here.
The reason I'm asking is that a while ago, probably 1-2 years, I saw online an "optimization" of the using-statement someone commented on. The case was where the using-statement was used as syntax for a short-time lock on an object, where the lock was acquired in the method, and a struct was returned, which when disposed of, would release the lock, code like this:
using (LockTheObject())
{
// use the object
}
and the comment was that by changing the return type of the LockTheObject
method from IDisposable
to the actual struct used, boxing was avoided.
But I'm wondering if this is true, or still true.
Can anyone point me in the right direction? If, in order to see the box operation, I'll have to inspect the runtime assembly code, please show me an example of what to look for, I'm well versed in assembly code so that's not a problem, but nothing jumped out at me when I looked at that either.
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:
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: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 followingcallvirt
instruction is receiving a managed pointer to aConsoleApplication2.Disposable
struct instead of the object reference it normally receives. In doing so, the JIT can resolve the sealed overload ofDispose()
implemented by the struct and call it directly without boxing the object. Without theconstrained
prefix, the object passed to thecallvirt
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.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 implementsIDisposable
is in ausing
statement, not in any other context.For more info please see The Using Statement And Disposable Value Types:
And also Oh No! Not the TimedLock Again!: