Могут ли конструкторы быть асинхронными?
У меня есть проект, в котором я пытаюсь заполнить некоторые данные в конструкторе:
public class ViewModel
{
public ObservableCollection<TData> Data { get; set; }
async public ViewModel()
{
Data = await GetDataTask();
}
public Task<ObservableCollection<TData>> GetDataTask()
{
Task<ObservableCollection<TData>> task;
//Create a task which represents getting the data
return task;
}
}
К сожалению, я получаю сообщение об ошибке:
Модификатор async
недопустим для этого элемента
Конечно, если я заверну в стандартный метод и вызову его из конструктора:
public async void Foo()
{
Data = await GetDataTask();
}
это работает отлично. Точно так же, если я использую старый способ наизнанку
GetData().ContinueWith(t => Data = t.Result);
Это тоже работает. Мне было просто интересно, почему мы не можем вызвать await
из конструктора напрямую. Вероятно, есть много (даже очевидных) крайних случаев и причин против этого, я просто не могу придумать ни одного. Я также ищу объяснения, но не могу найти ни одного.
Ответы
Ответ 1
Конструктор действует очень похоже на метод, возвращающий построенный тип. И метод async
не может возвращать только какой-либо тип, он должен быть либо "огонь и забыть" void
, либо Task
.
Если конструктор типа T
действительно возвратил Task<T>
, это будет очень запутанным, я думаю.
Если конструктор async вел себя так же, как метод async void
, такой разрыв прерывается, каким должен быть конструктор. После возврата конструктора вы должны получить полностью инициализированный объект. Не объект, который на самом деле будет корректно инициализирован в какой-то точке undefined в будущем. То есть, если вам повезет, и инициализация async не завершится.
Все это всего лишь предположение. Но мне кажется, что наличие возможности конструктора async приносит больше проблем, чем это стоит.
Если вы действительно хотите использовать семантику "огонь и забыть" методов async void
(чего следует избегать, если это возможно), вы можете легко инкапсулировать весь код в метод async void
и вызвать это из своего конструктора, так как вы упомянули в вопросе.
Ответ 2
Поскольку невозможно создать конструктор async, я использую статический метод async, который возвращает экземпляр класса, созданный частным конструктором. Это не изящно, но работает нормально.
public class ViewModel
{
public ObservableCollection<TData> Data { get; set; }
//static async method that behave like a constructor
async public static Task<ViewModel> BuildViewModelAsync()
{
ObservableCollection<TData> tmpData = await GetDataTask();
return new ViewModel(tmpData);
}
// private constructor called by the async method
private ViewModel(ObservableCollection<TData> Data)
{
this.Data=Data;
}
}
Ответ 3
Ваша проблема сопоставима с созданием файлового объекта и открытием файла. На самом деле существует множество классов, в которых вам нужно выполнить два шага, прежде чем вы сможете использовать объект: create + Initialize (часто называемый чем-то похожим на Open).
Преимущество этого в том, что конструктор может быть легким. При желании вы можете изменить некоторые свойства перед фактической инициализацией объекта. Когда все свойства установлены, вызывается функция Initialize
/Open
для подготовки объекта к использованию. Эта функция Initialize
может быть асинхронной.
Недостатком является то, что вы должны доверять пользователю вашего класса, который он вызовет Initialize()
, прежде чем он использует какую-либо другую функцию вашего класса. Фактически, если вы хотите сделать свой класс полным доказательством (глупым?), Вы должны проверить каждую функцию, которая была вызвана Initialize()
.
Чтобы упростить это, нужно объявить конструктор частным и сделать общедоступную статическую функцию, которая будет создавать объект и вызывать Initialize()
перед возвратом сконструированного объекта. Таким образом, вы будете знать, что каждый, кто имеет доступ к объекту, использовал функцию Initialize
.
В этом примере показан класс, имитирующий желаемый асинхронный конструктор
public MyClass
{
public static async Task<MyClass> CreateAsync(...)
{
MyClass x = new MyClass();
await x.InitializeAsync(...)
return x;
}
// make sure no one but the Create function can call the constructor:
private MyClass(){}
private async Task InitializeAsync(...)
{
// do the async things you wanted to do in your async constructor
}
public async Task<int> OtherFunctionAsync(int a, int b)
{
return await ... // return something useful
}
Использование будет следующим:
public async Task<int> SomethingAsync()
{
// Create and initialize a MyClass object
MyClass myObject = await MyClass.CreateAsync(...);
// use the created object:
return await myObject.OtherFunctionAsync(4, 7);
}
Ответ 4
В этом конкретном случае для запуска задачи требуется viewModel и уведомление о ее завершении. "Асинхронное свойство", а не "асинхронный конструктор", имеет порядок.
Я только что выпустил AsyncMVVM, который решает именно эту проблему (среди прочего). Если вы его используете, ваша ViewModel станет следующей:
public class ViewModel : AsyncBindableBase
{
public ObservableCollection<TData> Data
{
get { return Property.Get(GetDataAsync); }
}
private Task<ObservableCollection<TData>> GetDataAsync()
{
//Get the data asynchronously
}
}
Как ни странно, Silverlight поддерживается.:)
Ответ 5
если вы сделаете конструктор асинхронным, после создания объекта вы можете столкнуться с проблемами, такими как нулевые значения вместо объектов экземпляра. Например,
MyClass instance = new MyClass();
instance.Foo(); // null exception here
Вот почему они этого не догадываются.
Ответ 6
Мне просто интересно, почему мы не можем вызвать await
непосредственно внутри конструктора.
Я считаю, что короткий ответ просто: потому что команда .Net не запрограммировала эту функцию.
Я считаю, что с правильным синтаксисом это может быть реализовано и не должно быть слишком запутанным или подверженным ошибкам. Я думаю, что Стивен Клири сообщение в блоге и несколько других ответов здесь косвенно указывают, что нет основополагающей причины для этого, и более того - решена которые не имеют обходных решений. Существование этих относительно простых обходных решений, вероятно, является одной из причин, почему эта функция еще не реализована.
Ответ 7
вызов async в конструкторе может вызвать взаимоблокировку, см.
http://social.msdn.microsoft.com/Forums/en/winappswithcsharp/thread/0d24419e-36ad-4157-abb5-3d9e6c5dacf1
http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115163.aspx
Ответ 8
Некоторые ответы включают создание нового public
метода. Без этого используйте класс Lazy<T>
:
public class ViewModel
{
private Lazy<ObservableCollection<TData>> Data;
async public ViewModel()
{
Data = new Lazy<ObservableCollection<TData>>(GetDataTask);
}
public ObservableCollection<TData> GetDataTask()
{
Task<ObservableCollection<TData>> task;
//Create a task which represents getting the data
return task.GetAwaiter().GetResult();
}
}
Чтобы использовать Data
, используйте Data.Value
.
Ответ 9
Я не знаком с ключевым словом async (это относится к Silverlight или новой функции в бета-версии Visual Studio?), но я думаю, что могу дать вам представление о том, почему вы не можете этого сделать.
Если я это сделаю:
var o = new MyObject();
MessageBox(o.SomeProperty.ToString());
o не может быть инициализировано до следующей строки кода. Невозможно назначить экземпляр вашего объекта до тех пор, пока ваш конструктор не будет завершен, и создание асинхронного конструктора не изменится, так что будет точкой? Тем не менее, вы можете вызвать асинхронный метод из своего конструктора, а затем ваш конструктор может завершиться, и вы получите свой экземпляр, в то время как метод async все еще делает все, что ему нужно, чтобы настроить ваш объект.
Ответ 10
вы можете использовать Action in Constructor
public class ViewModel
{
public ObservableCollection<TData> Data { get; set; }
public ViewModel()
{
new Action(async () =>
{
Data = await GetDataTask();
}).Invoke();
}
public Task<ObservableCollection<TData>> GetDataTask()
{
Task<ObservableCollection<TData>> task;
//Create a task which represents getting the data
return task;
}
}
Ответ 11
Я использую этот простой трюк.
public sealed partial class NamePage
{
private readonly Task _initializingTask;
public NamePage()
{
_initializingTask = Init();
}
private async Task Init()
{
/*
Initialization that you need with await/async stuff allowed
*/
}
}
Ответ 12
Я бы использовал что-то вроде этого.
public class MyViewModel
{
public MyDataTable Data { get; set; }
public MyViewModel()
{
loadData(() => GetData());
}
private async void loadData(Func<DataTable> load)
{
try
{
MyDataTable = await Task.Run(load);
}
catch (Exception ex)
{
//log
}
}
private DataTable GetData()
{
DataTable data;
// get data and return
return data;
}
}
Это как можно ближе к конструкторам.
Ответ 13
Это отличное место для создания шаблона. Это ленивая инициализация. Конструктор не может быть асинхронным, он должен быть атомарным, иметь конкретность и состояние.