Implementare un metodo DisposeAsync

L'interfaccia System.IAsyncDisposable è stata introdotta come parte di C# 8.0. Implementare il metodo IAsyncDisposable.DisposeAsync() quando è necessario eseguire la pulizia delle risorse, esattamente come quando si implementa un metodo Dispose. Una delle differenze principali, tuttavia, è che questa implementazione consente operazioni di pulizia asincrone. L’ggetto DisposeAsync() restituisce un oggetto ValueTask che rappresenta l'operazione di eliminazione asincrona.

È tipico quando si implementa l'interfaccia IAsyncDisposable che le classi implementino anche l'interfaccia IDisposable. Un buon modello di implementazione dell'interfaccia IAsyncDisposable deve essere preparato per l'eliminazione sia sincrona che asincrona, ma non è un requisito. Se non è possibile disporre di una classe sincrona, è accettabile avere solo IAsyncDisposable. Tutte le linee guida per l'implementazione del modello di eliminazione si applicano anche all'implementazione asincrona. Questo articolo presuppone che si abbia già familiarità con l'implementazione di un metodo Dispose.

Attenzione

Se implementi l'interfaccia IAsyncDisposable ma non l'interfaccia IDisposable, l'app può potenzialmente perdere risorse. Se una classe implementa IAsyncDisposable, ma non IDisposable, e un consumer chiama solo Dispose, l'implementazione non chiamerebbe mai DisposeAsync. In questo modo si verifica una perdita di risorse.

Suggerimento

Per quanto riguarda l'inserimento delle dipendenze, quando si registrano servizi in un oggetto IServiceCollection, la durata del servizio viene gestita in modo implicito per conto dell'utente. IServiceProvider e IHost corrispondente orchestrano la pulizia delle risorse. In particolare, le implementazioni di IDisposable e IAsyncDisposable vengono eliminate correttamente alla fine della durata specificata.

Per altre informazioni, vedere Inserimento delle dipendenze in .NET.

Esplorare metodi DisposeAsync e DisposeAsyncCore

L'interfaccia IAsyncDisposable dichiara un singolo metodo senza parametri, DisposeAsync(). Qualsiasi classe nonsealed deve definire un metodo DisposeAsyncCore() che restituisce anche un oggetto ValueTask.

  • Un’implementazione publicIAsyncDisposable.DisposeAsync() senza parametri.

  • Un metodo protected virtual ValueTask DisposeAsyncCore() la cui firma è:

    protected virtual ValueTask DisposeAsyncCore()
    {
    }
    

Metodo DisposeAsync

Il metodo DisposeAsync() senza parametri public viene chiamato in modo implicito in un'istruzione await using e lo scopo è liberare risorse non gestite, eseguire la pulizia generale e indicare che non è necessario eseguire il finalizzatore, se presente. Liberare la memoria associata a un oggetto gestito è sempre il dominio del Garbage Collector. Per questo motivo il metodo ha un'implementazione standard:

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

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

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

Nota

Una differenza principale nel modello dispose asincrono rispetto al modello dispose consiste nel fatto che alla chiamata da DisposeAsync() al metodo di overload Dispose(bool) viene dato false come argomento. Quando si implementa il metodo IDisposable.Dispose(), tuttavia, viene passato true. Ciò consente di garantire l'equivalenza funzionale con il modello dispose sincrono e garantisce inoltre che i percorsi di codice del finalizzatore vengano comunque richiamati. In altre parole, il metodo DisposeAsyncCore() eliminerà le risorse gestite in modo asincrono, quindi non si vuole eliminarle in modo sincrono. Pertanto, chiamare Dispose(false) invece di Dispose(true).

Metodo DisposeAsyncCore

Il metodo DisposeAsyncCore() è progettato per eseguire la pulizia asincrona delle risorse gestite o per le chiamate a catena a DisposeAsync(). Incapsula le operazioni comuni di pulizia asincrona quando una sottoclasse eredita una classe base che è un'implementazione di IAsyncDisposable. Il metodo DisposeAsyncCore() è virtual in modo che le classi derivate possano definire la pulizia personalizzata nelle sostituzioni.

Suggerimento

Se un'implementazione di IAsyncDisposable è sealed, il metodo DisposeAsyncCore() non è necessario e la pulizia asincrona può essere eseguita direttamente nel metodo IAsyncDisposable.DisposeAsync().

Implementare il modello dispose asincrono

Tutte le classi nonsealed devono essere considerate una classe base potenziale, perché potrebbero essere ereditate. Se si implementa il modello dispose asincrono per qualsiasi classe base potenziale, è necessario fornire il metodo protected virtual ValueTask DisposeAsyncCore(). Alcuni degli esempi seguenti usano una classe NoopAsyncDisposable definita come segue:

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

Ecco un esempio di implementazione del modello dispose asincrono che usa il tipo NoopAsyncDisposable. Il tipo implementa DisposeAsync restituendo 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;
    }
}

Nell'esempio precedente:

  • ExampleAsyncDisposable è una classe nonsealed che implementa l'interfaccia IAsyncDisposable.
  • Contiene un campo IAsyncDisposable privato, _example, inizializzato nel costruttore.
  • Il metodo DisposeAsync delega al metodo DisposeAsyncCore e chiama GC.SuppressFinalize per notificare al Garbage Collector che il finalizzatore non deve essere eseguito.
  • Contiene un metodo DisposeAsyncCore() che chiama il metodo _example.DisposeAsync() e imposta il campo su null.
  • Il metodo DisposeAsyncCore() è virtual, che consente alle sottoclassi di eseguirne l'override con un comportamento personalizzato.

Modello alternativo di eliminazione asincrona sealed

Se la classe di implementazione può essere sealed, è possibile implementare il modello dispose asincrono eseguendo l'override del metodo IAsyncDisposable.DisposeAsync(). L'esempio seguente illustra come implementare il modello dispose asincrono per una classe sealed:

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

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

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

Nell'esempio precedente:

  • SealedExampleAsyncDisposable è una classe sealed che implementa l'interfaccia IAsyncDisposable.
  • Il campo contenitore _example è readonly e viene inizializzato nel costruttore.
  • Il metodo DisposeAsync chiama il metodo _example.DisposeAsync(), implementando il modello tramite il campo contenitore (eliminazione a catena).

Implementare modelli dispose e async dispose

Potrebbe essere necessario implementare entrambe le interfacce IDisposable e IAsyncDisposable, soprattutto quando l'ambito della classe contiene istanze di queste implementazioni. In questo modo è possibile eseguire correttamente la pulizia delle chiamate. Ecco una classe di esempio che implementa entrambe le interfacce e illustra le linee guida appropriate per la pulizia.

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

Le implementazioni di IDisposable.Dispose() e IAsyncDisposable.DisposeAsync() sono entrambe semplici codice boilerplate.

Nel metodo di overload Dispose(bool), l'istanza IDisposable viene eliminata in modo condizionale se non è null. Viene eseguito il cast dell'istanza IAsyncDisposable come IDisposable e, se anch’essa non è null, viene eliminata. Entrambe le istanze vengono quindi assegnate a null.

Con il metodo DisposeAsyncCore() viene seguito lo stesso approccio logico. Se l'istanza IAsyncDisposable non è null, è attesa la chiamata a DisposeAsync().ConfigureAwait(false). Se l'istanza IDisposable è anche un'implementazione di IAsyncDisposable, viene eliminata in modo asincrono. Entrambe le istanze vengono quindi assegnate a null.

Ogni implementazione cerca di eliminare tutti i possibili oggetti eliminabili. In questo modo si garantisce che la pulizia venga propagata correttamente.

Uso di asincrono eliminabile

Per utilizzare correttamente un oggetto che implementa l'interfaccia IAsyncDisposable, usare le parole chiave await e using insieme. Si consideri l'esempio seguente, in cui viene creata un'istanza della classe ExampleAsyncDisposable e quindi viene eseguito il wrapping in un'istruzione await using.

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

        Console.ReadLine();
    }
}

Importante

Usare il metodo di estensione ConfigureAwait(IAsyncDisposable, Boolean) dell'interfaccia IAsyncDisposable per configurare la modalità di marshalling della continuazione dell'attività nel contesto o nell'utilità di pianificazione originale. Per altre informazioni su ConfigureAwait, vedere Domande frequenti su ConfigureAwait.

Per le situazioni in cui l'utilizzo di ConfigureAwait non è necessario, l'istruzione await using potrebbe essere semplificata come indicato di seguito:

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

        Console.ReadLine();
    }
}

Inoltre, potrebbe essere scritto per usare l'ambito implicito di una dichiarazione using.

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

        // Interact with the exampleAsyncDisposable instance.

        Console.ReadLine();
    }
}

Più parole chiave await in una singola riga

A volte la parola chiave await può essere visualizzata più volte all'interno di una singola riga. Si consideri il codice di esempio seguente:

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

Nell'esempio precedente:

  • Il metodo BeginTransactionAsync è in attesa.
  • Il tipo restituito è DbTransaction, che implementa IAsyncDisposable.
  • transaction viene usato in modo asincrono e anche atteso.

Usi in pila

Nelle situazioni in cui si creano e usano più oggetti che implementano IAsyncDisposable, è possibile che le istruzioni di stacking await using con ConfigureAwait potrebbero impedire chiamate a DisposeAsync() in condizioni errate. Per assicurarsi che DisposeAsync() venga sempre chiamato, è consigliabile evitare lo stacking. I tre esempi di codice seguenti mostrano modelli accettabili da usare.

Modello accettabile uno


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

Nell'esempio precedente ogni operazione di pulizia asincrona ha come ambito esplicito il blocco await using. L'ambito esterno segue il modo in cui objOne imposta le parentesi graffe, racchiudendo objTwo, in quanto objTwo viene eliminato per primo, seguito da objOne. Entrambe le istanze IAsyncDisposable hanno il metodo DisposeAsync() atteso, quindi ogni istanza esegue l'operazione di pulizia asincrona. Le chiamate sono annidate, non in pila.

Modello accettabile due

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

Nell'esempio precedente ogni operazione di pulizia asincrona ha come ambito esplicito il blocco await using. Alla fine di ogni blocco, l'istanza IAsyncDisposable corrispondente ha il relativo metodo DisposeAsync() atteso, eseguendo così l'operazione di pulizia asincrona. Le chiamate sono sequenziali, non in pila. In questo scenario objOne viene eliminato per primo, poi viene eliminato objTwo.

Modello accettabile tre

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

Nell'esempio precedente ogni operazione di pulizia asincrona ha come ambito implicito il corpo del metodo contenitore. Alla fine del blocco di inclusione, le istanze IAsyncDisposable eseguono le operazioni di pulizia asincrone. Questo esempio viene eseguito in ordine inverso rispetto a quello in cui sono stati dichiarati, ovvero objTwo viene eliminato prima di objOne.

Modello inaccettabile

Le righe evidenziate nel codice seguente mostrano il significato di "usi in pila". Se viene generata un'eccezione dal costruttore AnotherAsyncDisposable, nessuno dei due oggetti viene eliminato correttamente. La variabile objTwo non viene mai assegnata perché il costruttore non è stato completato correttamente. Di conseguenza, il costruttore per AnotherAsyncDisposable è responsabile dell'eliminazione di tutte le risorse allocate prima che generi un'eccezione. Se il tipo ExampleAsyncDisposable ha un finalizzatore, è idoneo per la finalizzazione.

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

Suggerimento

Evitare questo modello perché potrebbe causare un comportamento imprevisto. Se si usa uno dei modelli accettabili, il problema degli oggetti non archiviati non è esistente. Le operazioni di pulizia vengono eseguite correttamente quando le istruzioni using non sono in pila.

Vedi anche

Per un esempio di implementazione dual di IDisposable e IAsyncDisposable, vedere il codice sorgente Utf8JsonWriterin GitHub.