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 Disposemetody 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 publicIAsyncDisposable.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 , _examplektóre jest inicjowane w konstruktorze.
  • Metoda DisposeAsync deleguje do DisposeAsyncCore 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 na null.
  • Metoda DisposeAsyncCore() to virtual, która umożliwia podklasom zastąpienie jej zachowaniem niestandardowym.

Zapieczętowany alternatywny wzorzec usuwania asynchronicznego

Jeśli klasa implementowania może mieć sealedwartość , 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 jest readonly 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 nulljest . 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 nulljest , 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 ConfigureAwaittemat 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 IAsyncDisposableelement .
  • 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 objTwoelement , 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.