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 dei 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
public
IAsyncDisposable.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 metodoDisposeAsyncCore
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 sunull
. - 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 Utf8JsonWriter in GitHub.