Bart
Bart

Reputation: 10015

Choosing between Task or void method for asynchronous code

I'm using the LoadState method as example, but this can be seen as a scenario for asynchronous programming in general.

The LoadState and SaveState method implementation typically have following signature:

public override void LoadState(..)
public async override void LoadState(..)

You can choose to add the async keyword depending on the fact that you want to await the loading of some data or not. But as the return type of LoadState is void, the LoadState method itself can not be awaited and is triggered as fire and forget. This is typically good, because it allows a responsive UI, while being able to use async/await in your data loading logic.

In some scenarios I'd like to await the async LoadState method. However, if I change the signature to Task instead of void, all my implementations that don't use async logic have to return null (which doesn't seem like the best solution). Is there another possible solution in which some method calls can be awaited and most stay fire and forget (as default)?

Upvotes: 2

Views: 1578

Answers (4)

Bart
Bart

Reputation: 10015

One possible solution I see is defining 2 methods in the base class (and an aditional 3rd for extra logic):

virtual void LoadState(){}
virtual async Task LoadStateAsync{ }

virtual void SomeOtherLogic(){}

Where you would call the LoadState, you now have 2 method calls:

LoadState();
await LoadStateAsync();

SomeOtherLogic();

In this solution you can override the LoadState method if SomeOtherLogic is empty or doesn't require data from the LoadState method. If you do require data, you await the LoadStateAsync method before executing SomeOtherLogic.

Downside is that you (and other developers) always have to pay attention which method is/should be used.

Upvotes: 0

Stephen Cleary
Stephen Cleary

Reputation: 457217

There are several problems with an async void method:

  • It's not composable; you can't await for it to complete (as you noted).
  • It's not easily testable (for the same reason).
  • Error handling is completely different; I've heard async void called "fire and forget" but I've used the term "fire and crash" myself.

Also, you should never return null from an Async method. That would be surprising to other coders used to TAP.

You can provide two methods void LoadState and Task LoadStateAsync, but if you do this I recommend you always provide both implementations. There is not a good easy way to wrap asynchronous methods in synchronous methods or synchronous methods in asynchronous methods, so you will actually end up with two almost-identical implementations.

I don't particularly like this for maintenance reasons. Personally, I prefer to have a possibly-async method always have an await-compatible signature:

interface IWhatever
{
  Task LoadStateAsync();
}

Implementations should be asynchronous or fast if they're synchronous. Synchronous implementations can make use of Task.FromResult:

class Whatever : IWhatever
{
  public Task LoadStateAsync()
  {
    ... // fast-running synchronous code
    return Task.FromResult<object>(null);
  }
}

The consuming code will always be async:

Whatever whatever = ...;
await whatever.LoadStateAsync();

Note that if the implementation is synchronous, the consuming code will not yield to its caller; it will continue running synchronously through the await.

Upvotes: 4

usr
usr

Reputation: 171246

You can implement your logic as a Task-returning method and have LoadState just discard the return value.

public override void LoadState() { LoadStateInner(); }
Task LoadStateInner() { await ...; }

Now you can call both variants while fulfilling the inheritance contract at the same time.

Upvotes: 0

Mike
Mike

Reputation: 418

There is a Loaded event I think which gets fired when the loading is complete. Perhaps you can do whatever it is you want to continue with in the event handler for the Loaded event?

Upvotes: 0

Related Questions