Say I have a method signature like this:
public async ValueTask<int> GetSomeValueAsync(int number);
Internally it may make an async HTTP call but it caches the result so next time it is called with the same number
it can return the value from cache without the async call. Thus it returns a ValueTask
as this avoids the overhead of creating a Task
for the synchronous path of execution.
My question is, the caller of this method, if it makes no other async calls, should the signature be a Task
or should ValueTask
be propagated all the way up the call stack?
E.g. should it be this:
public async Task<int> CallingMethodAsync(int number)
{
return await GetSomeValueAsync(number) + 1;
}
Or should it be:
public async ValueTask<int> CallingMethodAsync(int number)
{
return await GetSomeValueAsync(number) + 1;
}
(examples above are simplified to illustrate the point)
For clarity, this question is not about whether GetSomeValueAsync()
should return ValueTask
or Task
. I understand there are situations for both but take it as such that this evaluation has been made the outcome of that has correctly determined it should be ValueTask
. The essence of the question is, should the caller of this method propagate up ValueTask
in it's signature if it itself has no other async method call. Hopefully this clarification explains why the question, does not answer it.
Assuming that the GetSomeValueAsync
is expected to complete synchronously most of the time, and the CallingMethodAsync
doesn't await
anything else other than the GetSomeValueAsync
, and also your goal is to minimize the memory allocations, then the ValueTask<TResult>
is preferable.
public async ValueTask<int> CallingMethodAsync(int number)
{
return await GetSomeValueAsync(number) + 1;
}
That's because when a ValueTask<TResult>
is already completed upon creation, it wraps internally a TResult
, not a Task<TResult>
, so it doesn't allocate anything. On the contrary if you return a Task<TResult>
, a new object will always have to be created, unless it happens to be one of the few type+value combinations that are internally cached by the .NET runtime itself, like a Task<int>
with a value between -1 and 9 (see the internal static TaskCache
class).
In my PC running on .NET 7 Release mode it outputs:
CallingMethodAsyncValueTask: 0 bytes per operation
CallingMethodAsyncTask: 74 bytes per operation