ChatGPT解决这个技术问题 Extra ChatGPT

构造函数可以异步吗?

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

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
请提出这个语言请求:github.com/dotnet/csharplang/discussions/419 - 每个人都需要编写的样板代码量才能拥有一个完全初始化的异步对象,这与 C# 中的趋势完全相反(样板更少)。

U
Uwe Keim

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

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
从“用户”的角度来看,这是一个很好的解决方案,但它在 ie web 应用程序中很常见,您需要大量的样板文件。如果他们能在类似于异步构造函数的东西中对这种行为进行语法糖化,那就太好了。
s
svick

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

如果 T 类型的构造函数实际上返回了 Task<T>,我认为这将非常令人困惑。

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

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

如果您确实想要 async void 方法的“即发即弃”语义(应尽可能避免),您可以轻松地将所有代码封装在一个 async void 方法中并从您的构造函数中调用它,正如您在这个问题。


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

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

...但是异步方法的返回必须是任务?你怎么解决这个问题?
思路不是使用构造函数,而是使用静态函数构造对象并异步对其进行初始化。所以不要在构造函数中进行初始化,而是在一个单独的私有 Initialize 函数中,这个 Initialize 函数可以返回一个等待的任务,因此静态 Create 函数可以返回一个等待的任务
从现在开始,我将其称为 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.
E
Emir Akaydın

如果使构造函数异步,则在创建对象后,您可能会遇到空值而不是实例对象等问题。例如;

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
D
Dmitry Shechtman

在这种特殊情况下,需要一个 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 是受支持的。 :)


t
tsemer

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

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

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


异步构造函数是 currently being discussed and considered
S
Sylvain Rodrigue

在构造函数中调用 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


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

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


B
BionicCode

C# 不允许 async 构造函数。构造函数旨在在一些简短的初始化后快速返回。你不期望也不想等待一个实例,即构造函数返回。因此,即使可以使用异步构造函数,构造函数也不是长时间运行操作或启动后台线程的地方。构造函数的唯一目的是将实例或类成员初始化为默认值或捕获的构造函数参数。您始终创建实例,然后在此实例上调用 DoSomething()。异步操作也不例外。您总是推迟昂贵的成员初始化。

有一些解决方案可以避免 async 构造函数的要求。

使用 Lazy 或 AsyncLazy 的简单替代解决方案(需要通过 NuGet 包管理器安装 Microsoft.VisualStudio.Threading 包)。 Lazy 允许延迟昂贵资源的实例化或分配。

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

S
Sanjay Patel

您可以在构造函数中使用 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
D
Dirk Boer

请提出这个语言请求:

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

每个人都需要编写大量样板代码才能拥有一个完全初始化的异步对象,这与 C# 中的趋势完全相反(样板更少)。


o
omri tsufim

您可以创建一个包装器并注入一个代表构造函数的函子:

class AsyncConstruct<T>
    where T: class
{
    private readonly Task<T> m_construction;
    private T m_constructed;
    public AsyncConstruct(Func<T> createFunc)
    {
        m_constructed = null;
        m_construction = Task.Run(()=>createFunc());
    }

    public T Get()
    {
        if(m_constructed == null)
        {
            m_constructed = m_construction.Result;
        }
        return m_constructed;
    }
}

t
tomcat

我会使用这样的东西。

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

这与我可以为构造函数获得的一样接近。


u
user1630939

我使用这个简单的技巧。

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
    */
  }
}

离开构造函数时未完成
你还没有等待异步的返回,所以这是没有意义的
B
Brandon Moore

我不熟悉 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 分配一些东西,然后才能进入下一行……问题就在于此,因为在构造函数完成之前它不能。
显然,它会返回 Task<MyObject>()