Реализация метода DisposeAsync
Интерфейс System.IAsyncDisposable впервые появился в составе C# 8.0. Метод IAsyncDisposable.DisposeAsync() реализуется, когда нужно выполнить очистку ресурса. Для этих целей также реализуется метод Dispose. Однако одним из ключевых различий является то, что эта реализация позволяет выполнять асинхронные операции очистки. Возвращает объект DisposeAsync() , ValueTask представляющий асинхронную операцию удаления.
Обычно при реализации IAsyncDisposable интерфейса, который классы также реализуют IDisposable интерфейс. Хороший шаблон IAsyncDisposable реализации интерфейса заключается в том, чтобы быть готовым к синхронному или асинхронному удалению, однако это не обязательно. Если синхронный удаленный класс недоступен, IAsyncDisposable он может быть допустимым. Все рекомендации по реализации шаблона удаления также применяются к асинхронной реализации. В этой статье предполагается, что вы уже знаете, как реализовать метод Dispose.
Внимание
Если вы реализуете IAsyncDisposable IDisposable интерфейс, но не интерфейс, приложение может потенциально утечки ресурсов. Если класс реализует IAsyncDisposable, но не IDisposableвызывает Dispose
только потребитель, реализация никогда не будет вызываться DisposeAsync
. Это приведет к утечке ресурсов.
Совет
Что касается внедрения зависимостей, при регистрации служб в ней IServiceCollectionвремя существования службы управляется неявно от вашего имени. Очистка и соответствующая IServiceProvider IHost очистка ресурсов оркестрации. В частности, реализации IDisposable и IAsyncDisposable правильно удаляются в конце указанного времени существования.
Дополнительные сведения см. в статье Внедрение зависимостей в .NET.
Изучение DisposeAsync
и DisposeAsyncCore
методы
Интерфейс IAsyncDisposable объявляет единственный метод без параметров — DisposeAsync(). Любой DisposeAsyncCore()
незаверованный класс должен определять метод, который также возвращает ValueTaskобъект.
Реализация метода IAsyncDisposable.DisposeAsync() с атрибутом
public
без параметров.Метод
protected virtual ValueTask DisposeAsyncCore()
со следующей сигнатурой:protected virtual ValueTask DisposeAsyncCore() { }
Метод DisposeAsync
Метод DisposeAsync()
с атрибутом public
без параметров вызывается неявно в инструкции await using
, и его назначение состоит в том, чтобы освободить неуправляемые ресурсы, выполнить общую очистку и указать, что метод завершения, если он задан, не нужно выполнять. Освобождение памяти, связанной с управляемым объектом, всегда оставляется сборщику мусора. Он имеет стандартную реализацию:
public async ValueTask DisposeAsync()
{
// Perform async cleanup.
await DisposeAsyncCore();
// Dispose of unmanaged resources.
Dispose(false);
// Suppress finalization.
GC.SuppressFinalize(this);
}
Примечание.
Основным отличием шаблона асинхронного освобождения от шаблона освобождения является то, что когда из метода DisposeAsync() выполняется вызов метода перегрузки Dispose(bool)
, в качестве аргумента передается значение false
. В то же время при реализации метода IDisposable.Dispose() вместо него передается значение true
. Это помогает обеспечить функциональную эквивалентность с шаблоном синхронного освобождения, а также гарантирует, что пути кода метода завершения по-прежнему вызываются. Другими словами, метод DisposeAsyncCore()
будет освобождать управляемые ресурсы асинхронно, поэтому вы не захотите, чтобы они также освобождались и синхронно. Следовательно, вызывайте Dispose(false)
вместо Dispose(true)
.
Метод DisposeAsyncCore
Метод DisposeAsyncCore()
предназначен для выполнения асинхронной очистки управляемых ресурсов или для каскадных вызовов DisposeAsync()
. Он инкапсулирует общие асинхронные операции очистки, когда подкласс наследует базовый класс, который является реализацией IAsyncDisposable. Метод DisposeAsyncCore()
заключается в том virtual
, чтобы производные классы могли определять настраиваемую очистку в их переопределениях.
Совет
Если реализация IAsyncDisposable — sealed
, то метод DisposeAsyncCore()
не требуется, а асинхронная очистка может выполняться непосредственно в методе IAsyncDisposable.DisposeAsync().
Реализация шаблона асинхронного освобождения
Все нерегламентированные классы должны рассматриваться как потенциальный базовый класс, так как они могут быть унаследованы. При реализации шаблона асинхронного освобождения для любого потенциального базового класса вы должны предоставить метод protected virtual ValueTask DisposeAsyncCore()
. В некоторых из следующих примеров используется класс, определенный NoopAsyncDisposable
следующим образом:
public sealed class NoopAsyncDisposable : IAsyncDisposable
{
ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask;
}
Ниже приведен пример реализации асинхронного шаблона удаления, использующего NoopAsyncDisposable
тип. Тип реализуется DisposeAsync
путем 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;
}
}
В предыдущем примере:
- Это
ExampleAsyncDisposable
ненаправленный класс, реализующий IAsyncDisposable интерфейс. - Он содержит частное
IAsyncDisposable
поле,_example
которое инициализировано в конструкторе. - Метод
DisposeAsync
делегируетDisposeAsyncCore
метод и вызывает GC.SuppressFinalize , чтобы уведомить сборщика мусора о том, что методу завершения не нужно выполняться. - Он содержит
DisposeAsyncCore()
метод, который вызывает_example.DisposeAsync()
метод, и задает для поля значениеnull
. - Этот
DisposeAsyncCore()
метод позволяетvirtual
подклассам переопределять его с помощью пользовательского поведения.
Запечатанный альтернативный асинхронный шаблон удаления
Если вы можете sealed
реализовать класс, можно реализовать шаблон асинхронного удаления, переопределив IAsyncDisposable.DisposeAsync() метод. В следующем примере показано, как реализовать шаблон асинхронного удаления для запечатаемого класса:
public sealed class SealedExampleAsyncDisposable : IAsyncDisposable
{
private readonly IAsyncDisposable _example;
public SealedExampleAsyncDisposable() =>
_example = new NoopAsyncDisposable();
public ValueTask DisposeAsync() => _example.DisposeAsync();
}
В предыдущем примере:
- Это
SealedExampleAsyncDisposable
запечатанный класс, реализующий IAsyncDisposable интерфейс. _example
Содержащее поле инициализированоreadonly
в конструкторе.- Метод
DisposeAsync
вызывает_example.DisposeAsync()
метод, реализуя шаблон через содержащее поле (каскадное удаление).
Реализация шаблонов освобождения и асинхронного освобождения
Может потребоваться реализовать оба интерфейса IDisposable и IAsyncDisposable, особенно если область класса содержит экземпляры этих реализаций. Это гарантирует правильную каскадную очистку вызовов. Ниже приведен пример класса, реализующего оба интерфейса и демонстрирующий надлежащее руководство по очистке.
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;
}
}
Реализации IDisposable.Dispose() и IAsyncDisposable.DisposeAsync() являются простым шаблонным кодом.
В методе перегрузки Dispose(bool)
экземпляр условно удаляется, IDisposable если это не null
так. Экземпляр IAsyncDisposable создается как IDisposable, и если он также не null
является, он также удален. Затем оба экземпляра назначаются null
.
В методе DisposeAsyncCore()
используется один и тот же логический подход. IAsyncDisposable Если экземпляр не null
является, его вызов DisposeAsync().ConfigureAwait(false)
ожидается. Если экземпляр IDisposable также является реализацией IAsyncDisposable, он также освобождается асинхронно. Затем оба экземпляра назначаются null
.
Каждая реализация стремится удалить все возможные удаленные объекты. Это гарантирует правильность очистки.
Использование интерфейса асинхронного высвобождения
Чтобы правильно использовать объект, который реализует интерфейс IAsyncDisposable, следует использовать ключевые слова await и using вместе. Рассмотрим следующий пример. В нем создается экземпляр класса ExampleAsyncDisposable
, который затем заключается в инструкцию await using
.
class ExampleConfigureAwaitProgram
{
static async Task Main()
{
var exampleAsyncDisposable = new ExampleAsyncDisposable();
await using (exampleAsyncDisposable.ConfigureAwait(false))
{
// Interact with the exampleAsyncDisposable instance.
}
Console.ReadLine();
}
}
Внимание
Для настройки порядка маршалирования продолжения задачи в ее исходном контексте или в планировщике используйте метод расширения ConfigureAwait(IAsyncDisposable, Boolean) интерфейса IAsyncDisposable. Дополнительные сведения о методе ConfigureAwait
см. в разделе Вопросы и ответы по ConfigureAwait.
В ситуациях, когда использование ConfigureAwait
не требуется, await using
инструкция может быть упрощена следующим образом:
class ExampleUsingStatementProgram
{
static async Task Main()
{
await using (var exampleAsyncDisposable = new ExampleAsyncDisposable())
{
// Interact with the exampleAsyncDisposable instance.
}
Console.ReadLine();
}
}
Более того, ее можно написать так, чтобы неявно использовалась область объявления using.
class ExampleUsingDeclarationProgram
{
static async Task Main()
{
await using var exampleAsyncDisposable = new ExampleAsyncDisposable();
// Interact with the exampleAsyncDisposable instance.
Console.ReadLine();
}
}
Несколько ключевых слов await в одной строке
Иногда ключевое await
слово может отображаться несколько раз в одной строке. Рассмотрим следующий пример кода:
await using var transaction = await context.Database.BeginTransactionAsync(token);
В предыдущем примере:
- Метод BeginTransactionAsync ожидается.
- Тип возвращаемого значения — DbTransactionэто тип, реализующий
IAsyncDisposable
. - Используется
transaction
асинхронно, а также ожидается.
Стекированные объявления using
Когда вы создаете и используете несколько объектов, реализующих IAsyncDisposable, стекирование операторов await using
с помощью ConfigureAwait в ошибочных условиях может предотвратить вызовы DisposeAsync(). Чтобы метод DisposeAsync() вызывался всегда, следует избегать стекирования. В приведенных ниже трех примерах кода показаны допустимые шаблоны.
Допустимый шаблон 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();
}
}
В предыдущем примере каждая асинхронная операция очистки явно ограничена блоком await using
. Внешняя область следует тому, как objOne
задает его фигурные скобки, заключив objTwo
, как это objTwo
происходит, сначала удаляется, а затем objOne
. Оба IAsyncDisposable
экземпляра DisposeAsync() ожидают своего метода, поэтому каждый экземпляр выполняет асинхронную операцию очистки. Вызовы вложены, а не стекированы.
Допустимый шаблон 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();
}
}
В предыдущем примере каждая асинхронная операция очистки явно ограничена блоком await using
. В конце каждого блока IAsyncDisposable
соответствующий экземпляр имеет DisposeAsync() ожидаемый метод, таким образом выполняя асинхронную операцию очистки. Вызовы являются последовательными, а не стекированы. В этом сценарии сначала удаляется objOne
, а затем objTwo
.
Допустимый шаблон 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();
}
}
В предыдущем примере каждая асинхронная операция очистки неявно ограничена текстом содержащего метода. В конце заключиющего блока IAsyncDisposable
экземпляры выполняют асинхронные операции очистки. Этот пример выполняется в обратном порядке, из которого они были объявлены, то есть objTwo
удаляется раньше objOne
.
Недопустимый шаблон
Выделенные строки в следующем коде показывают, что означает наличие "стека с использованием". Если исключение создается из конструктора AnotherAsyncDisposable
, ни тот объект не удаляется должным образом. Переменная objTwo
никогда не назначается, так как конструктор не завершился успешно. В результате конструктор AnotherAsyncDisposable
отвечает за удаление всех ресурсов, выделенных перед созданием исключения. Если тип ExampleAsyncDisposable
имеет метод завершения, он имеет право на завершение.
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();
}
}
Совет
Избегайте такого шаблона, так как он может привести к непредвиденному поведению. Если вы используете один из допустимых шаблонов, проблема нерасположенных объектов не существует. Операции очистки выполняются правильно, когда using
операторы не стекаются.
См. также
Пример двойной реализации IDisposable
и IAsyncDisposable
, см. в исходном коде Utf8JsonWriter в GitHub.