Implementace metody DisposeAsync

Rozhraní System.IAsyncDisposable bylo zavedeno jako součást C# 8.0. Metodu IAsyncDisposable.DisposeAsync() implementujete, když potřebujete provést vyčištění prostředků, stejně jako při implementaci metody Dispose. Jedním z klíčových rozdílů však je, že tato implementace umožňuje asynchronní operace čištění. Vrátí DisposeAsync() hodnotu ValueTask , která představuje asynchronní operaci odstranění.

Je typické při implementaci IAsyncDisposable rozhraní, které třídy také implementují IDisposable rozhraní. Dobrým vzorem IAsyncDisposable implementace rozhraní je příprava na synchronní nebo asynchronní odstranění, ale není to požadavek. Pokud není možné žádnou synchronní uvolnitelnou třídu, je přijatelné pouze IAsyncDisposable to, že je to možné. Všechny pokyny pro implementaci modelu odstranění platí také pro asynchronní implementaci. Tento článek předpokládá, že už víte, jak implementovat metodu Dispose.

Upozornění

Pokud implementujete IAsyncDisposable rozhraní, ale ne IDisposable rozhraní, může vaše aplikace potenciálně uniknout prostředkům. Pokud třída implementuje IAsyncDisposable, ale ne IDisposable, a příjemce pouze volá Dispose, vaše implementace by nikdy volání DisposeAsync. To by vedlo k úniku prostředků.

Tip

Pokud jde o injektáž závislostí, při registraci služeb v rámci IServiceCollectionslužby je životnost služby spravována implicitně vaším jménem. Vyčištění IServiceProvider prostředků a odpovídající IHost orchestrace. Konkrétně implementace IDisposable a IAsyncDisposable jsou řádně uvolněny na konci jejich zadané životnosti.

Další informace naleznete v tématu Injektáž závislostí v .NET.

Prozkoumání DisposeAsync a DisposeAsyncCore metody

Rozhraní IAsyncDisposable deklaruje jednu metodu bez parametrů , DisposeAsync(). Každá nezapečetěná třída by měla definovat metodu DisposeAsyncCore()ValueTask, která také vrací .

  • Implementace publicIAsyncDisposable.DisposeAsync() , která nemá žádné parametry.

  • Metoda protected virtual ValueTask DisposeAsyncCore() , jejíž podpis je:

    protected virtual ValueTask DisposeAsyncCore()
    {
    }
    

Metoda DisposeAsync

Metoda public bez DisposeAsync() parametrů je volána implicitně v await using příkazu a jeho účelem je uvolnit nespravované prostředky, provést obecné vyčištění a indikovat, že finalizátor, pokud existuje, nemusí být spuštěn. Uvolnění paměti přidružené ke spravovanému objektu je vždy doménou uvolňování paměti. Z tohoto důvodu má standardní implementaci:

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

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

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

Poznámka:

Jedním z primárních rozdílů ve vzoru async dispose ve srovnání se vzorem dispose je, že volání metody DisposeAsync()Dispose(bool) přetížení je dáno false jako argument. Při implementaci IDisposable.Dispose() metody se však true předá. To pomáhá zajistit funkční ekvivalenci se vzorem synchronní dispose a dále zajišťuje, že se cesty kódu finalizátoru stále vyvolávají. Jinými slovy, DisposeAsyncCore() metoda odstraní spravované prostředky asynchronně, takže je nechcete likvidovat synchronně také. Proto místo volání Dispose(false)Dispose(true).

Metoda DisposeAsyncCore

Metoda DisposeAsyncCore() je určena k provedení asynchronního čištění spravovaných prostředků nebo pro kaskádové volání DisposeAsync(). Zapouzdřuje běžné asynchronní operace čištění, když podtřída dědí základní třídu, která je implementací IAsyncDisposable. Metoda DisposeAsyncCore() je virtual tak, aby odvozené třídy mohly definovat vlastní vyčištění v jejich přepsání.

Tip

Pokud je sealedimplementace IAsyncDisposable , DisposeAsyncCore() metoda není nutná a asynchronní vyčištění lze provést přímo v IAsyncDisposable.DisposeAsync() metodě.

Implementace vzoru async dispose

Všechny nezapečetěné třídy by měly být považovány za potenciální základní třídu, protože by mohly být zděděné. Pokud implementujete vzor async dispose pro libovolnou potenciální základní třídu, musíte zadat metodu protected virtual ValueTask DisposeAsyncCore() . Některé z následujících příkladů používají NoopAsyncDisposable třídu, která je definována takto:

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

Tady je příklad implementace vzoru async dispose, který používá typ NoopAsyncDisposable . Typ implementuje DisposeAsync vrácením 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;
    }
}

V předchozím příkladu:

  • Je ExampleAsyncDisposable nezapečetěná třída, která implementuje IAsyncDisposable rozhraní.
  • Obsahuje soukromé IAsyncDisposable pole, _examplekteré je inicializováno v konstruktoru.
  • Metoda DisposeAsync deleguje na metodu DisposeAsyncCore a volání GC.SuppressFinalize , která upozorní systém uvolňování paměti, že finalizátor nemusí běžet.
  • Obsahuje metodu DisposeAsyncCore() , která volá metodu _example.DisposeAsync() , a nastaví pole na null.
  • Metoda DisposeAsyncCore() je virtual, která umožňuje podtřídy přepsat ho vlastním chováním.

Zapečetěný alternativní vzor async dispose

Pokud vaše implementovat třídu může být sealed, můžete implementovat asynchronní dispose vzor přepsáním IAsyncDisposable.DisposeAsync() metody. Následující příklad ukazuje, jak implementovat vzor async dispose pro zapečetěnou třídu:

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

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

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

V předchozím příkladu:

  • Je SealedExampleAsyncDisposable zapečetěná třída, která implementuje IAsyncDisposable rozhraní.
  • Pole obsahující _example je readonly a inicializuje se v konstruktoru.
  • Metoda DisposeAsync volá metodu _example.DisposeAsync() , implementuje vzor prostřednictvím obsahujícího pole (kaskádové odstranění).

Implementace vzorů dispose i async dispose

Možná budete muset implementovat rozhraní IDisposable i IAsyncDisposable rozhraní, zejména pokud obor třídy obsahuje instance těchto implementací. Tím zajistíte, že můžete správně kaskádně vyčistit volání. Tady je příklad třídy, která implementuje obě rozhraní a ukazuje správné pokyny pro vyčištění.

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

IAsyncDisposable.DisposeAsync() Obě implementace IDisposable.Dispose() jsou jednoduchý často používaný kód.

Dispose(bool) V metodě IDisposable přetížení je instance podmíněně uvolněna, pokud není null. Instance IAsyncDisposable je přetypována jako IDisposable, a pokud také není null, je odstraněna také. Obě instance jsou pak přiřazeny .null

S metodou DisposeAsyncCore() se postupuje stejně jako logický přístup. IAsyncDisposable Pokud instance nenínull, čeká se DisposeAsync().ConfigureAwait(false) její volání. IDisposable Pokud je instance také implementací IAsyncDisposable, je také uvolněna asynchronně. Obě instance jsou pak přiřazeny .null

Každá implementace se snaží likvidovat všechny možné uvolnitelné objekty. Tím se zajistí, že se vyčištění správně kaskáduje.

Použití asynchronního jednorázového použití

Chcete-li správně využívat objekt, který implementuje IAsyncDisposable rozhraní, použijete příkaz await a použijete klíčová slova společně. Podívejte se na následující příklad, kde ExampleAsyncDisposable je třída vytvořena instance a pak zabalena do await using příkazu.

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

        Console.ReadLine();
    }
}

Důležité

ConfigureAwait(IAsyncDisposable, Boolean) Pomocí rozšiřující metody IAsyncDisposable rozhraní můžete nakonfigurovat, jak se pokračování úkolu zařadí do původního kontextu nebo plánovače. Další informace najdete v ConfigureAwaittématu Nejčastější dotazy ke konfiguraci Await.

V situacích, kdy není použití ConfigureAwait potřeba, await using může být příkaz zjednodušen takto:

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

        Console.ReadLine();
    }
}

Kromě toho může být zapsána tak, aby používala implicitní určení rozsahu deklarace using.

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

        // Interact with the exampleAsyncDisposable instance.

        Console.ReadLine();
    }
}

Několik klíčových slov await v jednom řádku

Někdy se await klíčové slovo může objevit několikrát v jednom řádku. Představte si například následující kód:

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

V předchozím příkladu:

  • Metoda BeginTransactionAsync je očekávána.
  • Návratový typ je DbTransaction, který implementuje IAsyncDisposable.
  • Používá se transaction asynchronně a také očekává.

Skládané použití

V situacích, kdy vytváříte a používáte více objektů, které implementují IAsyncDisposable, je možné, že příkazy stackingu await using s ConfigureAwait můžou bránit volání DisposeAsync() v chybných podmínkách. Pokud chcete zajistit, aby DisposeAsync() se vždy volala, měli byste se vyhnout vytváření zásobníků. Následující tři příklady kódu ukazují přijatelné vzory pro použití.

Přijatelný vzor 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();
    }
}

V předchozím příkladu je každá asynchronní operace čištění explicitně vymezena pod blokem await using . Vnější obor se řídí tím, jak objOne nastaví jeho složené závorky, ohraničující objTwo, jako takový objTwo je uvolněn jako první, následovaný objOne. Obě IAsyncDisposable instance mají DisposeAsync() očekávanou metodu, takže každá instance provádí svou asynchronní operaci čištění. Volání jsou vnořená, ne skládaná.

Přijatelný vzor 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();
    }
}

V předchozím příkladu je každá asynchronní operace čištění explicitně vymezena pod blokem await using . Na konci každého bloku má odpovídající IAsyncDisposable instance svou DisposeAsync() metodu očekávanou, a proto provádí asynchronní operaci čištění. Volání jsou sekvenční, ne skládaná. V tomto scénáři objOne se nejprve odstraní a pak objTwo se vyhodí.

Přijatelný vzor 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();
    }
}

V předchozím příkladu je každá asynchronní operace čištění implicitně vymezena tělem obsahující metodu. Na konci ohraničujícího bloku IAsyncDisposable instance provádějí své asynchronní operace čištění. Tento příklad se spouští v obráceném pořadí, ze kterého byly deklarovány, což znamená, že objTwo je uvolněn před objOne.

Nepřijatelný vzor

Zvýrazněné řádky v následujícím kódu ukazují, co znamená mít "skládané použití". Pokud je vyvolána výjimka z konstruktoru AnotherAsyncDisposable , není žádný objekt správně odstraněn. Proměnná objTwo se nikdy nepřiřazuje, protože konstruktor nebyl úspěšně dokončen. V důsledku toho je konstruktor AnotherAsyncDisposable zodpovědný za likvidaci všech prostředků přidělených před vyvolání výjimky. Pokud má ExampleAsyncDisposable typ finalizátor, je způsobilý k dokončení.

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

Tip

Vyhněte se tomuto vzoru, protože by to mohlo vést k neočekávanému chování. Pokud použijete některý z přijatelných vzorů, problém nedispozovaných objektů neexistuje. Operace čištění se správně provádějí, když using nejsou příkazy naskládané.

Viz také

Příklad IDisposable duální implementace a IAsyncDisposablepodívejte se na Utf8JsonWriter zdrojový kód na GitHubu.