Partilhar via


Implementar um método DisposeAsync

A System.IAsyncDisposable interface foi introduzida como parte do C# 8.0. Você implementa o método quando precisa executar a IAsyncDisposable.DisposeAsync() limpeza de recursos, assim como faria ao implementar um método Dispose. Uma das principais diferenças, no entanto, é que essa implementação permite operações de limpeza assíncronas. O DisposeAsync() retorna a ValueTask que representa a operação de descarte assíncrona.

É típico ao implementar a interface que as IAsyncDisposable classes também implementem a IDisposable interface. Um bom padrão de implementação da interface deve ser preparado para eliminação síncrona ou assíncrona IAsyncDisposable , no entanto, não é um requisito. Se não for possível nenhum descartável síncrono da sua turma, ter apenas IAsyncDisposable é aceitável. Todas as orientações para implementar o padrão de descarte também se aplicam à implementação assíncrona. Este artigo pressupõe que você já esteja familiarizado com como implementar um método Dispose.

Atenção

Se você implementar a IAsyncDisposable interface, mas não a IDisposable interface, seu aplicativo poderá vazar recursos. Se uma classe implementa IAsyncDisposable, mas não IDisposable, e um consumidor só chama Dispose, sua implementação nunca chamaria DisposeAsync. Isso resultaria em um vazamento de recursos.

Gorjeta

Com relação à injeção de dependência, ao registrar serviços em um IServiceCollection, o tempo de vida do serviço é gerenciado implicitamente em seu nome. O e correspondente IServiceProvider IHost orquestrar a limpeza de recursos. Especificamente, implementações de IDisposable e IAsyncDisposable são adequadamente descartadas no final de sua vida útil especificada.

Para obter mais informações, consulte Injeção de dependência no .NET.

Exploração DisposeAsync e DisposeAsyncCore métodos

A IAsyncDisposable interface declara um único método sem parâmetros, DisposeAsync(). Qualquer classe não selada deve definir um DisposeAsyncCore() método que também retorne um ValueTaskarquivo .

  • Uma public IAsyncDisposable.DisposeAsync() implementação que não tem parâmetros.

  • Um protected virtual ValueTask DisposeAsyncCore() método cuja assinatura é:

    protected virtual ValueTask DisposeAsyncCore()
    {
    }
    

O DisposeAsync método

O public método sem DisposeAsync() parâmetros é chamado implicitamente em uma await using instrução e sua finalidade é liberar recursos não gerenciados, executar limpeza geral e indicar que o finalizador, se estiver presente, não precisa ser executado. Liberar a memória associada a um objeto gerenciado é sempre o domínio do coletor de lixo. Por isso, tem uma implementação padrão:

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

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

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

Nota

Uma diferença primária no padrão de descarte assíncrono em comparação com o padrão de descarte é que a chamada de para o Dispose(bool) método de DisposeAsync() sobrecarga é dada false como um argumento. Ao implementar o IDisposable.Dispose() método, no entanto, true é passado em vez disso. Isso ajuda a garantir a equivalência funcional com o padrão de descarte síncrono e garante ainda que os caminhos de código do finalizador ainda sejam invocados. Em outras palavras, o DisposeAsyncCore() método descartará recursos gerenciados de forma assíncrona, portanto, você não deseja descartá-los de forma síncrona também. Portanto, chame Dispose(false) em vez de Dispose(true).

O DisposeAsyncCore método

O DisposeAsyncCore() método destina-se a executar a limpeza assíncrona de recursos gerenciados ou para chamadas em cascata para DisposeAsync(). Ele encapsula as operações de limpeza assíncronas comuns quando uma subclasse herda uma classe base que é uma implementação do IAsyncDisposable. O DisposeAsyncCore() método é virtual para que as classes derivadas possam definir a limpeza personalizada em suas substituições.

Gorjeta

Se uma implementação de é sealed, o DisposeAsyncCore() método não é necessário, e a limpeza assíncrona pode ser executada IAsyncDisposable diretamente no IAsyncDisposable.DisposeAsync() método.

Implementar o padrão de descarte assíncrono

Todas as classes não seladas devem ser consideradas uma potencial classe base, porque podem ser herdadas. Se você implementar o padrão de descarte assíncrono para qualquer classe base potencial, deverá fornecer o protected virtual ValueTask DisposeAsyncCore() método. Alguns dos exemplos a seguir usam uma NoopAsyncDisposable classe que é definida da seguinte maneira:

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

Aqui está um exemplo de implementação do padrão de descarte assíncrono que usa o NoopAsyncDisposable tipo. O tipo implementa DisposeAsync retornando ValueTask.CompletedTask.

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

No exemplo anterior:

  • O ExampleAsyncDisposable é uma classe não selada que implementa a IAsyncDisposable interface.
  • Ele contém um campo privado IAsyncDisposable , _example, que é inicializado no construtor.
  • O DisposeAsync método delega ao DisposeAsyncCore método e chama GC.SuppressFinalize para notificar o coletor de lixo de que o finalizador não precisa ser executado.
  • Ele contém um DisposeAsyncCore() método que chama o _example.DisposeAsync() método e define o campo como null.
  • O DisposeAsyncCore() método é virtual, que permite que as subclasses o substituam por um comportamento personalizado.

Padrão de descarte assíncrono alternativo selado

Se sua classe de implementação pode ser sealed, você pode implementar o padrão de descarte assíncrono substituindo o IAsyncDisposable.DisposeAsync() método. O exemplo a seguir mostra como implementar o padrão de descarte assíncrono para uma classe selada:

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

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

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

No exemplo anterior:

  • O SealedExampleAsyncDisposable é uma classe selada que implementa a IAsyncDisposable interface.
  • O campo que contém _example é readonly e é inicializado no construtor.
  • O DisposeAsync método chama o _example.DisposeAsync() método, implementando o padrão através do campo de contenção (descarte em cascata).

Implementar padrões de descarte assíncrono e de descarte assíncrono

Talvez seja necessário implementar as interfaces e IAsyncDisposable , especialmente quando o IDisposable escopo da classe contém instâncias dessas implementações. Isso garante que você possa fazer chamadas de limpeza em cascata corretamente. Aqui está uma classe de exemplo que implementa ambas as interfaces e demonstra a orientação adequada para limpeza.

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

As IDisposable.Dispose() implementações e IAsyncDisposable.DisposeAsync() são um código clichê simples.

Dispose(bool) No método de sobrecarga, a IDisposable instância é descartada condicionalmente se não nullfor . A IAsyncDisposable instância é lançada como IDisposable, e se também não nullfor, também é descartada. Ambas as instâncias são então atribuídas a null.

Com o DisposeAsyncCore() método, a mesma abordagem lógica é seguida. Se a IAsyncDisposable instância não nullfor , sua chamada para DisposeAsync().ConfigureAwait(false) será aguardada. Se a IDisposable instância também for uma implementação do IAsyncDisposable, ela também será descartada de forma assíncrona. Ambas as instâncias são então atribuídas a null.

Cada implementação se esforça para descartar todos os objetos descartáveis possíveis. Isso garante que a limpeza seja feita em cascata corretamente.

Usando assíncrono descartável

Para consumir corretamente um objeto que implementa a IAsyncDisposable interface, use as palavras-chave await e use juntas. Considere o exemplo a seguir, onde a classe é instanciada ExampleAsyncDisposable e, em seguida, encapsulada em uma await using instrução.

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

        Console.ReadLine();
    }
}

Importante

Use o ConfigureAwait(IAsyncDisposable, Boolean) método de extensão da IAsyncDisposable interface para configurar como a continuação da tarefa é organizada em seu contexto ou agendador original. Para obter mais informações sobre ConfigureAwaito , consulte Perguntas frequentes sobre ConfigureAwait.

Para situações em que o uso de ConfigureAwait não é necessário, a await using declaração pode ser simplificada da seguinte forma:

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

        Console.ReadLine();
    }
}

Além disso, poderia ser escrito para usar o escopo implícito de uma declaração de uso.

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

        // Interact with the exampleAsyncDisposable instance.

        Console.ReadLine();
    }
}

Várias palavras-chave aguardam em uma única linha

Às vezes, a await palavra-chave pode aparecer várias vezes dentro de uma única linha. Por exemplo, considere o seguinte código:

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

No exemplo anterior:

  • O BeginTransactionAsync método é aguardado.
  • O tipo de retorno é DbTransaction, que implementa IAsyncDisposable.
  • O transaction é usado de forma assíncrona, e também aguardado.

Utilizações empilhadas

Em situações em que você cria e usa vários objetos que implementam IAsyncDisposableo , é possível que o empilhamento await using de instruções com ConfigureAwait possa impedir chamadas para DisposeAsync() em condições errantes. Para garantir que DisposeAsync() é sempre chamado, você deve evitar o empilhamento. Os três exemplos de código a seguir mostram padrões aceitáveis para usar.

Padrão aceitável um


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

No exemplo anterior, cada operação de limpeza assíncrona tem um escopo explícito sob o await using bloco . O escopo externo segue como objOne define seus aparelhos, encerrando objTwo, como tal objTwo é descartado primeiro, seguido por objOne. Ambas as IAsyncDisposable instâncias têm seu DisposeAsync() método aguardado, para que cada instância execute sua operação de limpeza assíncrona. As chamadas são aninhadas, não empilhadas.

Padrão aceitável dois

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

No exemplo anterior, cada operação de limpeza assíncrona tem um escopo explícito sob o await using bloco . No final de cada bloco, a instância correspondente IAsyncDisposable tem seu DisposeAsync() método aguardado, executando assim sua operação de limpeza assíncrona. As chamadas são sequenciais, não empilhadas. Neste cenário objOne é descartado primeiro, depois objTwo é descartado.

Padrão aceitável três

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

No exemplo anterior, cada operação de limpeza assíncrona tem um escopo implícito com o corpo do método que contém. No final do bloco de delimitação, as IAsyncDisposable instâncias executam suas operações de limpeza assíncronas. Este exemplo é executado na ordem inversa a partir da qual eles foram declarados, o que significa que objTwo é descartado antes objOnede .

Padrão inaceitável

As linhas destacadas no código a seguir mostram o que significa ter "usos empilhados". Se uma exceção for lançada do AnotherAsyncDisposable construtor, nenhum objeto será descartado corretamente. A variável objTwo nunca é atribuída porque o construtor não foi concluído com êxito. Como resultado, o construtor for AnotherAsyncDisposable é responsável por descartar quaisquer recursos alocados antes de lançar uma exceção. Se o ExampleAsyncDisposable tipo tiver um finalizador, ele é elegível para finalização.

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

Gorjeta

Evite esse padrão, pois ele pode levar a um comportamento inesperado. Se você usar um dos padrões aceitáveis, o problema de objetos não dispostos é inexistente. As operações de limpeza são executadas corretamente quando using as instruções não são empilhadas.

Consulte também

Para obter um exemplo de implementação dupla de IDisposable e IAsyncDisposable, consulte o Utf8JsonWriter código-fonte no GitHub.