Implementieren einer DisposeAsync-Methode
Dies System.IAsyncDisposable-Schnittstelle wurde als Teil von C# 8.0 eingeführt. Sie implementieren die IAsyncDisposable.DisposeAsync()-Methode, wenn Sie eine Ressourcenbereinigung durchführen müssen, genauso wie bei der Implementierung einer Dispose-Methode. Einer der Hauptunterschiede besteht jedoch darin, dass diese Implementierung asynchrone Bereinigungsvorgänge zulässt. DisposeAsync() gibt eine ValueTask zurück, die den asynchronen Bereinigungsvorgang darstellt.
Bei der Implementierung der IAsyncDisposable-Schnittstelle implementieren die Klassen in der Regel auch die IDisposable-Schnittstelle. Ein gutes Implementierungsmuster der IAsyncDisposable-Schnittstelle besteht darin, entweder für das synchrone oder asynchrone Bereinigen vorbereitet zu sein, dies ist jedoch keine Voraussetzung. Ist kein synchrones Verwerfen Ihrer Klasse möglich, ist es akzeptabel, wenn nur IAsyncDisposable verfügbar ist. Alle Leitfäden zum Implementieren des Bereinigungsmusters gelten auch für die asynchrone Implementierung. In diesem Artikel wird davon ausgegangen, dass Sie bereits mit der Implementierung einer Dispose-Methode vertraut sind.
Achtung
Wenn Sie die Schnittstelle IAsyncDisposable, aber nicht die Schnittstelle IDisposable implementieren, kann es in Ihrer App möglicherweise zu Ressourcenverlusten kommen. Wenn eine Klasse IAsyncDisposable implementiert, aber nicht IDisposable, und ein Consumer nur Dispose
aufruft, würde Ihre Implementierung niemals DisposeAsync
aufrufen. Dies würde zu Ressourcenverlusten führen.
Tipp
In Bezug auf die Abhängigkeitsinjektion (DI) wird die Dienstlebensdauer beim Registrieren von Diensten in einer IServiceCollection implizit in Ihrem Namen verwaltet. Der IServiceProvider und die entsprechende IHost-Orchestrierungsressourcenbereinigung. Insbesondere Implementierungen von IDisposable und IAsyncDisposable werden am Ende ihrer angegebenen Lebensdauer ordnungsgemäß gelöscht.
Weitere Informationen finden Sie unter Abhängigkeitsinjektion in .NET.
Erkunden der DisposeAsync
- und DisposeAsyncCore
-Methoden
Die IAsyncDisposable-Schnittstelle deklariert eine einzelne parameterlose Methode: DisposeAsync(). Jede nicht versiegelte Klasse sollte eine DisposeAsyncCore()
-Methode definieren, die auch einen ValueTask zurückgibt.
Eine
public
IAsyncDisposable.DisposeAsync()-Implementierung, die keine Parameter aufweist.Eine
protected virtual ValueTask DisposeAsyncCore()
-Methode mit der folgenden Signatur:protected virtual ValueTask DisposeAsyncCore() { }
Die DisposeAsync
-Methode
Die public
parameterlose DisposeAsync()
-Methode wird implizit in einer await using
-Anweisung aufgerufen, und ihr Zweck ist es, nicht verwaltete Ressourcen freizugeben, eine generelle Bereinigung durchzuführen und anzugeben, dass der Finalizer, sofern vorhanden, nicht ausgeführt werden muss. Das Freigeben des Speichers, der einem verwalteten Objekt zugeordnet ist, ist immer die Domäne des Garbage Collectors. Daher weist sie eine Standardimplementierung auf:
public async ValueTask DisposeAsync()
{
// Perform async cleanup.
await DisposeAsyncCore();
// Dispose of unmanaged resources.
Dispose(false);
// Suppress finalization.
GC.SuppressFinalize(this);
}
Hinweis
Ein primärer Unterschied im asynchronen Dispose-Muster im Vergleich zum Dispose-Muster besteht darin, dass dem Aufruf von DisposeAsync() an die Dispose(bool)
-Überladungsmethode false
als Argument übergeben wird. Beim Implementieren der IDisposable.Dispose()-Methode wird jedoch stattdessen true
übergeben. Dadurch wird die funktionale Äquivalenz mit dem synchronen Dispose-Muster sichergestellt, und es wird weiterhin sichergestellt, dass Finalizer-Codepfade auch noch aufgerufen werden. Mit anderen Worten: Die DisposeAsyncCore()
-Methode gibt verwaltete Ressourcen asynchron frei, sodass Sie diese nicht auch noch synchron löschen sollten. Rufen Sie daher Dispose(false)
anstelle von Dispose(true)
auf.
Die DisposeAsyncCore
-Methode
Die DisposeAsyncCore()
-Methode ist dafür vorgesehen, die asynchrone Bereinigung verwalteter Ressourcen oder kaskadierende Aufrufe von DisposeAsync()
auszuführen. Sie kapselt die allgemeinen asynchronen Bereinigungsvorgänge, wenn eine Unterklasse eine Basisklasse erbt, die eine Implementierung von IAsyncDisposable ist. Die DisposeAsyncCore()
-Methode ist virtual
, damit abgeleitete Klassen benutzerdefinierte Bereinigungen in ihren Außerkraftsetzungen definieren können.
Tipp
Wenn eine Implementierung von IAsyncDisposablesealed
ist, wird die DisposeAsyncCore()
-Methode nicht benötigt, und die asynchrone Bereinigung kann direkt in der IAsyncDisposable.DisposeAsync()-Methode ausgeführt werden.
Implementieren des asynchronen Dispose-Musters
Alle nicht versiegelten Klassen sollten als potenzielle Basisklasse angesehen werden, da sie geerbt werden könnten. Wenn Sie das asynchrone Dispose-Muster für eine potenzielle Basisklasse implementieren, müssen Sie die protected virtual ValueTask DisposeAsyncCore()
-Methode bereitstellen. Einige der folgenden Beispiele verwenden eine NoopAsyncDisposable
-Klasse, die wie folgt definiert ist:
public sealed class NoopAsyncDisposable : IAsyncDisposable
{
ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask;
}
Im Folgenden finden Sie eine Beispielimplementierung des asynchronen Bereinigungsmusters, das den Typ NoopAsyncDisposable
verwendet. Der Typ implementiert DisposeAsync
, indem ValueTask.CompletedTask zurückgegeben wird.
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;
}
}
Im vorherigen Beispiel:
ExampleAsyncDisposable
ist eine nicht versiegelte Klasse, die die IAsyncDisposable-Schnittstelle implementiert.- Sie enthält ein privates
IAsyncDisposable
-Feld_example
, das im Konstruktor initialisiert wird. - Die
DisposeAsync
-Methode delegiert an dieDisposeAsyncCore
-Methode und ruft GC.SuppressFinalize auf, um den Garbage Collector zu benachrichtigen, dass der Finalizer nicht ausgeführt werden muss. - Sie enthält eine
DisposeAsyncCore()
-Methode, die die_example.DisposeAsync()
-Methode aufruft, und legt das Feld aufnull
fest. - Die
DisposeAsyncCore()
-Methode istvirtual
, damit Unterklassen sie mit benutzerdefiniertem Verhalten außer Kraft setzen können.
Versiegeltes alternatives asynchrones Dispose-Muster
Wenn Ihre implementierende Klasse sealed
sein kann, können Sie das asynchrone Dispose-Muster implementieren, indem Sie die IAsyncDisposable.DisposeAsync()-Methode außer Kraft setzen. Das folgende Beispiel zeigt, wie das asynchrone Dispose-Muster für eine versiegelte Klasse implementiert wird:
public sealed class SealedExampleAsyncDisposable : IAsyncDisposable
{
private readonly IAsyncDisposable _example;
public SealedExampleAsyncDisposable() =>
_example = new NoopAsyncDisposable();
public ValueTask DisposeAsync() => _example.DisposeAsync();
}
Im vorherigen Beispiel:
SealedExampleAsyncDisposable
ist eine versiegelte Klasse, die die IAsyncDisposable-Schnittstelle implementiert.- Das enthaltende
_example
-Feld istreadonly
und wird im Konstruktor initialisiert. - Die
DisposeAsync
-Methode ruft die_example.DisposeAsync()
-Methode auf und implementiert das Muster über das enthaltende Feld (kaskadierendes Verwerfen).
Implementieren von Dispose-Mustern und asynchronen Dispose-Mustern
Möglicherweise müssen Sie sowohl die IDisposable- als auch die IAsyncDisposable-Schnittstelle implementieren, insbesondere wenn der Klassenbereich Instanzen dieser Implementierungen enthält. Dadurch wird sichergestellt, dass Bereinigungsaufrufe ordnungsgemäß kaskadiert werden können. Im Folgenden finden Sie eine Beispielklasse, die beide Schnittstellen implementiert und die richtige Vorgehensweise beim Bereinigen veranschaulicht.
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;
}
}
Bei der IDisposable.Dispose()- und der IAsyncDisposable.DisposeAsync()-Implementierung handelt es sich um einfache Codebausteine.
In der Überladungsmethode Dispose(bool)
wird die IDisposable-Instanz bedingt bereinigt, wenn sie nicht den Wert null
aufweist. Die IAsyncDisposable-Instanz wird in IDisposable umgewandelt. Wenn der Wert dieser Instanz auch nicht null
ist, wird sie ebenfalls bereinigt. Beiden Instanzen wird dann der Wert null
zugewiesen.
Bei der DisposeAsyncCore()
-Methode wird der gleiche logische Ansatz verfolgt. Wenn die IAsyncDisposable-Instanz nicht null
ist, wird auf ihren Aufruf von DisposeAsync().ConfigureAwait(false)
gewartet. Wenn die IDisposable-Instanz auch eine Implementierung von IAsyncDisposable ist, wird sie auch asynchron verworfen. Beiden Instanzen wird dann der Wert null
zugewiesen.
Jede Implementierung ist bestrebt, alle möglichen verwerfbaren Objekte zu löschen. Dadurch wird sichergestellt, dass die Bereinigung ordnungsgemäß kaskadiert wird.
Verwenden von asynchron verwerfbar
Wenn Sie ein Objekt ordnungsgemäß nutzen können möchten, das die IAsyncDisposable-Schnittstelle implementiert, verwenden Sie die Schlüsselwörter await und using zusammen. Sehen Sie sich das folgende Beispiel an, in dem die ExampleAsyncDisposable
-Klasse instanziiert und dann von einer await using
-Anweisung umschlossen wird.
class ExampleConfigureAwaitProgram
{
static async Task Main()
{
var exampleAsyncDisposable = new ExampleAsyncDisposable();
await using (exampleAsyncDisposable.ConfigureAwait(false))
{
// Interact with the exampleAsyncDisposable instance.
}
Console.ReadLine();
}
}
Wichtig
Verwenden Sie die ConfigureAwait(IAsyncDisposable, Boolean)-Erweiterungsmethode der IAsyncDisposable-Schnittstelle, um zu konfigurieren, wie die Fortsetzung der Aufgabe in ihrem ursprünglichen Kontext oder Scheduler gemarshallt wird. Weitere Informationen zu ConfigureAwait
finden Sie in den Häufig gestellten Fragen zu ConfigureAwait.
In Situationen, in denen die Verwendung von ConfigureAwait
nicht erforderlich ist, könnte die await using
-Anweisung wie folgt vereinfacht werden:
class ExampleUsingStatementProgram
{
static async Task Main()
{
await using (var exampleAsyncDisposable = new ExampleAsyncDisposable())
{
// Interact with the exampleAsyncDisposable instance.
}
Console.ReadLine();
}
}
Darüber hinaus könnte sie so geschrieben werden, dass Sie den impliziten Bereich einer using-Deklaration verwendet.
class ExampleUsingDeclarationProgram
{
static async Task Main()
{
await using var exampleAsyncDisposable = new ExampleAsyncDisposable();
// Interact with the exampleAsyncDisposable instance.
Console.ReadLine();
}
}
Mehrere await-Schlüsselwörter in einer einzelnen Zeile
Manchmal kann das Schlüsselwort await
mehrmals innerhalb einer einzelnen Zeile angezeigt werden. Beachten Sie z. B. folgenden Code:
await using var transaction = await context.Database.BeginTransactionAsync(token);
Im vorherigen Beispiel:
- Die BeginTransactionAsync-Methode wird erwartet.
- Der Rückgabetyp ist DbTransaction, der
IAsyncDisposable
implementiert. - Die
transaction
wird asynchron verwendet und ebenfalls erwartet.
Gestapelte using-Anweisungen
In Situationen, in denen Sie mehrere Objekte erstellen und verwenden, die IAsyncDisposable implementieren, kann das Stapeln von await using
-Anweisungen mit ConfigureAwait in fehlgeleiteten Bedingungen Aufrufe von DisposeAsync() verhindern. Sie sollten das Stapeln vermeiden, um sicherzustellen, dass DisposeAsync() immer aufgerufen wird. Die folgenden drei Codebeispiele zeigen akzeptable Muster, die stattdessen verwendet werden können.
Akzeptables Muster 1
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();
}
}
Im vorherigen Beispiel ist der Bereich für die einzelnen asynchronen Bereinigungsvorgänge jeweils explizit unter dem await using
-Block festgelegt. Der äußere Bereich wird dadurch definiert, wie objOne
Klammern setzt und dabei objTwo
umschließt. Insofern wird also zuerst objTwo
und danach objOne
bereinigt. Beide IAsyncDisposable
-Instanzen haben ihre DisposeAsync()-Methode erwartet, sodass jede Instanz ihren asynchronen Bereinigungsvorgang ausführt. Die Aufrufe werden geschachtelt, nicht gestapelt.
Akzeptables Muster 2
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();
}
}
Im vorherigen Beispiel ist der Bereich für die einzelnen asynchronen Bereinigungsvorgänge jeweils explizit unter dem await using
-Block festgelegt. Am Ende jedes Blocks wartet die entsprechende IAsyncDisposable
-Instanz auf ihre DisposeAsync()-Methode, führt also den asynchronen Bereinigungsvorgang durch. Die Aufrufe erfolgen sequenziell, nicht gestapelt. In diesem Szenario wird zunächst objOne
und dann objTwo
gelöscht.
Akzeptables Muster 3
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();
}
}
Im vorherigen Beispiel wird der Bereich für die einzelnen asynchronen Bereinigungsvorgänge implizit mit dem Methodenkörper festgelegt, in dem die Vorgänge jeweils enthalten sind. Am Ende des umschließenden Blocks führen die IAsyncDisposable
-Instanzen die asynchronen Bereinigungsvorgänge durch. Die Ausführung im Beispiel erfolgt in umgekehrter Reihenfolge dazu, wie sie deklariert wurden, d. h. objTwo
wird vor objOne
bereinigt.
Unzulässiges Muster
Die hervorgehobenen Zeilen im folgenden Code zeigen, was es bedeutet, dass „gestapelte using-Instanzen“ verwendet werden. Wenn eine Ausnahme aus dem AnotherAsyncDisposable
-Konstruktor ausgelöst wird, wird kein Objekt ordnungsgemäß verworfen. Die Variable objTwo
wird nie zugewiesen, da der Konstruktor nicht erfolgreich abgeschlossen wurde. Folglich ist der Konstruktor für AnotherAsyncDisposable
dafür verantwortlich, alle zugeordneten Ressourcen zu verwerfen, bevor er eine Ausnahme auslöst. Wenn der ExampleAsyncDisposable
-Typ über einen Finalizer verfügt, ist er zur Finalisierung berechtigt.
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();
}
}
Tipp
Vermeiden Sie dieses Muster, da es zu unerwartetem Verhalten führen kann. Wenn Sie eines der akzeptablen Muster verwenden, gibt es das Problem der nicht verworfenen Objekte nicht. Die Bereinigungsvorgänge werden ordnungsgemäß ausgeführt, wenn using
-Anweisungen nicht gestapelt werden.
Weitere Informationen
Ein Beispiel für eine Implementierung von IDisposable
und IAsyncDisposable
finden Sie im Quellcode von Utf8JsonWriterauf GitHub.