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 documentation isn't very clear. From the source :
/// <summary>null if representing a successful synchronous completion,
/// otherwise a <see cref="Task"/> or a <see cref="IValueTaskSource"/>.</summary>
internal readonly object? _obj;
ValueTask
internally declares a nullable container object to hold either the Task
or IValueTaskSource
that the ValueTask
encapsulates should it be a ValueTask
that does so. It is otherwise null
if the ValueTask
instead is representing a completion.
public ValueTask Preserve() => _obj == null ? this : new ValueTask(AsTask());
Preserve()
returns a new ValueTask
representing its underlying task in the case that the ValueTask
represents a Task
or an IValueTaskSource
(ie: _obj != null
) or otherwise just returns itself if the ValueTask
represents a successful synchronous completion. It's necessary because _obj
is internal and can't be tested externally.
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 baked by an IValueTaskSource
. 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 task 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
.
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.