构造函数可以是异步的吗?

467

我有一个项目,我想在构造函数中填充一些数据:

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。可能有很多(甚至显而易见的)边缘情况和反对理由,但我想不出任何理由。我也搜索了一下解释,但好像找不到。


11
不,但在他的博客中,Stephen Cleary 提供了一个工厂方法以及其他几种可供考虑的方法。 - DavidRR
1
这个答案中提出的模式效果非常好,它是工厂模式的一个分支,但我将开始具体地称之为“异步构造函数模式”。 - BrainSlugs83
3
请提高这个语言请求的优先级:https://github.com/dotnet/csharplang/discussions/419 - 每个人都需要编写大量样板代码才能拥有完全初始化的异步对象,这种情况与 C# 的趋势(减少样板代码)完全相反,非常疯狂。 - Dirk Boer
“如果我使用标准方法进行包装,并从构造函数中调用它...那么它可以正常工作。”实际上并不是这样的。由于缺少await,您将在调用时看到警告。请勿忽略此类警告;它可能会导致死锁;您的用户界面可能会永久冻结。 - ToolmakerSteve
16个回答

504

由于无法创建异步构造函数,我使用了一个静态的异步方法来返回由私有构造函数创建的类实例。虽然不够优雅但它能正常工作。

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;   
    }
}  

48
在我看来,这个回答应该有更多的赞同。它提供了一个回答,将调用Initialize()方法的需求封装和隐藏起来,从而防止构造对象并忘记调用其初始化方法的潜在错误。 - Robert Oschler
3
如果您可以控制构造函数,那么这将是一个很好的解决方案。但是,如果您的类实现了一个抽象基类,例如:public class LoginModelValidator : AbstractValidator那么您就会遇到问题。 - Damian Green
19
这种方法使用了工厂模式(factory pattern)。在这里可以查看另一个写得很好的类似答案。 - DavidRR
4
有时候你无法控制调用者,所以工厂并不总是通用解决方案(重新表述了Damian在Stack Overflow上的评论)。 - Matt Thomas
2
这是一个从“用户”角度来看很不错的解决方案,但在例如Web应用程序中很常见,需要大量的样板文件。如果他们能够在类似于异步构造函数的东西中语法糖此行为,那将是非常好的。 - Dirk Boer
显示剩余7条评论

278
构造函数的行为类似于返回构造类型的方法。而 async 方法不能返回任何类型,它必须是“fire and forget” 的 voidTask
如果类型 T 的构造函数实际上返回了 Task<T>,那将会非常令人困惑。
如果异步构造函数的行为方式与 async void 方法相同,这会打破构造函数的本意。在构造函数返回后,您应该得到一个完全初始化的对象,而不是在未来的某个不确定时间点才真正地初始化对象。如果你很幸运,并且异步初始化不失败的话。
所有这些只是猜测。但我认为,有可能有一个异步构造函数带来比它值得的更多麻烦。
如果您确实需要 async void 方法的“fire and forget”语义(如果可能,应该避免),您可以轻松地将所有代码封装在一个 async void 方法中,并从构造函数中调用该方法,就像您在问题中提到的那样。

5
我认为这个最接近了。 await 经常可以替换 .ContinueWith,以至于我很容易忘记它并不那么简单。我甚至不确定我当时在想什么,但我认为我是在想 await 应该“返回”一个构造的 T(你指出这不是异步方法所能返回的),因为构造函数就像 void 一样“返回”,但当 await 继续执行时,构造函数不会返回任何东西,因为它是构造函数。我说的都不通顺了,但是你的答案是最有帮助的。谢谢。 - Marty Neal
28
我不同意。就像异步Dispose一样,这将是非常自然的。 - drowa
3
"async void" 不要这样做。对象的构建还没有完成,可能会引发未被处理的异常等问题。 - Eldar
它必须是“fire and forget” voidTask。Stephen Cleary将async void方法称为“fire and crash”。这是公平的,因为async void方法中的异常默认情况下会导致进程崩溃。 - Theodor Zoulias

85

你的问题类似于创建文件对象并打开文件。实际上,有很多类需要在您实际使用对象之前执行两个步骤:创建+初始化(通常称为类似于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);
}

1
但是异步方法的返回值必须是一个Task,你如何解决这个问题? - Malte R
5
这个想法是不使用构造函数,而是使用一个静态函数来构造对象并异步初始化它。因此,不要在构造函数中进行初始化,而是在一个单独的私有初始化函数中进行,这个初始化函数可以返回可等待的任务,因此静态创建函数也可以返回可等待的任务。 - Harald Coppoolse
5
从现在开始,我将称之为“异步构造器模式”。在我看来,这应该是被接受的答案,因为它既简洁又直接。做得好! - BrainSlugs83
我尝试了使用XAML视图文件(Xamarin.Forms)的代码后台,并且我认为这种解决问题的方式不适用于我的情况。无论如何,感谢@HaraldCoppoolse提供的想法。错误是完全有道理的:“类型'MyClassViewModel'不能用作对象元素,因为它不是公共的,也没有定义公共的无参数构造函数或类型转换器。” - s3c
那么为什么不将类定义为public呢?唯一不应该做的事情是声明构造函数为public。由于您不会调用此构造函数,因此您不需要公共构造函数。如果您无法从XAML调用CreateAsync,为什么不使用代码后台呢? - Harald Coppoolse
显示剩余3条评论

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被支持。:)


4
如果您将构造函数异步化,在创建对象之后,可能会遇到空值而不是实例对象的问题。例如:
MyClass instance = new MyClass();
instance.Foo(); // null exception here

这就是为什么他们不允许这样做的原因吧。

你可能会这样想,但实际上这甚至没有意义。如果你像这样调用 'var o = sqlcmd.BeginExecuteReader();',它将在继续下一行之前将一个 IAsyncResult 对象分配给 o。在你的例子中,在构造函数完成之前它无法将任何东西分配给 instance,因此允许构造函数是异步的就没有意义。 - Brandon Moore
我期望(实际上是希望,“期望”这个词太强烈了)它的行为方式是返回构造好的对象,但是当等待完成时,该对象将完成构造。因为我认为await更像是设置一个续集然后返回,所以我希望这是可能的。我不希望返回null。 - Marty Neal
2
允许半构造对象(如异步构造函数所隐含的)将破坏其他语言结构,例如readonly关键字所做出的保证。 - spender
5
如果一个类C的构造函数真正是异步的,那么你会得到一个Task<C>,需要使用await等待它。 - Ykok

3
我只是想知道为什么我们不能直接从构造函数中调用await。简短的答案是:因为.Net团队没有编写此功能。我相信通过正确的语法,这可以实现,并且不应该太令人困惑或容易出错。我认为Stephen Cleary的博客文章和其他一些答案隐含地指出,没有根本性的反对理由,更何况 - 用解决方法解决了这种缺陷。这些相对简单的解决方法的存在可能是为什么这个功能尚未实现的原因之一。

9
异步构造函数目前正在讨论和考虑中。 - Stephen Cleary

1

1
这是关于从构造函数中调用async方法的问题(虽然可能可行,但不是一个好主意)。而本问题则是关于构造函数本身被标记为async(这根本无法编译通过)。 - Andrew Barber
许多答案都说“没有理由不可能”,这是一个好理由 - 而且,如果库在它们的构造函数中开始执行异步操作(即使是.Wait()或.GetResult()),它可能会引起其他问题;例如,ASP.NET Web表单需要特殊配置才能使异步调用工作(即它不是死锁,但执行上下文只是在某个地方掉落,永远不会回来 - 即使在配置之后,它仅在页面生命周期的某些部分内工作...) - 一般来说,我认为将异步调用隐藏在同步方法中应该被视为反模式。 - BrainSlugs83

0
请提高这个语言请求的优先级:

https://github.com/dotnet/csharplang/discussions/419

每个人需要编写的样板代码量,以便拥有完全初始化的异步对象,是疯狂的,并且完全与 C# 中的趋势(减少样板代码)相反。


0
C#不允许使用异步构造函数。构造函数的目的是进行一些简短的初始化后快速返回。您不希望等待实例即构造函数返回。因此,即使异步构造函数是可能的,构造函数也不是执行长时间操作或启动后台线程的地方。构造函数的唯一目的是将实例或类成员初始化为默认值或捕获的构造函数参数。您始终先创建实例,然后在该实例上调用DoSomething()。异步操作也不例外。您始终要延迟成本高昂的成员初始化。
有几种解决方案可以避免使用异步构造函数。
  1. 通过使用Lazy<T>AsyncLazy<T>(需要通过NuGet包管理器安装Microsoft.VisualStudio.Threading包),可以使用简单的替代方案。 Lazy<T>允许延迟实例化或分配昂贵的资源。
public class OrderService
{
  public List<object> Orders => this.OrdersInitializer.GetValue();
  private AsyncLazy<List<object>> OrdersInitializer { get; }

  public OrderService()
    => this.OrdersInitializer = new AsyncLazy<List<object>>(InitializeOrdersAsync, new JoinableTaskFactory(new JoinableTaskContext()));

  private async Task<List<object>> InitializeOrdersAsync()
  {
    await Task.Delay(TimeSpan.FromSeconds(5));
    return new List<object> { 1, 2, 3 };
  }
}

public static void Main()
{
  var orderService = new OrderService();

  // Trigger async initialization
  orderService.Orders.Add(4);
}

你可以使用方法而不是属性来公开数据。
public class OrderService
{
  private List<object> Orders { get; set; }

  public async Task<List<object>> GetOrdersAsync()
  {
    if (this.Orders == null)
    {
      await Task.Delay(TimeSpan.FromSeconds(5));
      this.Orders = new List<object> { 1, 2, 3 };
    }
    return this.Orders;
  }
}

public static async Task Main()
{
  var orderService = new OrderService();

  // Trigger async initialization
  List<object> orders = await orderService.GetOrdersAsync();
}

使用一个必须在实例使用前调用的InitializeAsync方法。
public class OrderService
{
  private List<object> orders;
  public List<object> Orders 
  { 
    get
    {
      if (!this.IsInitialized)
      {
        throw new InvalidOperationException(); 
      }
      return this.orders;
    }
    private set
    {
      this.orders = value;
    }
  }

  public bool IsInitialized { get; private set; }

  public async Task<List<object>> InitializeAsync()
  {
    if (this.IsInitialized)
    {
      return;
    }

    await Task.Delay(TimeSpan.FromSeconds(5));
    this.Orders = new List<object> { 1, 2, 3 };
    this.IsInitialized = true;
  }
}

public static async Task Main()
{
  var orderService = new OrderService();

  // Trigger async initialization
  await orderService.InitializeAsync();
}

通过将昂贵的参数传递给构造函数实例化实例。
public class OrderService
{
  public List<object> Orders { get; }

  public async Task<List<object>> OrderService(List<object> orders)
    => this.Orders = orders;
}

public static async Task Main()
{
  List<object> orders = await GetOrdersAsync();

  // Instantiate with the result of the async operation
  var orderService = new OrderService(orders);
}

private static async Task<List<object>> GetOrdersAsync()
{
  await Task.Delay(TimeSpan.FromSeconds(5));
  return new List<object> { 1, 2, 3 };
}

使用工厂方法和私有构造函数。
public class OrderService
{
  public List<object> Orders { get; set; }

  private OrderServiceBase()  
    => this.Orders = new List<object>();

  public static async Task<OrderService> CreateInstanceAsync()
  {
    var instance = new OrderService();
    await Task.Delay(TimeSpan.FromSeconds(5));
    instance.Orders = new List<object> { 1, 2, 3 };
    return instance;
  }
}

public static async Task Main()
{
  // Trigger async initialization  
  OrderService orderService = await OrderService.CreateInstanceAsync();
}

0
一些答案涉及创建新的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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接