Làm thế nào để gọi một phương thức async từ getter hoặc setter?


223

Điều gì là cách thanh lịch nhất để gọi một phương thức async từ getter hoặc setter trong C #?

Đây là một số mã giả để giúp giải thích bản thân mình.

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

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

4
Câu hỏi của tôi sẽ là tại sao. Một thuộc tính được cho là bắt chước một cái gì đó giống như một trường trong đó nó thường sẽ thực hiện công việc nhỏ (hoặc ít nhất là rất nhanh). Nếu bạn có một thuộc tính chạy dài thì tốt hơn hết là viết nó như một phương thức để người gọi biết đó là một khối công việc phức tạp hơn.
James Michael Hare

@James: Điều đó hoàn toàn chính xác - và tôi nghi ngờ đó là lý do tại sao điều này rõ ràng không được hỗ trợ trong CTP. Điều đó đang được nói, bạn luôn có thể tạo thuộc tính loại Task<T>, sẽ trả về ngay lập tức, có ngữ nghĩa thuộc tính thông thường và vẫn cho phép mọi thứ được xử lý không đồng bộ khi cần.
Sậy Copsey

17
@James Nhu cầu của tôi phát sinh từ việc sử dụng Mvvm và Silverlight. Tôi muốn có thể liên kết với một tài sản, trong đó việc tải dữ liệu được thực hiện một cách lười biếng. Lớp mở rộng ComboBox mà tôi đang sử dụng yêu cầu liên kết xảy ra ở giai đoạn LaunchizeComponent (), tuy nhiên việc tải dữ liệu thực tế xảy ra muộn hơn nhiều. Khi cố gắng thực hiện với càng ít mã càng tốt, getter và async có cảm giác như sự kết hợp hoàn hảo.
Doguhan Uluca


James và Reed, dường như bạn đang quên rằng luôn có những trường hợp cạnh. Trong trường hợp của WCF, tôi muốn xác minh rằng dữ liệu được đặt trên một thuộc tính là chính xác và nó phải được xác minh bằng mã hóa / giải mã. Các chức năng tôi sử dụng để giải mã xảy ra để sử dụng chức năng không đồng bộ từ nhà cung cấp bên thứ ba. (KHÔNG PHẢI LÀ TÔI CÓ THỂ LÀM Ở ĐÂY).
RashadRivera

Câu trả lời:


211

Không có lý do kỹ thuật rằng asynccác thuộc tính không được phép trong C #. Đó là một quyết định thiết kế có mục đích, bởi vì "tính chất không đồng bộ" là một oxymoron.

Các thuộc tính nên trả về giá trị hiện tại; họ không nên khởi động các hoạt động nền.

Thông thường, khi ai đó muốn có "tài sản không đồng bộ", điều họ thực sự muốn là một trong những điều sau:

  1. Một phương thức không đồng bộ trả về một giá trị. Trong trường hợp này, thay đổi thuộc tính thành một asyncphương thức.
  2. Một giá trị có thể được sử dụng trong liên kết dữ liệu nhưng phải được tính toán / truy xuất không đồng bộ. Trong trường hợp này, hoặc sử dụng một asyncphương thức xuất xưởng cho đối tượng chứa hoặc sử dụng một async InitAsync()phương thức. Giá trị giới hạn dữ liệu sẽ default(T)cho đến khi giá trị được tính / lấy.
  3. Một giá trị đắt tiền để tạo, nhưng nên được lưu trữ để sử dụng trong tương lai. Trong trường hợp này, sử dụng AsyncLazy từ thư viện blog hoặc AsyncEx của tôi . Điều này sẽ cung cấp cho bạn một awaittài sản có thể.

Cập nhật: Tôi bao gồm các thuộc tính không đồng bộ trong một trong các bài đăng blog "async OOP" gần đây của tôi.


Trong điểm 2. imho bạn không tính đến kịch bản thông thường trong đó thiết lập thuộc tính sẽ khởi tạo lại dữ liệu cơ bản (không chỉ trong hàm tạo). Có cách nào khác ngoài việc sử dụng Nito AsyncEx hoặc sử dụng Dispatcher.CurrentDispatcher.Invoke(new Action(..)không?
Gerard

@Gerard: Tôi không hiểu tại sao điểm (2) sẽ không hoạt động trong trường hợp đó. Chỉ cần thực hiện INotifyPropertyChangedvà sau đó quyết định xem bạn muốn trả về giá trị cũ hay default(T)trong khi cập nhật không đồng bộ đang trong chuyến bay.
Stephen Cleary

1
@Stephan: ok, nhưng khi tôi gọi phương thức async trong setter, tôi nhận được cảnh báo "không chờ đợi" CS4014 (hoặc chỉ có trong Framework 4.0?). Bạn có lời khuyên để ngăn chặn cảnh báo đó trong trường hợp như vậy?
Gerard

@Gerard: Đề xuất đầu tiên của tôi sẽ là sử dụng NotifyTaskCompletiontừ dự án AsyncEx của tôi . Hoặc bạn có thể xây dựng của riêng bạn; nó không khó lắm đâu
Stephen Cleary

1
@Stephan: ok sẽ thử điều đó. Có lẽ một bài viết hay về kịch bản dữ liệu không đồng bộ-viewmodel không đồng bộ này được đưa ra. Ví dụ: ràng buộc để {Binding PropName.Result}không phải là tầm thường để tôi tìm hiểu.
Gerard

101

Bạn không thể gọi nó là không đồng bộ, vì không có hỗ trợ thuộc tính không đồng bộ, chỉ có các phương thức không đồng bộ. Như vậy, có hai tùy chọn, cả hai đều lợi dụng thực tế là các phương thức không đồng bộ trong CTP thực sự chỉ là một phương thức trả về Task<T>hoặc Task:

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

Hoặc là:

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

1
Cảm ơn bạn đã phản hồi của bạn. Tùy chọn A: Trả lại một Nhiệm vụ không thực sự tập luyện cho mục đích ràng buộc. Tùy chọn B: .Result, như bạn đã đề cập, chặn luồng UI (trong Silverlight), do đó yêu cầu thao tác thực thi trên luồng nền. Tôi sẽ xem liệu tôi có thể đưa ra một giải pháp khả thi với ý tưởng này không.
Doguhan Uluca

3
@duluca: Bạn cũng có thể thử có một phương thức như thế private async void SetupList() { MyList = await MyAsyncMethod(); } này Điều này sẽ khiến MyList được thiết lập (và sau đó tự động liên kết, nếu nó thực hiện INPC) ngay khi hoạt động async hoàn thành ...
Reed Copsey 7/07/11

Thuộc tính cần phải nằm trong một đối tượng mà tôi đã khai báo là tài nguyên trang, vì vậy tôi thực sự cần cuộc gọi này bắt nguồn từ getter. Xin vui lòng xem câu trả lời của tôi cho giải pháp tôi đã đưa ra.
Doguhan Uluca

1
@duluca: Đó là, một cách hiệu quả, những gì tôi đã đề nghị bạn làm ... Nhận ra, tuy nhiên, nếu bạn truy cập Tiêu đề nhiều lần một cách nhanh chóng, giải pháp hiện tại của bạn sẽ dẫn đến nhiều cuộc gọi đến getTitle()mô phỏng ...
Reed Copsey

Điểm rất tốt. Mặc dù không phải là vấn đề đối với trường hợp cụ thể của tôi, nhưng kiểm tra boolean cho isLoading sẽ khắc phục vấn đề.
Doguhan Uluca

55

Tôi thực sự cần cuộc gọi bắt nguồn từ phương thức get, do kiến ​​trúc tách rời của tôi. Vì vậy, tôi đã đưa ra cách thực hiện sau đây.

Cách sử dụng: Tiêu đề nằm trong ViewModel hoặc một đối tượng bạn có thể khai báo tĩnh dưới dạng tài nguyên trang. Liên kết với nó và giá trị sẽ được điền mà không chặn UI, khi getTitle () trả về.

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

9
updfrom 18/07/2012 trong Win8 RP, chúng ta nên thay đổi cuộc gọi của Dispatcher thành: Window.Civerse.CoreWindow.Dispatcher.RunAsync (CoreDispatcherP Warriority.N normal, async () => {Title = đang chờ GetTyussyAsync (url);});
Anton Sizikov

7
@ChristopherStevenson, tôi cũng nghĩ vậy, nhưng tôi không tin đó là trường hợp. Bởi vì getter đang được thực thi như một ngọn lửa và quên đi, mà không gọi setter sau khi hoàn thành, ràng buộc sẽ không được cập nhật khi getter kết thúc.
Iain

3
Không, nó đã và có một điều kiện cuộc đua, nhưng người dùng sẽ không thấy điều đó vì 'RaisePropertyChanged ("Tiêu đề")'. Nó trở lại trước khi nó hoàn thành. Nhưng, sau khi hoàn thành, bạn đang thiết lập tài sản. Điều đó kích hoạt sự kiện PropertyChanged. Binder được giá trị tài sản một lần nữa.
Medeni Baykal

1
Về cơ bản, getter đầu tiên sẽ trả về giá trị null, sau đó nó sẽ được cập nhật. Lưu ý rằng nếu chúng ta muốn getTitle được gọi mỗi lần, có thể có một vòng lặp xấu.
tofutim

1
Bạn cũng nên lưu ý rằng mọi ngoại lệ trong cuộc gọi không đồng bộ đó sẽ bị nuốt hoàn toàn. Họ thậm chí không tiếp cận với trình xử lý ngoại lệ chưa được xử lý của bạn trên ứng dụng nếu bạn có.
Philter

9

Tôi nghĩ rằng chúng ta có thể chờ đợi giá trị chỉ trả về null đầu tiên và sau đó nhận giá trị thực, vì vậy trong trường hợp Pure MVVM (ví dụ dự án PCL) Tôi nghĩ rằng đây là giải pháp thanh lịch nhất:

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

3
Điều này không tạo ra cảnh báo trình biên dịchCS4014: Async method invocation without an await expression
Nick

6
Hãy rất hoài nghi làm theo lời khuyên này. Xem video này sau đó làm cho tâm trí của bạn lên: channel9.msdn.com/Series/Three-Essential-Tips-for-Async/ù .
Contango

1
Bạn NÊN tránh sử dụng các phương pháp "async void"!
SuperJMN

1
mỗi tiếng hét sẽ đi kèm với một câu trả lời khôn ngoan, bạn có muốn @SuperJMN giải thích cho chúng tôi tại sao không?
Juan Pablo Garcia Coello

1
@Contango Video hay. Ông nói, " async voidChỉ sử dụng cho các trình xử lý cấp cao nhất và tương tự". Tôi nghĩ rằng điều này có thể đủ điều kiện là "và như của họ".
HappyNomad

7

Bạn có thể sử dụng Tasknhư thế này:

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

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

5

Tôi nghĩ .GetAwaiter (). GetResult () chính xác là giải pháp cho vấn đề này, phải không? ví dụ:

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

5
Điều đó giống như chỉ chặn với .Result- nó không đồng bộ và nó có thể dẫn đến bế tắc.
McGuireV10

bạn cần thêm IsAsync = True
Alexsandr Ter

Tôi đánh giá cao phản hồi về câu trả lời của tôi; Tôi thực sự yêu thích ai đó cung cấp một ví dụ về sự bế tắc này để tôi có thể thấy nó hoạt động
bc3tech

2

Vì "thuộc tính async" của bạn nằm trong chế độ xem, bạn có thể sử dụng AsyncMVVM :

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

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

Nó sẽ chăm sóc bối cảnh đồng bộ hóa và thông báo thay đổi thuộc tính cho bạn.


Là một tài sản, nó phải được.
Dmitry Shechtman

Xin lỗi, nhưng có lẽ tôi đã bỏ lỡ điểm của mã này. Bạn có thể giải thích?
Patrick Hofman

Các thuộc tính được chặn theo định nghĩa. GetTitleAsync () đóng vai trò là "async getter" thay đổi đường cú pháp.
Dmitry Shechtman

1
@DmitryShechtman: Không, nó không phải bị chặn. Đây chính xác là những gì thông báo thay đổi và máy trạng thái dành cho. Và họ không chặn theo định nghĩa. Chúng được đồng bộ theo định nghĩa. Điều này không giống như chặn. "Chặn" có nghĩa là họ có thể làm việc nặng và có thể tiêu tốn thời gian đáng kể để thực hiện. Đến lượt nó chính xác là những gì thuộc tính KHÔNG NÊN LÀM.
quetzalcoatl

1

Necromance.
Trong .NET Core / NetSt Chuẩn2, bạn có thể sử dụng Nito.AsyncEx.AsyncContext.Runthay vì 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;

            // /programming/6602244/how-to-call-an-async-method-from-a-getter-or-setter
            // /programming/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"));
    }


}

Nếu bạn chỉ đơn giản chọn System.Threading.Tasks.Task.Runhoặc System.Threading.Tasks.Task<int>.Run, thì nó sẽ không hoạt động.


-1

Tôi nghĩ ví dụ của tôi dưới đây có thể theo cách tiếp cận của @ Stephen-Cleary nhưng tôi muốn đưa ra một ví dụ được mã hóa. Điều này được sử dụng trong bối cảnh ràng buộc dữ liệu, ví dụ Xamarin.

Hàm tạo của lớp - hoặc thực sự là setter của một thuộc tính khác mà nó phụ thuộc - có thể gọi một khoảng trống không đồng bộ sẽ tạo ra thuộc tính khi hoàn thành nhiệm vụ mà không cần phải chờ đợi hoặc chặn. Khi cuối cùng nó nhận được một giá trị, nó sẽ cập nhật giao diện người dùng của bạn thông qua cơ chế NotifyPropertyChanged.

Tôi không chắc chắn về bất kỳ tác dụng phụ nào của việc gọi khoảng trống aysnc từ hàm tạo. Có lẽ một bình luận viên sẽ giải thích chi tiết về xử lý lỗi, v.v.

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


}

-1

Khi tôi gặp phải vấn đề này, việc cố gắng chạy đồng bộ phương thức async từ trình cài đặt hoặc hàm tạo đã khiến tôi gặp bế tắc trên luồng UI và sử dụng trình xử lý sự kiện đòi hỏi quá nhiều thay đổi trong thiết kế chung.
Giải pháp là, thường là, chỉ viết một cách rõ ràng những gì tôi muốn xảy ra ngầm, đó là có một luồng khác xử lý thao tác và lấy luồng chính để chờ nó kết thúc:

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

Bạn có thể lập luận rằng tôi lạm dụng khuôn khổ, nhưng nó hoạt động.


-1

Tôi xem lại tất cả câu trả lời nhưng tất cả đều có vấn đề về hiệu suất.

ví dụ trong:

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.C Hiện.Dispatcher.InvokeAsync (async () => {Title = await getTitle ();});

sử dụng điều phối không phải là một câu trả lời tốt.

nhưng có một giải pháp đơn giản, chỉ cần làm điều đó:

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

nếu chức năng của bạn là asyn, hãy sử dụng getTitle (). Wait () thay vì getTitle ()
Mahdi Rastegari

-4

Bạn có thể thay đổi proerty thành Task<IEnumerable>

và làm một cái gì đó như:

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

và sử dụng nó như đang chờ MyList;

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.