This really is interesting!.
It is apparent that combination of the use of an existing variable in the using()
statement and the reassignment of the original within the block, results in a new reference which is passed to Dispose()
, and not the existing _proxy
, as per the IL below.
In fact, LinqPad issues a warning to this effect:
Possibly incorrect assignment to local 'variable' which is the argument to a using or lock statement. The Dispose call or unlocking will happen on the original value of the local.
However this behaviour does not appear to require a cast to as IDisposable
, nor does the static
seem to have any effect.
Disassembling the following code to IL in LINQPad:
IMyService _proxy = null;
if (_proxy == null)
{
using (_proxy)
{
_proxy = new SomeService();
}
}
Yields
IL_0001: ldnull
IL_0002: stloc.0 // _proxy
IL_0003: ldloc.0 // _proxy
IL_0004: ldnull
IL_0005: ceq
IL_0007: ldc.i4.0
IL_0008: ceq
IL_000A: stloc.1 // CS$4$0000
IL_000B: ldloc.1 // CS$4$0000
IL_000C: brtrue.s IL_002D
IL_000E: nop
** IL_000F: ldloc.0 // _proxy
** IL_0010: stloc.2 // CS$3$0001
IL_0011: nop
IL_0012: newobj UserQuery+SomeService..ctor
IL_0017: stloc.0 // _proxy
IL_0018: nop
IL_0019: leave.s IL_002B
$IL_001B: ldloc.2 // CS$3$0001
$IL_001C: ldnull
$IL_001D: ceq
IL_001F: stloc.1 // CS$4$0000
IL_0020: ldloc.1 // CS$4$0000
$IL_0021: brtrue.s IL_002A
** IL_0023: ldloc.2 // CS$3$0001
IL_0024: callvirt System.IDisposable.Dispose
IL_0029: nop
IL_002A: endfinally
IL_002B: nop
As can be seen (by the **'s), that a new loc 2 is created, and it is this reference which is passed to Dispose()
. But prior to this is a check for null ($), which bypasses the Dispose
in any event.
Contrast this with the explicit try-finally
:
if (_proxy == null)
{
try
{
_proxy = new SomeService();
}
finally
{
_proxy.Dispose();
}
}
IL_0001: ldnull
IL_0002: stloc.0 // _proxy
IL_0003: ldloc.0 // _proxy
IL_0004: ldnull
IL_0005: ceq
IL_0007: ldc.i4.0
IL_0008: ceq
IL_000A: stloc.1 // CS$4$0000
IL_000B: ldloc.1 // CS$4$0000
IL_000C: brtrue.s IL_0025
IL_000E: nop
IL_000F: nop
IL_0010: newobj UserQuery+SomeService..ctor
IL_0015: stloc.0 // _proxy
IL_0016: nop
IL_0017: leave.s IL_0023
IL_0019: nop
** IL_001A: ldloc.0 // _proxy
IL_001B: callvirt System.IDisposable.Dispose
IL_0020: nop
IL_0021: nop
IL_0022: endfinally
IL_0023: nop
Where it can be clearly seen that it is the "original" _proxy
which is Disposed, without the extra null check done in the using()
, confirming the docs.
Needless to say this code is evil:
_proxy
will never be Disposed
, viz the using
is redundant.
- It relies on passing null to
using
not failing.
- Because
_proxy
is defined prior to the using
, it can be reassigned within the block, unlike variables declared IN the using statement - e.g. using (var _proxy = ...)
would be read-only
, and attempting to mutate such a variable would result in a compile time error anyway.
- The merits of static singleton channels aside, the lazy initialization of
_proxy
is also not thread-safe (e.g. no double-check lock), and could be simplified with a private static readonly Lazy<IMyService>(() => ...)
and a simple getter.