Introducing Asynchronous Return of ValueTask<T>

We use asynchronous methods for long-running, high-latency operations. And (obviously), since Task/Task<T> is the return, we always need to obtain an instance of one of these objects to return. The alternative, to return null, would force callers to always check for null before accessing the Task—an unreasonable and frustrating API from a usability perspective. Generally, the cost to create a Task/Task<T> is insignificant in comparison to the long-running, high-latency operation.

What happens, though, if the operation can be short-circuited and a result returned immediately? Consider, for example, compressing a buffer. If the amount of data is significant, performing the operation asynchronously makes sense. If, however, data is zero-length, then the operation can return immediately, and obtaining a (cached or new instance of) Task/Task<T> is pointless because there is no need for a task when the operation completes immediately. What is needed is a task-like object that can manage the asynchrony but not require the expense of a full Task/Task<T> when it isn’t needed. Support for additional valid async return types was added8—that is, types that support a GetAwaiter() method, as detailed in “Advanced Topic: Valid Async Return Types Expanded.”9

Listing 20.4 provides an example of file compression but escaping via ValueTask<T> if the compression can be short-circuited.

Listing 20.4: Returning ValueTask<T> from an async Method
using System.IO;
using System.Text;
using System.Threading.Tasks;
 
public static class Program
{
    public static async ValueTask<byte[]> CompressAsync(byte[] buffer)
    {
        if (buffer.Length == 0)
        {
            return buffer;
        }
        using MemoryStream memoryStream = new();
        using System.IO.Compression.GZipStream gZipStream =
            new(
                memoryStream, 
                    System.IO.Compression.CompressionMode.Compress);
        await gZipStream.WriteAsync(buffer.AsMemory(0, buffer.Length));
 
        return memoryStream.ToArray();
    }
    // ...
}

Notice that even though an asynchronous method, such as GZipStream.WriteAsync(), might return Task<T>, the await implementation still works within a ValueTask<T> returning method. In Listing 20.4, for example, changing the return from ValueTask<T> to Task<T> involves no other code changes.

The availability of ValueTask<T> raises the question of when to use it versus Task/Task<T>. If your operation doesn’t return a value, just use Task (there is no nongeneric ValueTask<T> because it has no benefit). Likewise, if your operation is likely to complete asynchronously, or if it’s not possible to cache tasks for common result values, Task<T> is preferred. For example, there’s generally no benefit to returning ValueTask<bool> instead of Task<bool>, because you can easily cache a Task<bool> for both true and false values—and in fact, the async infrastructure does this automatically. In other words, when returning an asynchronous Task<bool> method that completes synchronously, a cached result Task<bool> will return regardless. If, however, the operation is likely to complete synchronously and you can’t reasonably cache all common return values, ValueTask<T> might be appropriate.

Beginner Topic
Common Async Return Types Explained

The expression that follows the await keyword is most frequently of type Task, Task<T>, or ValueTask<T>. From a syntax perspective, an await operating on type Task is essentially the equivalent of an expression that returns void. In fact, because the compiler does not even know whether the task has a result, much less which type it is, such an expression is classified in the same way as a call to a void-returning method; that is, you can use it only in a statement context. Listing 20.5 shows some await expressions used as statement expressions.

Listing 20.5: An await Expression May Be a Statement Expression
public async Task<int> DoStuffAsync()
{
    await DoSomethingAsync();
    await DoSomethingElseAsync();
    return await GetAnIntegerAsync() + 1;
}

Here we presume that the first methods return a Task rather than a Task<T> or ValueTask<T>. Since there is no result value associated with the first two tasks, awaiting them produces no value; thus the expression must appear as a statement. The third task is presumably of type Task<int>, and its value can be used in the computation of the value of the task returned by DoStuffAsync().

________________________________________

8. Starting in C# 7.0.
9. C# 7.0–related .NET frameworks include ValueTask<T>, a value type that scales down to support lightweight instantiation when a long-running operation can be short-circuited or that can be converted to a full Task otherwise.
{{ snackbarMessage }}
;