Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does the C# 7 discard identifier _ still work in a using block?

So, a pattern I use very often while working on my UWP app is to use a SemaphoreSlim instance to avoid race conditions (I prefer not to use lock as it needs an additional target object, and it doesn't lock asynchronously).

A typical snippet would look like this:

private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1);

public async Task FooAsync()
{
    await Semaphore.WaitAsync();
    // Do stuff here
    Semaphore.Release();
}

With the additional try/finally block around the whole thing, if the code in between could crash but I want to keep the semaphore working properly.

To reduce the boilerplate, I tried to write a wrapper class that would have the same behavior (including the try/finally bit) with less code needed. I also didn't want to use a delegate, as that'd create an object every time, and I just wanted to reduce my code without changing the way it worked.

I came up with this class (comments removed for brevity):

public sealed class AsyncMutex
{
    private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1);

    public async Task<IDisposable> Lock()
    {
        await Semaphore.WaitAsync().ConfigureAwait(false);
        return new _Lock(Semaphore);
    }

    private sealed class _Lock : IDisposable
    {
        private readonly SemaphoreSlim Semaphore;

        public _Lock(SemaphoreSlim semaphore) => Semaphore = semaphore;

        void IDisposable.Dispose() => Semaphore.Release();
    }
}

And the way it works is that by using it you only need the following:

private readonly AsyncMutex Mutex = new AsyncMutex();

public async Task FooAsync()
{
    using (_ = await Mutex.Lock())
    {
        // Do stuff here
    }
}

One line shorter, and with try/finally built in (using block), awesome.

Now, I have no idea why this works, despite the discard operator being used.

That discard _ was actually just out of curiosity, as I knew I should have just written var _, since I needed that IDisposable object to be used at the end of the using block, and not discarder.

But, to my surprise, the same IL is generated for both methods:

.method public hidebysig instance void T1() cil managed 
{
    .maxstack 1
    .locals init (
        [0] class System.Threading.Tasks.AsyncMutex mutex,
        [1] class System.IDisposable V_1
    )
    IL_0001: newobj       instance void System.Threading.Tasks.AsyncMutex::.ctor()
    IL_0006: stloc.0      // mutex

    IL_0007: ldloc.0      // mutex
    IL_0008: callvirt     instance class System.Threading.Tasks.Task`1<class System.IDisposable> System.Threading.Tasks.AsyncMutex::Lock()
    IL_000d: callvirt     instance !0/*class System.IDisposable*/ class System.Threading.Tasks.Task`1<class System.IDisposable>::get_Result()
    IL_0012: stloc.1      // V_1
    .try
    {
        // Do stuff here..
        IL_0025: leave.s      IL_0032
    }
    finally
    {
        IL_0027: ldloc.1      // V_1
        IL_0028: brfalse.s    IL_0031
        IL_002a: ldloc.1      // V_1
        IL_002b: callvirt     instance void System.IDisposable::Dispose()
        IL_0031: endfinally   
    }
    IL_0032: ret    
}

The "discarder" IDisposable is stored in the field V_1 and correctly disposed.

So, why does this happen? The docs don't say anything about the discard operator being used with the using block, and they just say the discard assignment is ignored completely.

Thanks!

like image 488
Sergio0694 Avatar asked Mar 02 '18 19:03

Sergio0694


1 Answers

The using statement does not require an explicit declaration of a local variable. An expression is also allowed.

The language specification specifies the following syntax.

using_statement
    : 'using' '(' resource_acquisition ')' embedded_statement
    ;

resource_acquisition
    : local_variable_declaration
    | expression
    ;

If the form of resource_acquisition is local_variable_declaration then the type of the local_variable_declaration must be either dynamic or a type that can be implicitly converted to System.IDisposable. If the form of resource_acquisition is expression then this expression must be implicitly convertible to System.IDisposable.

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#the-using-statement

An assignment of an existing variable (or discarding the result) is also an expression. For example the following code compiles:

var a = (_ = 10);
like image 154
Wazner Avatar answered Oct 03 '22 18:10

Wazner