【问题标题】:How to call an async method from a getter or setter?如何从 getter 或 setter 调用异步方法?
【发布时间】:2011-09-29 22:37:32
【问题描述】:

在 C# 中从 getter 或 setter 调用异步方法的最优雅的方式是什么?

这里有一些伪代码来帮助解释我自己。

async Task<IEnumerable> MyAsyncMethod()
{
    return await DoSomethingAsync();
}

public IEnumerable MyList
{
    get
    {
         //call MyAsyncMethod() here
    }
}

【问题讨论】:

  • 我的问题是为什么。属性应该模仿类似于字段的东西,因为它通常应该执行很少(或至少非常快速)的工作。如果你有一个长期运行的属性,最好把它写成一个方法,这样调用者就知道它是一个更复杂的工作体。
  • @James:完全正确——我怀疑这就是 CTP 明确不支持的原因。话虽如此,您始终可以创建Task&lt;T&gt; 类型的属性,它将立即返回,具有正常的属性语义,并且仍然允许根据需要异步处理事物。
  • @James 我的需求源于使用 Mvvm 和 Silverlight。我希望能够绑定到一个属性,其中数据的加载是延迟完成的。我使用的 ComboBox 扩展类需要在 InitializeComponent() 阶段进行绑定,但实际数据加载要晚得多。在尝试用尽可能少的代码完成任务时,getter 和 async 感觉就像是完美的组合。
  • 我知道您的示例返回一个 Enumerable,但如果您的异步方法只返回一个 Task,您可以将其更改为返回 void 并从 setter 调用它。

标签: c# async-ctp


【解决方案1】:

没有技术原因在 C# 中不允许使用 async 属性。这是一个有目的的设计决策,因为“异步属性”是矛盾的。

属性应该返回当前值;他们不应该启动后台操作。

通常,当有人想要一个“异步属性”时,他们真正想要的是以下之一:

  1. 返回值的异步方法。在这种情况下,请将属性更改为 async 方法。
  2. 可用于数据绑定但必须异步计算/检索的值。在这种情况下,要么对包含对象使用 async 工厂方法,要么使用 async InitAsync() 方法。在计算/检索该值之前,数据绑定值将是 default(T)
  3. 创建成本高,但应缓存以备将来使用的值。在这种情况下,请使用 AsyncLazy from my blogAsyncEx library。这将为您提供awaitable 属性。

更新:我在最近的一篇“异步 OOP”博文中介绍了 asynchronous properties

【讨论】:

  • 在第 2 点中。恕我直言,您没有考虑设置属性应该再次初始化基础数据(不仅在构造函数中)的常规场景。除了使用 Nito AsyncEx 或使用Dispatcher.CurrentDispatcher.Invoke(new Action(..)之外还有其他方法吗?
  • @Gerard:我不明白为什么第 (2) 点在这种情况下不起作用。只需实现INotifyPropertyChanged,然后在异步更新进行时决定是否要返回旧值或default(T)
  • @Stephan:好的,但是当我在设置器中调用异步方法时,我收到 CS4014“未等待”警告(或者仅在 Framework 4.0 中?)。在这种情况下,您是否建议取消该警告?
  • @Gerard:我的第一个建议是使用NotifyTaskCompletion from my AsyncEx project。或者你可以建立自己的;没那么难。
  • @Stephan:好的,我会试试的。也许一篇关于这个异步数据绑定视图模型场景的好文章已经到位。例如。绑定到{Binding PropName.Result} 对我来说并非易事。
【解决方案2】:

你不能异步调用它,因为没有异步属性支持,只有异步方法。因此,有两种选择,都利用了 CTP 中的异步方法实际上只是返回 Task&lt;T&gt;Task 的方法这一事实:

// Make the property return a Task<T>
public Task<IEnumerable> MyList
{
    get
    {
         // Just call the method
         return MyAsyncMethod();
    }
}

或者:

// Make the property blocking
public IEnumerable MyList
{
    get
    {
         // Block via .Result
         return MyAsyncMethod().Result;
    }
}

【讨论】:

  • 感谢您的回复。选项 A:返回任务并不能真正用于绑定目的。选项 B:.Result,正如您所提到的,会阻塞 UI 线程(在 Silverlight 中),因此需要在后台线程上执行操作。我会看看我能不能用这个想法想出一个可行的解决方案。
  • @duluca:您也可以尝试使用类似private async void SetupList() { MyList = await MyAsyncMethod(); } 的方法,这将导致在异步操作完成后立即设置 MyList(然后自动绑定,如果它实现 INPC)。 .
  • 该属性需要在一个我声明为页面资源的对象中,所以我真的需要这个调用来自 getter。请参阅我的答案以了解我想出的解决方案。
  • @duluca:实际上,这就是我建议您执行的操作...但是请意识到,如果您快速多次访问 Title,您当前的解决方案将导致多次调用 getTitle()同时...
  • 非常好。虽然对于我的具体情况不是问题,但对 isLoading 进行布尔检查可以解决问题。
【解决方案3】:

由于我的解耦架构,我真的需要调用源自 get 方法。所以我想出了以下实现。

用法: 标题位于 ViewModel 或您可以静态声明为页面资源的对象中。绑定到它,当 getTitle() 返回时,该值将被填充而不会阻塞 UI。

string _Title;
public string Title
{
    get
    {
        if (_Title == null)
        {   
            Deployment.Current.Dispatcher.InvokeAsync(async () => { Title = await getTitle(); });
        }
        return _Title;
    }
    set
    {
        if (value != _Title)
        {
            _Title = value;
            RaisePropertyChanged("Title");
        }
    }
}

【讨论】:

  • updfrom 18/07/2012 在 Win8 RP 中,我们应该将 Dispatcher 调用更改为:Window.Current.CoreWindow.Dispatcher.RunAsync( CoreDispatcherPriority.Normal, async ( ) => { Title= await GetTytleAsync(url);});
  • @ChristopherStevenson,我也这么认为,但我不相信是这样。因为 getter 被执行为一劳永逸,所以在完成时不调用 setter,当 getter 完成执行时绑定将不会更新。
  • 不,它有并且有一个竞争条件,但用户不会看到它,因为'RaisePropertyChanged("Title")'。它在完成之前返回。但是,完成后,您将设置属性。这会触发 PropertyChanged 事件。 Binder 再次获取属性值。
  • 基本上,第一个getter会返回一个空值,然后它会被更新。请注意,如果我们希望每次都调用 getTitle,则可能存在错误循环。
  • 您还应该知道,该异步调用中的任何异常都将被完全吞没。如果你有一个,它们甚至不会到达应用程序上未处理的异常处理程序。
【解决方案4】:

你可以像这样使用Task

public int SelectedTab
        {
            get => selected_tab;
            set
            {
                selected_tab = value;

                new Task(async () =>
                {
                    await newTab.ScaleTo(0.8);
                }).Start();
            }
        }

【讨论】:

  • 那么当它抛出异常时就会进入黑洞
【解决方案5】:

我认为我们可以等待只返回第一个 null 的值,然后获取真正的值,所以对于 Pure MVVM(例如 PCL 项目),我认为以下是最优雅的解决方案:

private IEnumerable myList;
public IEnumerable MyList
{
  get
    { 
      if(myList == null)
         InitializeMyList();
      return myList;
     }
  set
     {
        myList = value;
        NotifyPropertyChanged();
     }
}

private async void InitializeMyList()
{
   MyList = await AzureService.GetMyList();
}

【讨论】:

  • 这不会产生编译器警告CS4014: Async method invocation without an await expression
  • 对遵循这个建议持非常的怀疑。观看此视频,然后自己决定:channel9.msdn.com/Series/Three-Essential-Tips-for-Async/…
  • 你应该避免使用“async void”方法!
  • 每一次呼喊都应该有一个明智的回答,你能@SuperJMN解释一下为什么吗?
  • @Contango 好视频。他说,“仅将async void 用于顶级处理程序等”。我认为这可能符合“和他们的同类”的条件。
【解决方案6】:

我认为 .GetAwaiter().GetResult() 正是解决这个问题的方法,不是吗? 例如:

string _Title;
public string Title
{
    get
    {
        if (_Title == null)
        {   
            _Title = getTitle().GetAwaiter().GetResult();
        }
        return _Title;
    }
    set
    {
        if (value != _Title)
        {
            _Title = value;
            RaisePropertyChanged("Title");
        }
    }
}

【讨论】:

  • 这与使用.Result 阻塞相同——它不是异步的,它可能导致死锁。
  • 你需要添加 IsAsync=True
  • 感谢对我的回答的反馈;我真的很想有人提供这个死锁的例子,这样我就可以看到它的实际效果
【解决方案7】:

由于您的“异步属性”在视图模型中,您可以使用AsyncMVVM

class MyViewModel : AsyncBindableBase
{
    public string Title
    {
        get
        {
            return Property.Get(GetTitleAsync);
        }
    }

    private async Task<string> GetTitleAsync()
    {
        //...
    }
}

它将为您处理同步上下文和属性更改通知。

【讨论】:

  • 作为财产,它必须是。
  • 抱歉,我可能错过了这段代码的重点。能详细点吗?
  • 属性被定义为阻塞。 GetTitleAsync() 用作没有语法糖的“异步 getter”。
  • @DmitryShechtman:不,它不必阻塞。这正是更改通知和状态机的用途。从定义上讲,它们并没有阻塞。根据定义,它们是同步的。这与阻塞不同。 “阻塞”意味着他们可能会做繁重的工作并且可能会花费大量时间来执行。反过来,这正是属性不应该做的事情。
【解决方案8】:

当我遇到这个问题时,尝试从 setter 或构造函数运行异步方法同步使我陷入 UI 线程的死锁,并且使用事件处理程序需要对一般设计进行太多更改。
解决方案通常是明确地写出我想要隐式发生的事情,即让另一个线程处理操作并让主线程等待它完成:

string someValue=null;
var t = new Thread(() =>someValue = SomeAsyncMethod().Result);
t.Start();
t.Join();

你可以说我滥用了这个框架,但它确实有效。

【讨论】:

    【解决方案9】:

    死灵术。
    在 .NET Core/NetStandard2 中,可以使用Nito.AsyncEx.AsyncContext.Run 代替System.Windows.Threading.Dispatcher.InvokeAsync

    class AsyncPropertyTest
    {
    
        private static async System.Threading.Tasks.Task<int> GetInt(string text)
        {
            await System.Threading.Tasks.Task.Delay(2000);
            System.Threading.Thread.Sleep(2000);
            return int.Parse(text);
        }
    
    
        public static int MyProperty
        {
            get
            {
                int x = 0;
    
                // https://stackoverflow.com/questions/6602244/how-to-call-an-async-method-from-a-getter-or-setter
                // https://stackoverflow.com/questions/41748335/net-dispatcher-for-net-core
                // https://github.com/StephenCleary/AsyncEx
                Nito.AsyncEx.AsyncContext.Run(async delegate ()
                {
                    x = await GetInt("123");
                });
    
                return x;
            }
        }
    
    
        public static void Test()
        {
            System.Console.WriteLine(System.DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss.fff"));
            System.Console.WriteLine(MyProperty);
            System.Console.WriteLine(System.DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss.fff"));
        }
    
    
    }
    

    如果您只是选择了System.Threading.Tasks.Task.RunSystem.Threading.Tasks.Task&lt;int&gt;.Run,那么它不会起作用。

    【讨论】:

    • 什么是Nito.AsyncEx.AsyncContext.Run?我在哪里可以找到它?是外部依赖吗?这些是您应该编辑到此答案中以使其更好的重要细节。
    • 仅供参考:Nito.AsyncEx 是 Stephen Cleary 的库,他之前在此线程的回复(或评论)中链接到该库。这是当前的 github 链接。 github.com/StephenCleary/AsyncEx
    【解决方案10】:

    您可以创建事件并在属性更改时调用事件。 像这样的:

    private event EventHandler<string> AddressChanged;
    public YourClassConstructor(){
         AddressChanged += GoogleAddressesViewModel_AddressChanged;
    }
    private async void GoogleAddressesViewModel_AddressChanged(object sender, string e){
          ... make your async call
    }
    private string _addressToSearch;
    public string AddressToSearch
    {
        get { return _addressToSearch; }
        set
        {
            _addressToSearch = value;
            AddressChanged.Invoke(this, AddressToSearch);
        }
    }
    

    【讨论】:

      【解决方案11】:

      我认为下面的示例可能遵循@Stephen-Cleary 的方法,但我想给出一个编码示例。这适用于数据绑定上下文,例如 Xamarin。

      类的构造函数 - 或者实际上是它所依赖的另一个属性的设置器 - 可能会调用异步 void,它会在任务完成时填充属性,而无需等待或阻塞。当它最终获得一个值时,它将通过 NotifyPropertyChanged 机制更新您的 UI。

      我不确定从构造函数调用 aysnc void 的任何副作用。也许评论者会详细说明错误处理等。

      class MainPageViewModel : INotifyPropertyChanged
      {
          IEnumerable myList;
      
          public event PropertyChangedEventHandler PropertyChanged;
      
          public MainPageViewModel()
          {
      
              MyAsyncMethod()
      
          }
      
          public IEnumerable MyList
          {
              set
              {
                  if (myList != value)
                  {
                      myList = value;
      
                      if (PropertyChanged != null)
                      {
                          PropertyChanged(this, new PropertyChangedEventArgs("MyList"));
                      }
                  }
              }
              get
              {
                  return myList;
              }
          }
      
          async void MyAsyncMethod()
          {
              MyList = await DoSomethingAsync();
          }
      
      
      }
      

      【讨论】:

      • 调用 async void 是个糟糕的主意,因为它不会阻塞,任何异常都会进入黑洞
      • @rolls 避免的好理由。仍然只是看,我看不到批准或赞成的编码示例。我很乐意为 ref 提供一个很好的答案并投票。
      【解决方案12】:

      我查看了所有答案,但都存在性能问题。

      例如:

      string _Title;
      public string Title
      {
          get
          {
              if (_Title == null)
              {   
                  Deployment.Current.Dispatcher.InvokeAsync(async () => { Title = await getTitle(); });
              }
              return _Title;
          }
          set
          {
              if (value != _Title)
              {
                  _Title = value;
                  RaisePropertyChanged("Title");
              }
          }
      }
      

      Deployment.Current.Dispatcher.InvokeAsync(async () => { Title = await getTitle(); });

      使用不是一个好的答案的调度程序。

      但是有一个简单的解决方案,就去做吧:

      string _Title;
          public string Title
          {
              get
              {
                  if (_Title == null)
                  {   
                      Task.Run(()=> 
                      {
                          _Title = getTitle();
                          RaisePropertyChanged("Title");
                      });        
                      return;
                  }
                  return _Title;
              }
              set
              {
                  if (value != _Title)
                  {
                      _Title = value;
                      RaisePropertyChanged("Title");
                  }
              }
          }
      

      【讨论】:

      • 如果你的函数是异步的,使用 getTitle().wait() 而不是 getTitle()
      • 如果我可以让你的函数连续 100 倍。它将产生 100 个线程,从而破坏应用程序的性能
      • 所以在你的情况下,你可以使用任务内部的锁......
      • 更糟。它会产生 100 个任务,然后将它们全部阻止。
      【解决方案13】:

      您可以将属性更改为Task&lt;IEnumerable&gt;

      然后做一些类似的事情:

      get
      {
          Task<IEnumerable>.Run(async()=>{
             return await getMyList();
          });
      }
      

      并像使用它一样 等待我的列表;

      【讨论】:

        猜你喜欢
        • 2015-05-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-03-06
        • 1970-01-01
        • 2016-07-10
        相关资源
        最近更新 更多