Асинхронный конструктор класса
Всем известно, что асинхронных конструкторов не бывает, ну во всяком случае пока. Что же делать?
Уже есть подобный вопрос Асинхронный код в конструкторе, и несмотря на множество безусловно полезных комментов под ним, решения вопроса предлагаются только для промышленных масштабов - правильные и сложные.
Сидел, ломал голову, как же наиболее просто и безопасно проинициализировать объект, который нельзя использовать до окончания асинхронной операции инициализации внутреннего состояния объекта.
Самое очевидное лобовое решение, которое не подходит по выше указанные условия:
public class MyClass
{
private int _data;
public async Task InitAsync()
{
await Task.Delay(100);
data = 42;
}
public int GetData()
{
if (_data != 42)
throw new Exception("Объект не инициализирован, запустите InitAsync(), и дождитесь его завершения");
return _data;
}
}
То есть проверять при вызове рабочих методов, а корректно ли был проинициализирован объект, и бросать исключение, если нет.
var instance = new MyClass();
await instance.InitAsync();
int result = instance.GetData();
Решение рабочее, но уязвимо к ошибкам при разработке в случае если разработчик забудет вызвать InitAsync() или этот метод не будет вызван по каким-то другим причинам, или в методе инициализации возникнет исключение. В общем, шанс получить нерабочий экземпляр есть, и это плохо.
Есть ли простой способ реализации консистентного решения? Без фабрик, кучи интерфейсов и прочего.
Ответы (1 шт):
Способ есть, и не смотря на простоту, он оказался для меня неочевидным. А заключается он в скрытии конструктора и создании объекта через статический метод. Почему я этого решения раньше нигде не встретил? :)
public class MyClass
{
private int _data;
private MyClass() // прячу конструктор
{
}
public static async Task<MyClass> CreateAsync()
{
var instance = new MyClass();
await instance.InitAsync();
return instance;
}
private async Task InitAsync()
{
await Task.Delay(100);
data = 42;
}
public int GetData()
{
return _data;
}
}
Проверять корректность инициализации в рабочих методах теперь нет необходимости, так как за пределами метода CreateAsync() непроинициализированный экземпляр не будет существовать.
Работа разработчика сводится к такому коду:
var instance = await MyClass.CreateAsync();
int result = instance.GetData();
Такое решение мне кажется более целостным и безопасным. Не смог объект проинициализироваться, значит и не будет ссылки на него. А следовательно нелегального вызова GetData() не может произойти. Плюс публичное API класса теперь выглядит покороче, что тоже хорошо.
Как быть с readonly полями и get-only свойствами? А вот так:
public class MyClass
{
private readonly int _data;
private MyClass(int data)
{
_data = data;
}
public static async Task<MyClass> CreateAsync()
{
int data = await InitAsync();
return new MyClass(data);
}
private static async Task<int> InitAsync()
{
await Task.Delay(100);
return 42;
}
public int GetData()
{
return _data;
}
}