Implementacja metody DisposeAsync
Interfejs System.IAsyncDisposable został wprowadzony w ramach języka C# 8.0. Zaimplementujesz metodę IAsyncDisposable.DisposeAsync() , gdy musisz przeprowadzić oczyszczanie zasobów, tak jak podczas implementowania metody Dispose. Jedną z kluczowych różnic jest jednak to, że ta implementacja umożliwia wykonywanie operacji oczyszczania asynchronicznego. Zwraca DisposeAsync() wartość , która reprezentuje operację ValueTask usuwania asynchronicznego.
Zwykle podczas implementowania interfejsu IAsyncDisposable , który klasy implementują IDisposable również interfejs. Dobrym wzorcem implementacji interfejsu IAsyncDisposable jest przygotowanie do synchronicznej lub asynchronicznej usuwania, jednak nie jest to wymagane. Jeśli nie jest możliwe żadne synchroniczne jednorazowe usunięcie klasy, posiadanie tylko IAsyncDisposable jest dopuszczalne. Wszystkie wskazówki dotyczące implementowania wzorca usuwania mają również zastosowanie do implementacji asynchronicznej. W tym artykule założono, że wiesz już, jak zaimplementować metodę Dispose.
Uwaga
Jeśli zaimplementujesz IAsyncDisposable interfejs, ale nie IDisposable interfejs, aplikacja może potencjalnie wyciekać zasobów. Jeśli klasa implementuje metodę IAsyncDisposable, ale nie IDisposable, a użytkownik wywołuje tylko metodę , implementacja nigdy nie wywoła Dispose
metody DisposeAsync
. Spowodowałoby to wyciek zasobów.
Napiwek
W odniesieniu do wstrzykiwania zależności podczas rejestrowania usług w programie IServiceCollectionokres istnienia usługi jest zarządzany niejawnie w Twoim imieniu. I IServiceProvider odpowiednie IHost organizowanie oczyszczania zasobów. W szczególności implementacje IDisposable i IAsyncDisposable są prawidłowo usuwane na koniec określonego okresu istnienia.
Aby uzyskać więcej informacji, zobacz Wstrzykiwanie zależności na platformie .NET.
Eksplorowanie DisposeAsync
i DisposeAsyncCore
metody
Interfejs IAsyncDisposable deklaruje pojedynczą metodę bez parametrów, DisposeAsync(). Każda klasa niesealowana powinna definiować metodę DisposeAsyncCore()
, która zwraca ValueTaskrównież wartość .
Implementacja
public
IAsyncDisposable.DisposeAsync() , która nie ma parametrów.protected virtual ValueTask DisposeAsyncCore()
Metoda, której podpis to:protected virtual ValueTask DisposeAsyncCore() { }
Metoda DisposeAsync
public
Metoda bez DisposeAsync()
parametrów jest wywoływana niejawnie w await using
instrukcji, a jej celem jest zwolnienie niezarządzanych zasobów, przeprowadzenie ogólnego czyszczenia i wskazanie, że finalizator, jeśli taki jest obecny, nie musi działać. Zwalnianie pamięci skojarzonej z obiektem zarządzanym jest zawsze domeną modułu odśmiecania pamięci. Z tego powodu ma standardową implementację:
public async ValueTask DisposeAsync()
{
// Perform async cleanup.
await DisposeAsyncCore();
// Dispose of unmanaged resources.
Dispose(false);
// Suppress finalization.
GC.SuppressFinalize(this);
}
Uwaga
Jedną z podstawowych różnic w wzorcu asynchronicznego usuwania w porównaniu ze wzorcem usuwania jest to, że wywołanie z DisposeAsync() metody Dispose(bool)
przeciążenia jest podane false
jako argument. Jednak podczas implementowania IDisposable.Dispose() metody true
jest przekazywana. Pomaga to zapewnić równoważność funkcjonalną za pomocą synchronicznego wzorca usuwania i dodatkowo gwarantuje, że ścieżki kodu finalizatora nadal są wywoływane. Innymi słowy, DisposeAsyncCore()
metoda będzie usuwać zasoby zarządzane asynchronicznie, więc nie chcesz ich usuwać synchronicznie. W związku z tym wywołaj metodę Dispose(false)
Dispose(true)
zamiast .
Metoda DisposeAsyncCore
Metoda DisposeAsyncCore()
jest przeznaczona do przeprowadzania asynchronicznego czyszczenia zarządzanych zasobów lub kaskadowych wywołań metody .DisposeAsync()
Hermetyzuje ona typowe operacje oczyszczania asynchronicznego, gdy podklasa dziedziczy klasę bazową, która jest implementacją IAsyncDisposableklasy . Metoda DisposeAsyncCore()
jest virtual
taka, aby klasy pochodne mogły definiować czyszczenie niestandardowe w ich przesłonięciach.
Napiwek
Jeśli implementacja IAsyncDisposable metody to sealed
, DisposeAsyncCore()
metoda nie jest potrzebna, a czyszczenie asynchroniczne można wykonać bezpośrednio w metodzie IAsyncDisposable.DisposeAsync() .
Implementowanie wzorca asynchronicznego usuwania
Wszystkie niezwiązane klasy powinny być traktowane jako potencjalna klasa bazowa, ponieważ mogą być dziedziczone. Jeśli zaimplementujesz wzorzec asynchronicznego usuwania dla dowolnej potencjalnej klasy bazowej, musisz podać metodę protected virtual ValueTask DisposeAsyncCore()
. Niektóre z poniższych przykładów używają klasy zdefiniowanej NoopAsyncDisposable
w następujący sposób:
public sealed class NoopAsyncDisposable : IAsyncDisposable
{
ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask;
}
Oto przykładowa implementacja wzorca asynchronicznego usuwania, który używa NoopAsyncDisposable
typu . Typ implementuje DisposeAsync
przez zwrócenie wartości 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;
}
}
W powyższym przykładzie:
- Jest
ExampleAsyncDisposable
to klasa niesealowana, która implementuje IAsyncDisposable interfejs. - Zawiera ono pole prywatne
IAsyncDisposable
,_example
które jest inicjowane w konstruktorze. - Metoda
DisposeAsync
deleguje doDisposeAsyncCore
metody i wywołuje metodę GC.SuppressFinalize w celu powiadomienia modułu odśmieceń pamięci, że finalizator nie musi działać. - Zawiera metodę, która wywołuje metodę
DisposeAsyncCore()
_example.DisposeAsync()
i ustawia pole nanull
. - Metoda
DisposeAsyncCore()
tovirtual
, która umożliwia podklasom zastąpienie jej zachowaniem niestandardowym.
Zapieczętowany alternatywny wzorzec usuwania asynchronicznego
Jeśli klasa implementowania może mieć sealed
wartość , możesz zaimplementować wzorzec asynchronicznego usuwania, przesłaniając metodę IAsyncDisposable.DisposeAsync() . W poniższym przykładzie pokazano, jak zaimplementować wzorzec asynchronicznego usuwania dla zapieczętowanej klasy:
public sealed class SealedExampleAsyncDisposable : IAsyncDisposable
{
private readonly IAsyncDisposable _example;
public SealedExampleAsyncDisposable() =>
_example = new NoopAsyncDisposable();
public ValueTask DisposeAsync() => _example.DisposeAsync();
}
W powyższym przykładzie:
- Jest
SealedExampleAsyncDisposable
to zapieczętowana klasa, która implementuje IAsyncDisposable interfejs. - Pole zawierające
_example
jestreadonly
inicjowane w konstruktorze. - Metoda
DisposeAsync
wywołuje metodę_example.DisposeAsync()
, implementując wzorzec za pośrednictwem pola zawierającego (kaskadowego usuwania).
Implementowanie wzorców usuwania i asynchronicznego usuwania
Może być konieczne zaimplementowanie zarówno interfejsów, jak IDisposable i IAsyncDisposable , zwłaszcza gdy zakres klasy zawiera wystąpienia tych implementacji. Dzięki temu można prawidłowo wyczyścić kaskadowe wywołania. Oto przykładowa klasa, która implementuje oba interfejsy i demonstruje odpowiednie wskazówki dotyczące czyszczenia.
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;
}
}
Implementacje IDisposable.Dispose() i IAsyncDisposable.DisposeAsync() to zarówno prosty standardowy kod.
W metodzie Dispose(bool)
przeciążenia wystąpienie jest warunkowo usuwane, IDisposable jeśli nie null
jest . Wystąpienie IAsyncDisposable jest rzutowane jako IDisposable, a jeśli również nie null
, jest również usuwane. Oba wystąpienia są następnie przypisywane do elementu null
.
W przypadku DisposeAsyncCore()
metody następuje to samo podejście logiczne. IAsyncDisposable Jeśli wystąpienie nie null
jest , jego wywołanie DisposeAsync().ConfigureAwait(false)
jest oczekiwane. IDisposable Jeśli wystąpienie jest również implementacją IAsyncDisposable, jest również usuwane asynchronicznie. Oba wystąpienia są następnie przypisywane do elementu null
.
Każda implementacja dąży do usuwania wszystkich możliwych obiektów jednorazowych. Dzięki temu czyszczenie jest prawidłowo kaskadowe.
Używanie asynchronicznego jednorazowego użytku
Aby prawidłowo korzystać z obiektu, który implementuje IAsyncDisposable interfejs, należy użyć funkcji await i używać słów kluczowych razem. Rozważmy poniższy przykład, w którym ExampleAsyncDisposable
klasa jest utworzona, a następnie opakowana w instrukcję await using
.
class ExampleConfigureAwaitProgram
{
static async Task Main()
{
var exampleAsyncDisposable = new ExampleAsyncDisposable();
await using (exampleAsyncDisposable.ConfigureAwait(false))
{
// Interact with the exampleAsyncDisposable instance.
}
Console.ReadLine();
}
}
Ważne
ConfigureAwait(IAsyncDisposable, Boolean) Użyj metody rozszerzenia interfejsuIAsyncDisposable, aby skonfigurować sposób działania kontynuacji zadania w oryginalnym kontekście lub harmonogramie. Aby uzyskać więcej informacji na ConfigureAwait
temat programu , zobacz ConfigureAwait FAQ (Często zadawane pytania dotyczące konfigurowania elementu ConfigureAwait).
W sytuacjach, w których użycie ConfigureAwait
elementu nie jest potrzebne, instrukcję await using
można uprościć w następujący sposób:
class ExampleUsingStatementProgram
{
static async Task Main()
{
await using (var exampleAsyncDisposable = new ExampleAsyncDisposable())
{
// Interact with the exampleAsyncDisposable instance.
}
Console.ReadLine();
}
}
Ponadto można napisać, aby użyć niejawnego określania zakresu deklaracji using.
class ExampleUsingDeclarationProgram
{
static async Task Main()
{
await using var exampleAsyncDisposable = new ExampleAsyncDisposable();
// Interact with the exampleAsyncDisposable instance.
Console.ReadLine();
}
}
Wiele słów kluczowych await w jednym wierszu
await
Czasami słowo kluczowe może pojawiać się wiele razy w jednym wierszu. Rozważmy na przykład następujący kod:
await using var transaction = await context.Database.BeginTransactionAsync(token);
W powyższym przykładzie:
- Oczekiwana BeginTransactionAsync jest metoda.
- Zwracany typ to DbTransaction, który implementuje
IAsyncDisposable
element . - Element
transaction
jest używany asynchronicznie, a także oczekiwany.
Skumulowane użycie
W sytuacjach, w których tworzysz i używasz wielu obiektów, które implementują IAsyncDisposableprogram , możliwe, że tworzenie instrukcji stosu za ConfigureAwait pomocą polecenia może uniemożliwić DisposeAsync() wywołania await using
w niekonsekwentnych warunkach. Aby upewnić się, że DisposeAsync() jest zawsze wywoływana, należy unikać stosu. W poniższych trzech przykładach kodu przedstawiono dopuszczalne wzorce do użycia.
Akceptowalny wzorzec jeden
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();
}
}
W poprzednim przykładzie każda operacja czyszczenia asynchronicznego jest jawnie ograniczona w await using
bloku. Zakres zewnętrzny jest zgodny ze sposobem objOne
ustawiania nawiasów klamrowych, otaczającego objTwo
element , jako taki objTwo
jest najpierw usuwany, a następnie .objOne
Oba IAsyncDisposable
wystąpienia mają DisposeAsync() oczekiwaną metodę, więc każde wystąpienie wykonuje operację asynchronicznego czyszczenia. Wywołania są zagnieżdżone, a nie skumulowane.
Akceptowalny wzorzec dwa
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();
}
}
W poprzednim przykładzie każda operacja czyszczenia asynchronicznego jest jawnie ograniczona w await using
bloku. Na końcu każdego bloku odpowiednie IAsyncDisposable
wystąpienie ma oczekiwaną DisposeAsync() metodę, wykonując w ten sposób operację asynchronicznego czyszczenia. Wywołania są sekwencyjne, a nie skumulowane. W tym scenariuszu objOne
najpierw jest usuwana, a następnie objTwo
jest usuwana.
Akceptowalny wzorzec trzy
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();
}
}
W poprzednim przykładzie każda operacja asynchronicznego czyszczenia jest niejawnie ograniczona do treści zawierającej metodę. Na końcu otaczającego bloku IAsyncDisposable
wystąpienia wykonują operacje asynchronicznego czyszczenia. Ten przykład działa w odwrotnej kolejności, z której zostały zadeklarowane, co oznacza, że objTwo
jest usuwany przed objOne
.
Wzorzec niedopuszczalny
Wyróżnione wiersze w poniższym kodzie pokazują, co to znaczy, że "skumulowane użycie". Jeśli wyjątek jest zgłaszany z konstruktora AnotherAsyncDisposable
, żaden obiekt nie jest prawidłowo usuwany. Zmienna objTwo
nigdy nie jest przypisana, ponieważ konstruktor nie zakończył się pomyślnie. W związku z tym konstruktor jest AnotherAsyncDisposable
odpowiedzialny za dysponowanie wszelkich przydzielonych zasobów przed zgłoszeniem wyjątku. ExampleAsyncDisposable
Jeśli typ ma finalizator, kwalifikuje się do finalizacji.
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();
}
}
Napiwek
Unikaj tego wzorca, ponieważ może to prowadzić do nieoczekiwanego zachowania. Jeśli używasz jednego z akceptowalnych wzorców, problem niedysponowanych obiektów nie istnieje. Operacje czyszczenia są prawidłowo wykonywane, gdy using
instrukcje nie są ułożone.
Zobacz też
Aby zapoznać się z przykładem podwójnej implementacji IDisposable
i IAsyncDisposable
, zobacz Utf8JsonWriter kod źródłowy w witrynie GitHub.