为什么 C# 7 丢弃标识符 _ 仍然在 using 块中工作?

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

因此,我在开发 UWP 应用程序时经常使用的一种模式是使用 SemaphoreSlim 实例来避免竞争条件(我不喜欢使用 lock,因为它需要一个额外的目标对象,并且它不会异步锁定)。

典型的代码片段如下所示:

private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1);

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

随着额外的 try/finally 块围绕整个事情,如果中间的代码可能崩溃但我想保持信号量正常工作。

为了减少样板文件,我尝试编写一个包装器 class,它具有相同的行为(包括 try/finally 位),但需要的代码更少。我也不想使用 delegate,因为那样每次都会创建一个对象,我只是想减少我的代码而不改变它的工作方式。

我想出了这个 class(为简洁起见删除了评论):

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();
    }
}

它的工作方式是,通过使用它,您只需要以下内容:

private readonly AsyncMutex Mutex = new AsyncMutex();

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

缩短了一行,并且内置了 try/finallyusing 块),太棒了。

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

丢弃 _ 实际上只是出于好奇,因为我知道我应该写 var _,因为我需要在 [=20] 的末尾使用 IDisposable 对象=] 块,而不是丢弃器。

但是,令我惊讶的是,两种方法都生成了相同的 IL:

.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    
}

"discarder" IDisposable 存储在字段 V_1 中并正确处理。

那么,为什么会这样呢? docs 没有说明丢弃运算符与 using 块一起使用,他们只是说丢弃赋值被完全忽略。

谢谢!

丢弃功能的使用在这里真的是一个转移注意力的问题。之所以可行,是因为 using 语句可以接受解析为要处理的值的表达式(除了声明变量的替代语法之外)。此外,赋值运算符 解析为赋值

您在赋值运算符右侧提供的值是您的 Lock 对象,因此这就是表达式 _ = await Mutex.Lock() 解析的内容。由于 value(不是作为变量声明,而是作为独立值)是一次性的,因此它将在 using 结束时清理。

using 语句不需要显式声明局部变量。也可以使用表达式。

语言规范指定了以下语法。

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

现有变量的赋值(或丢弃结果)也是一个表达式。例如下面的代码编译:

var a = (_ = 10);