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.
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.