Могут ли конструкторы быть асинхронными?

У меня есть проект, в котором я пытаюсь заполнить некоторые данные в конструкторе:

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 не запрограммировала эту функцию.

Я считаю, что с правильным синтаксисом это может быть реализовано и не должно быть слишком запутанным или подверженным ошибкам. Я думаю, что Стивен Клири сообщение в блоге и несколько других ответов здесь косвенно указывают, что нет основополагающей причины для этого, и более того - решена которые не имеют обходных решений. Существование этих относительно простых обходных решений, вероятно, является одной из причин, почему эта функция еще не реализована.

Ответ 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

Это отличное место для создания шаблона. Это ленивая инициализация. Конструктор не может быть асинхронным, он должен быть атомарным, иметь конкретность и состояние.