ValueTask
and ValueTask<TResult>
have a Preserve()
method which is summarized as "Gets a ValueTask that may be used at any point in the future."
What does that mean and when should we use it? Does it imply that a 'normal' ValueTask
can't be used "at any point in the future"? If so, why?
The ValueTask
is a performance optimization over a Task
s, but this performance comes with a cost: You cannot use a ValueTask
as freely as a Task
. The documentation mentions these restrictions:
The following operations should never be performed on a
ValueTask
instance:
- Awaiting the instance multiple times.
- Calling
AsTask
multiple times.- Using more than one of these techniques to consume the instance.
These restrictions apply only for ValueTask
s that are backed by a IValueTaskSource
. A ValueTask
can also be backed by a Task
, or by a TResult
value (for a ValueTask<TResult>
). If you know with 100% certainty that a ValueTask
is not backed by an IValueTaskSource
, you can use it as freely as a Task
. For example you can await
is multiple times, or you can wait it synchronously to complete with .GetAwaiter().GetResult()
.
In general you don't know how a ValueTask
is implemented internally, and even if you know (by studying the source code) you may not want to rely on an implementation detail. With the ValueTask.Preserve
method you can create a new ValueTask
that represents the original ValueTask
, and can be used without restrictions because it's not backed by an IValueTaskSource
(it is backed by either a Task
or a result). This method affects the original ValueTask
only if it's backed by an IValueTaskSource
, otherwise it's a no-op (it just returns the original ValueTask
unaltered).
ValueTask preserved = originalValueTask.Preserve();
After calling Preserve
, the original ValueTask
has been consumed. It should no longer be awaited, or Preserve
d again, or converted to Task
with the AsTask
method. Doing any of those actions is likely to result in an InvalidOperationException
. But now you have the preserved
representation of it, which can be used with the same freedom as a Task
.
Performance note: The designers of the Preserve
API have assumed that most of the time a ValueTask
is going to be preserved immediately after its creation. So they haven't optimized the Preserve
implementation for the case that the Preserve
is called later, and meanwhile the initially incomplete ValueTask
has been completed. In this case a needless allocation is going to occur. You can learn more about this decision, and the rationale behind it, in this GitHub issue.
My answer included initially a practical example of using the Preserve
method, which proved to be incorrect. This example can be found in the 4th revision of this answer.