День 1328. #ЗаметкиНаПолях #AsyncTips
Асинхронное Создание Объектов: Фабрики
Задача: вы создаёте тип, который требует выполнения некоторой асинхронной работы в конструкторе.
Решение
Конструкторы не могут объявляться с async; кроме того, они не могут содержать ключевое слово await. Конечно, использование await в конструкторе могло бы быть полезным, но это привело бы к существенному изменению языка C#.
Одна из возможностей — использовать конструктор в паре с инициализирующим async-методом, чтобы тип использовался следующим образом:
var instance = new MyAsyncClass();
await instance.InitAsync();
У такого подхода есть недостатки. Разработчик может забыть вызвать метод InitAsync, а экземпляр не может использоваться сразу же после выполнения конструктора.
Вот более качественное решение, которое основано на применении паттерна асинхронного фабричного метода:
class AsyncClass
{
private AsyncClass()
{
}
private async Task<AsyncClass> InitAsync()
{
await Task.Delay(TimeSpan.FromSeconds(1));
return this;
}
public static Task<AsyncClass> CreateAsync()
{
var result = new AsyncClass();
return result.InitAsync();
}
}
Конструктор и метод InitAsync объявлены приватными, чтобы они не могли использоваться в вызывающем коде; экземпляры могут создаваться только одним способом — статическим фабричным методом CreateAsync. Вызывающий код не может обратиться к экземпляру до того, как инициализация будет завершена.
Экземпляр может создаваться следующим образом:
var instance = await AsyncClass.CreateAsync();
Главное преимущество этого паттерна заключается в том, что вызывающий код никак не сможет получить неинициализированный экземпляр AsyncClass.
К сожалению, в некоторых сценариях этот способ не работает — в частности, когда в коде используется провайдер внедрения зависимостей. Ни одна заметная библиотека внедрения зависимостей или инверсии управления не работает с async-кодом. Если вы окажетесь в одной из таких ситуаций, существует пара альтернатив, которые также стоит рассмотреть.
Если создаваемый экземпляр в действительности является общим ресурсом, можно использовать ленивую инициализацию. В противном случае можно воспользоваться паттерном асинхронной инициализации, который рассмотрим позже.
Пример того, как поступать не следует:
class AsyncClass
{
public AsyncClass()
{
InitAsync();
}
// ПЛОХОЙ КОД!!
private async void InitAsync()
{
await Task.Delay(TimeSpan.FromSeconds(1));
}
}
На первый взгляд решение может показаться разумным: вы получаете обычный конструктор, который запускает асинхронную операцию; при этом у него есть ряд недостатков, обусловленных использованием async void. Первая проблема заключается в том, что при завершении конструктора экземпляр всё ещё продолжает асинхронно инициализироваться, и не существует очевидного способа определить, когда завершится асинхронная инициализация. Вторая проблема связана с обработкой ошибок: любые исключения, выданные из InitAsync, не могут быть перехвачены секциями catch, окружающими вызов конструктора объекта.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 11.