构造函数可以异步吗?

Posted

技术标签:

【中文标题】构造函数可以异步吗?【英文标题】:Can constructors be async? 【发布时间】:2011-12-30 00:52:31 【问题描述】:

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

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。可能有很多(甚至是明显的)边缘案例和反对它的原因,我只是想不出。我也四处寻找解释,但似乎找不到任何解释。

【问题讨论】:

不,但在他的blog 中,Stephen Cleary 提供了一种工厂方法 方法以及其他可供考虑的方法。 this answer 中提出的模式效果很好,它是工厂模式的一个分支,但我将开始将其称为 async constructor pattern 【参考方案1】:

由于无法创建异步构造函数,我使用静态异步方法返回由私有构造函数创建的类实例。这并不优雅,但可以正常工作。

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;   
    
  

【讨论】:

我认为这个答案应该有更多的选票。它给出了一个答案,封装并隐藏了在构造项目后调用 Initialize() 方法的需要,从而防止了构造对象而忘记调用其初始化方法的潜在错误。 Ag,如果您可以控制构造函数,但如果您的类实现了抽象基类,这将是一个很好的解决方案,例如public class LoginModelValidator : AbstractValidator 你有问题 此方法使用factory pattern。查看另一个写得很好的类似答案here。 您并不总是可以控制调用者,因此工厂并非总是通用解决方案(以更通用的方式重述 Damian said 的内容)跨度> 从“用户”的角度来看,这是一个很好的解决方案,但它在 Web 应用程序中很常见,您需要大量的样板文件。如果他们能在类似于异步构造函数的东西中语法糖这种行为,那就太好了。【参考方案2】:

构造函数的行为与返回构造类型的方法非常相似。而async 方法不能只返回任何类型,它必须是“一劳永逸”voidTask

如果T 类型的构造函数实际上返回了Task&lt;T&gt;,我想那会很混乱。

如果异步构造函数的行为方式与async void 方法相同,那么就会破坏构造函数的本意。构造函数返回后,你应该得到一个完全初始化的对象。不是将来会在某个未定义的点实际正确初始化的对象。也就是说,如果你很幸运并且异步初始化没有失败。

这一切都只是猜测。但在我看来,拥有异步构造函数的可能性带来的麻烦多于其价值。

如果您真的想要 async void 方法的“即发即弃”语义(如果可能,应该避免这种情况),您可以轻松地将所有代码封装在 async void 方法中并从您的构造函数中调用它,如你在问题中提到。

【讨论】:

我认为这是最接近的。 await 可以经常替换 .ContinueWith,以至于我很容易忘记它并不是那么简单。我什至不确定我在想什么,但我想我在想 await 应该“返回”一个构造的 T (你指出这不是异步方法可以返回的),因为那是构造函数“返回”,但是当等待继续时,构造函数不会返回任何内容,因为它是一个构造函数,例如void。我什至没有意义了,但你的回答是最有帮助的。谢谢。 “如果类型 T 的构造函数实际上返回了 Task,我想那会很混乱。”我不同意。和 async Dispose 一样,会很自然。 "async void" 不要那样做。对象的构造不完整。它可以引发无法处理的异常等。【参考方案3】:

您的问题类似于创建文件对象并打开文件。事实上,在很多类中,您必须执行两个步骤才能真正使用该对象:创建 + 初始化(通常称为类似于 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);

【讨论】:

... 但是异步方法的返回一定是Task?你如何解决这个问题? 思路不是使用构造函数,而是使用静态函数构造对象并异步初始化它。所以不要在构造函数中进行初始化,而是在一个单独的私有Initialize函数中,这个Initialize函数可以返回一个等待的Task,因此静态Create函数可以返回一个等待的Task 从现在开始我将其称为async constructor pattern。 -- IMO,这应该是公认的答案,因为它很好、简单,而且很重要 -- 干得好! 这很有帮助,谢谢分享!并且足够详细,使其易于理解。荣誉 尝试使用 XAML 视图文件 (Xamarin.Forms) 的代码隐藏,我认为这种解决问题的方法不适用于我的上下文。无论如何感谢@HaraldCoppoolse 的想法。错误完全有意义:Type 'MyClassViewModel' is not usable as an object element because it is not public or does not define a public parameterless constructor or a type converter.【参考方案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

这就是我猜他们不允许这样做的原因。

【讨论】:

你会这么想,但实际上这甚至没有意义。如果您拨打“var o = sqlcmd.BeginExecuteReader();”之类的电话它会在继续下一行之前将 IAsyncResult 对象分配给 o。在您的示例中,在构造函数完成之前,它无法为实例分配任何内容,因此允许构造函数异步是没有意义的。 我期望的方式(实际上希望“期望”这个词太强了)它的行为是返回构造的对象,但是当它等待的对象准备好时,对象将完成构造。因为我认为 await 更像是一个设置-a-continuation-and-return,我希望这可能是可能的。我不希望返回 null。 允许一半构造对象(异步构造函数隐含)会破坏其他语言构造,例如 readonly 关键字所做的保证。 如果类 C 的构造函数是真正的 Async,你会得到一个 Task ,你必须等待。【参考方案6】:

我只是想知道为什么我们不能直接从构造函数中调用await

我相信简短的回答很简单:因为 .Net 团队尚未编写此功能。

我相信通过正确的语法可以实现这一点,并且不应该太混乱或容易出错。我认为 Stephen Cleary 的blog post 和这里的其他几个答案已经含蓄地指出,没有根本的理由反对它,而且更重要的是 - 通过变通方法解决了这种缺乏。这些相对简单的解决方法的存在可能是该功能尚未(尚未)实现的原因之一。

【讨论】:

异步构造函数是currently being discussed and considered。【参考方案7】:

在构造函数中调用异步可能会导致死锁,请参考 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

【讨论】:

这是关于调用async 方法 构造函数(这是可能的,但可能不是一个好主意)。这个问题是关于 构造函数本身async (根本不会编译)。 许多答案都在说“没有理由不应该”,这是一个很好的理由——此外,如果库开始在其构造函数中执行异步操作(即甚至 .Wait() 或.GetResult()) 它可能会导致其他问题;例如,ASP.NET Web 表单需要特殊配置才能使异步调用正常工作(即,它不是死锁,但执行上下文只是在某处掉线并且永远不会回来——即使在配置之后,它也只能在页面生命的某些部分内工作循环...)——总的来说,我认为在同步方法中隐藏异步调用应该被视为一种反模式。【参考方案8】:

一些答案​​涉及创建一个新的public 方法。不这样做,使用Lazy&lt;T&gt; 类:

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】:

你可以在构造函数中使用 Action

 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;
        
    

【讨论】:

这会创建并使用async void 方法,这不是一个好主意。 所以你必须使用 Data = GetDataTask().Result; 没有。 Result 可能会导致死锁。我有多种解决方案described on my blog。【参考方案10】:

我会使用这样的东西。

 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;
               
    

这是我可以为构造函数得到的最接近的结果。

【讨论】:

【参考方案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】:

我不熟悉 async 关键字(这是 Silverlight 特有的还是 Visual Studio 测试版中的新功能?),但我想我可以告诉您为什么不能这样做。

如果我这样做:

var o = new MyObject();
MessageBox(o.SomeProperty.ToString());

o 在下一行代码运行之前可能没有完成初始化。在构造函数完成之前无法分配对象的实例化,并且使构造函数异步不会改变这一点,那么重点是什么?但是,您可以从构造函数调用异步方法,然后您的构造函数可以完成,您将获得实例化,同时异步方法仍在执行设置对象所需的任何操作。

【讨论】:

此外,它在等待构造函数完成时会分配给 o 什么?我知道自然倾向于认为它应该为空,但这不是它的工作原理。如果没有线程,你永远不会返回 null ......使用线程不会改变这一点。 想想“var o;”只有没有“new MyObject()”。这就是你在构造函数完成它的工作之前得到的。由于使构造函数异步似乎是不可能的,我们无法测试原子时间,但我们可以假设它与“var o;”保持相同的状态。直到它被构造出来。 'var o;'不是一个有效的陈述。但是让我们假设我们正在指定类型。在第一行你会有'object o;'第二行是'o = new MyObject()'。现在,它必须在 o 进入下一行之前为 o 分配一些东西……这就是问题所在,因为在构造函数完成之前它不能。 它显然会返回Task&lt;MyObject&gt;()

以上是关于构造函数可以异步吗?的主要内容,如果未能解决你的问题,请参考以下文章

在构造函数中进行 GWT 异步回调安全吗?

从 ViewModel 构造函数 Xamarin.Forms 调用异步方法

在构造函数中调用异步方法?

异步/等待类构造函数

在 C# 中的类构造函数中调用异步方法 [重复]

在构造函数或 ngOnInit 中加载异步函数