Reputation: 93444
While debugging in an app, I found the following code. It's obviously wrong, but for some strange reason it works, and I can't seem to understand why. It would seem to me that the Proxy would be disposed as soon as it was created, yet calling methods on this proxy work correctly to connect to the WCF service.
Can anyone explain why this code doesn't blow up?
private static IMyService _proxy = null;
private static IMyService Proxy
{
get
{
if (_proxy == null)
{
using (_proxy as IDisposable)
{
ChannelFactory<IMyService> factory =
new ChannelFactory<IMyService>("MyService");
_proxy = factory.CreateChannel();
}
}
return _proxy;
}
}
Upvotes: 3
Views: 158
Reputation: 107357
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.using
not failing._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._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.Upvotes: 6
Reputation: 263097
That code works because of:
using (_proxy as IDisposable)
The expression _proxy as IDisposable
is not _proxy
, and that explains why _proxy
can be assigned to within the using
block without the C# compiler complaining in any way.
In addition, even if _proxy
is subsequently set to a non-null
disposable instance, the original value as "seen" by the using
block (_proxy as IDisposable
) will still be null
(there are no references involved in this context).
Therefore, nothing is disposed of, a new proxy is created and cached, and that code succeeds.
I'm still wondering about the potential reasons why one would like to write such a using
statement, however.
Upvotes: 2
Reputation: 116636
Well, apparently you can have null as the input to a using scope:
using (null)
{
Console.WriteLine("hamster");
}
What happens is that _proxy
is null and therefore _proxy as IDisposable
is also null which makes the using scope redundant, no Dispose
will be called (or attempted). The code is equivalent to this:
if (_proxy == null)
{
ChannelFactory<IBasketService> factory =
new ChannelFactory<IBasketService>("MyService");
_proxy = factory.CreateChannel();
}
return _proxy;
It doesn't matter that _proxy
is assigned to while inside the scope, Dispose
would be "invoked" on the original copied value (which is null).
Here's a simple snippet that reproduces the issue. The "Disposed" message will not be written to console.
internal class Program
{
private static DisposableScope _proxy = null;
private static void Main(string[] args)
{
using (_proxy)
{
_proxy = new DisposableScope();
}
}
}
public class DisposableScope : IDisposable
{
public void Dispose()
{
Console.WriteLine("Disposed");
}
}
Upvotes: 2