实现 DisposeAsync 方法

已将 System.IAsyncDisposable 接口作为 C# 8.0 的一部分引入。 需要执行资源清理时,可以实现 IAsyncDisposable.DisposeAsync() 方法,就像实现 Dispose 方法一样。 但是,其中一个主要区别是,此实现允许异步清理操作。 DisposeAsync() 返回表示异步释放操作的 ValueTask

通常,当实现IAsyncDisposable接口时,类还会实现IDisposable接口。 IAsyncDisposable接口的一种良好实现模式是为同步或异步释放做好准备,但这不是必需的。 如果无法实现类的同步可释放,则仅拥有 IAsyncDisposable 是可以接受的。 用于实现释放模式的所有指南也适用于异步实现。 本文假设你已熟悉如何实现 Dispose 方法

注意

如果实现 IAsyncDisposable 接口,但不实现 IDisposable 接口,则应用可能会泄漏资源。 如果一个类实现了 IAsyncDisposable,但没有实现 IDisposable,并且使用者只调用了 Dispose,那么实现将永远不会调用 DisposeAsync。 这将导致资源泄漏。

提示

对于依赖关系注入,在 IServiceCollection 中注册服务时,会代表你隐式管理IServiceCollectionIServiceProvider 和相应的 IHost 协调资源清理。 具体而言,IDisposableIAsyncDisposable 的实现在其指定生存期结束时正确释放。

有关详细信息,请参阅 .NET 中的依赖关系注入

探索 DisposeAsyncDisposeAsyncCore 方法

IAsyncDisposable 接口声明单个无参数方法 DisposeAsync()。 任何非密封类都应定义同时返回ValueTaskDisposeAsyncCore()方法。

  • 没有参数的 public IAsyncDisposable.DisposeAsync() 实现。

  • 一个 protected virtual ValueTask DisposeAsyncCore() 方法,其签名为:

    protected virtual ValueTask DisposeAsyncCore()
    {
    }
    

DisposeAsync 方法

public 无参数的 DisposeAsync() 方法在 await using 语句中隐式调用,其用途是释放非托管资源,执行常规清理,以及指示终结器(如果存在)不必运行。 释放与托管对象关联的内存始终是垃圾回收器的域。 因此,它具有标准实现:

public async ValueTask DisposeAsync()
{
    // Perform async cleanup.
    await DisposeAsyncCore();

    // Dispose of unmanaged resources.
    Dispose(false);

    // Suppress finalization.
    GC.SuppressFinalize(this);
}

注意

与释放模式相比,异步释放模式的主要差异在于,从 DisposeAsync()Dispose(bool) 重载方法的调用被赋予 false 作为参数。 但实现 IDisposable.Dispose() 方法时,改为传递 true。 这有助于确保与同步释放模式的功能等效性,并进一步确保仍调用终结器代码路径。 换句话说,DisposeAsyncCore() 方法将异步释放托管资源,因此不希望也同步释放这些资源。 因此,调用 Dispose(false) 而非 Dispose(true)

DisposeAsyncCore 方法

DisposeAsyncCore() 方法旨在执行受管理资源的异步清理,或对 DisposeAsync() 执行级联调用。 当子类继承作为 IAsyncDisposable 的实现的基类时,它会封装常见的异步清理操作。 DisposeAsyncCore()方法为virtual,以便派生类可以在其重写中定义自定义清理。

提示

如果 IAsyncDisposable 的实现是 sealed,则不需要 DisposeAsyncCore() 方法,异步清理可直接在 IAsyncDisposable.DisposeAsync() 方法中执行。

实现异步释放模式

所有非密封类都应被视为潜在的基类,因为它们可以被继承。 如果为任何潜在基类实现异步释放模式,则必须提供 protected virtual ValueTask DisposeAsyncCore() 方法。 下面的一些示例使用NoopAsyncDisposable定义如下的类:

public sealed class NoopAsyncDisposable : IAsyncDisposable
{
    ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask;
}

下面是使用NoopAsyncDisposable类型的异步释放模式的实现示例。 类型返回ValueTask.CompletedTask以实现DisposeAsync

public class ExampleAsyncDisposable : IAsyncDisposable
{
    private IAsyncDisposable? _example;

    public ExampleAsyncDisposable() =>
        _example = new NoopAsyncDisposable();

    public async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore().ConfigureAwait(false);

        GC.SuppressFinalize(this);
    }

    protected virtual async ValueTask DisposeAsyncCore()
    {
        if (_example is not null)
        {
            await _example.DisposeAsync().ConfigureAwait(false);
        }

        _example = null;
    }
}

在上面的示例中:

  • ExampleAsyncDisposable是实现IAsyncDisposable接口的非密封类。
  • 它包含私有的IAsyncDisposable 字段 _example,该字段在构造函数中进行初始化。
  • DisposeAsync方法委托给DisposeAsyncCore方法,并调用GC.SuppressFinalize以通知垃圾回收器,终结器不需要运行。
  • 它包含用来调用 _example.DisposeAsync() 方法的 DisposeAsyncCore() 方法,并将字段设置为 null
  • DisposeAsyncCore()方法为virtual,它允许子类使用自定义行为重写它。

密封的备用异步释放模式

如果你的实现类可以是 sealed,则可以通过重写 IAsyncDisposable.DisposeAsync() 方法来实现异步释放模式。 下面的示例展示了如何实现密封类的异步释放模式:

public sealed class SealedExampleAsyncDisposable : IAsyncDisposable
{
    private readonly IAsyncDisposable _example;

    public SealedExampleAsyncDisposable() =>
        _example = new NoopAsyncDisposable();

    public ValueTask DisposeAsync() => _example.DisposeAsync();
}

在上面的示例中:

  • SealedExampleAsyncDisposable 是实现 IAsyncDisposable 接口的密封类。
  • 包含的 _example 字段是 readonly,它在构造函数中进行初始化。
  • DisposeAsync 方法调用 _example.DisposeAsync() 方法,通过包含字段(级联处置)实现模式。

同时实现释放模式和异步释放模式

可能需要同时实现 IDisposableIAsyncDisposable 接口,尤其是当类范围包含这些实现的实例时。 这样做可确保你可以正确地级联清理调用。 下面是示例类,它实现两个接口并演示清理的正确指导。

class ExampleConjunctiveDisposableusing : IDisposable, IAsyncDisposable
{
    IDisposable? _disposableResource = new MemoryStream();
    IAsyncDisposable? _asyncDisposableResource = new MemoryStream();

    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    public async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore().ConfigureAwait(false);

        Dispose(disposing: false);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _disposableResource?.Dispose();
            _disposableResource = null;

            if (_asyncDisposableResource is IDisposable disposable)
            {
                disposable.Dispose();
                _asyncDisposableResource = null;
            }
        }
    }

    protected virtual async ValueTask DisposeAsyncCore()
    {
        if (_asyncDisposableResource is not null)
        {
            await _asyncDisposableResource.DisposeAsync().ConfigureAwait(false);
        }

        if (_disposableResource is IAsyncDisposable disposable)
        {
            await disposable.DisposeAsync().ConfigureAwait(false);
        }
        else
        {
            _disposableResource?.Dispose();
        }

        _asyncDisposableResource = null;
        _disposableResource = null;
    }
}

IDisposable.Dispose()IAsyncDisposable.DisposeAsync() 实现都是简单的样板代码。

Dispose(bool)重载方法中,如果IDisposable实例不为null,则有条件地将其释放。 IAsyncDisposable实例被强制转换为IDisposable,如果该实例也不为null,也将被释放。 然后,将这两个实例都分配给 null

使用 DisposeAsyncCore() 方法时,遵循相同的逻辑方法。 如果IAsyncDisposable实例不为null,等待其对DisposeAsync().ConfigureAwait(false)的调用。 如果 IDisposable 实例也是 IAsyncDisposable 的实现,也将其异步释放。 然后,将这两个实例都分配给 null

每个实现都努力释放所有可能的可释放对象。 这可确保正确级联清理。

使用异步释放

要正确使用实现 IAsyncDisposable 接口的对象,请将 awaitusing 关键字结合使用。 请考虑以下示例,其中 ExampleAsyncDisposable 类进行了实例化,然后包装在 await using 语句中。

class ExampleConfigureAwaitProgram
{
    static async Task Main()
    {
        var exampleAsyncDisposable = new ExampleAsyncDisposable();
        await using (exampleAsyncDisposable.ConfigureAwait(false))
        {
            // Interact with the exampleAsyncDisposable instance.
        }

        Console.ReadLine();
    }
}

重要

使用 IAsyncDisposable 接口的 ConfigureAwait(IAsyncDisposable, Boolean) 扩展方法配置延续任务在其原始上下文或计划程序上的封送方式。 有关 ConfigureAwait 的详细信息,请参阅 ConfigureAwait FAQ

对于不需要使用ConfigureAwait的情况,可以按如下所示简化await using语句:

class ExampleUsingStatementProgram
{
    static async Task Main()
    {
        await using (var exampleAsyncDisposable = new ExampleAsyncDisposable())
        {
            // Interact with the exampleAsyncDisposable instance.
        }

        Console.ReadLine();
    }
}

此外,它还可以编写为使用 using 声明的隐式范围。

class ExampleUsingDeclarationProgram
{
    static async Task Main()
    {
        await using var exampleAsyncDisposable = new ExampleAsyncDisposable();

        // Interact with the exampleAsyncDisposable instance.

        Console.ReadLine();
    }
}

单个行中有多个 await 关键字

有时 await 关键字可能在单个行中多次出现。 例如,考虑以下代码:

await using var transaction = await context.Database.BeginTransactionAsync(token);

在上面的示例中:

堆叠的 using

在创建和使用实现 IAsyncDisposable 的多个对象的情况下,残存错误条件中具有 ConfigureAwait 的堆叠 await using 语句可能会阻止调用 DisposeAsync()。 若要确保始终调用 DisposeAsync(),应避免堆叠。 下面的三个代码示例显示要改用的可接受模式。

可接受的模式一


class ExampleOneProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using (objOne.ConfigureAwait(false))
        {
            // Interact with the objOne instance.

            var objTwo = new ExampleAsyncDisposable();
            await using (objTwo.ConfigureAwait(false))
            {
                // Interact with the objOne and/or objTwo instance(s).
            }
        }

        Console.ReadLine();
    }
}

在前面的示例中,每个异步清理操作的范围都显式地限定在await using块下。 外部范围遵循objOne设置其大括号的方式;如果将objTwo括起来,这样就会先处理objTwo,然后处理objOne。 这两个IAsyncDisposable实例的DisposeAsync()方法都处于等待状态,因此每个实例都会执行其异步清理操作。 嵌套调用,而不是堆叠调用。

可接受的模式二

class ExampleTwoProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using (objOne.ConfigureAwait(false))
        {
            // Interact with the objOne instance.
        }

        var objTwo = new ExampleAsyncDisposable();
        await using (objTwo.ConfigureAwait(false))
        {
            // Interact with the objTwo instance.
        }

        Console.ReadLine();
    }
}

在前面的示例中,每个异步清理操作的范围都显式地限定在await using块下。 在每个块的末尾,相应的IAsyncDisposable实例使其DisposeAsync()方法等待,从而执行其异步清理操作。 按顺序排列调用,而不是堆叠调用。 在此场景中,首先处理 objOne,然后处理 objTwo

可接受的模式三

class ExampleThreeProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using var ignored1 = objOne.ConfigureAwait(false);

        var objTwo = new ExampleAsyncDisposable();
        await using var ignored2 = objTwo.ConfigureAwait(false);

        // Interact with objOne and/or objTwo instance(s).

        Console.ReadLine();
    }
}

在前面的示例中,每个异步清理操作都通过包含的方法主体隐式限定了范围。 在封闭块的末尾,IAsyncDisposable实例执行其异步清理操作。 此示例运行顺序与它们声明的顺序相反,这意味着objTwoobjOne之前被处理。

无法接受的模式

以下代码中突出显示的行显示了“堆积使用”的含义。 如果从 AnotherAsyncDisposable 构造函数引发异常,则这两个对象都不会正确释放。 从未分配该变量objTwo,因为构造函数未成功完成。 因此,AnotherAsyncDisposable 的构造函数负责释放在引发异常之前分配的任何资源。 如果 ExampleAsyncDisposable 类型具有终结器,则它能够实现终止。

class DoNotDoThisProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        // Exception thrown on .ctor
        var objTwo = new AnotherAsyncDisposable();

        await using (objOne.ConfigureAwait(false))
        await using (objTwo.ConfigureAwait(false))
        {
            // Neither object has its DisposeAsync called.
        }

        Console.ReadLine();
    }
}

提示

避免此模式,因为它可能导致意外行为。 如果使用其中一种可接受的模式,则不存在未处理对象。 using 语句未堆叠时,将正确执行清理操作。

请参阅

有关 IDisposableIAsyncDisposable 的双重实现示例,请参阅 GitHub 上的 Utf8JsonWriter 源代码。