ValueTask
y ValueTask<TResult>
tienen un método Preserve()
que se resume como "Obtiene una ValueTask que se puede usar en cualquier momento en el futuro".
¿Qué significa eso y cuándo debemos usarlo? ¿Significa que una ValueTask
'normal' no se puede usar "en ningún momento en el futuro"? Si es así, ¿por qué?
La documentación no es muy clara. De la fuente :
/// <summary>null if representing a successful synchronous completion, /// otherwise a <see cref="Task"/> or a <see cref="IValueTaskSource"/>.</summary> internal readonly object? _obj;
ValueTask
declara internamente un objeto contenedor anulable para contener la Task
o IValueTaskSource
que ValueTask
encapsula en caso de que sea un ValueTask
que lo haga. De lo contrario, es null
si ValueTask
representa una finalización.
public ValueTask Preserve() => _obj == null ? this : new ValueTask(AsTask());
Preserve()
devuelve una nueva ValueTask
representa su tarea subyacente en el caso de que ValueTask
represente una Task
o un IValueTaskSource
(es decir: _obj != null
) o simplemente se devuelve a sí misma si ValueTask
representa una finalización síncrona exitosa. Es necesario porque _obj
es interno y no se puede probar externamente.
ValueTask
es una optimización de rendimiento sobre Task
s, pero este rendimiento tiene un costo: no puede usar ValueTask
tan libremente como Task
. La documentación menciona estas restricciones:
Las siguientes operaciones nunca deben realizarse en una instancia de
ValueTask
:
- Esperando la instancia varias veces.
- Llamando
AsTask
varias veces.- Usar más de una de estas técnicas para consumir la instancia.
Estas restricciones se aplican solo a las ValueTask
respaldadas por un IValueTaskSource
. Una ValueTask
también puede estar respaldada por una Task
o por un valor de TResult
(para una ValueTask<TResult>
). Si sabe con 100 % de certeza que una ValueTask
no está respaldada por un IValueTaskSource
, puede usarla con la misma libertad que una Task
. Por ejemplo, puede await
varias veces, o puede esperar sincrónicamente para que se complete con .GetAwaiter().GetResult()
.
En general, no sabe cómo se implementa internamente una ValueTask
, e incluso si lo sabe (al estudiar el código fuente), es posible que no desee confiar en un detalle de implementación. Con el método ValueTask.Preserve
, puede crear una nueva ValueTask
que represente la ValueTask
original y se puede usar sin restricciones porque no está creada por un IValueTaskSource
. Este método afecta a la ValueTask
original solo si está respaldada por un IValueTaskSource
; de lo contrario, no funciona (simplemente devuelve la tarea original sin cambios).
ValueTask preserved = originalValueTask.Preserve();
Después de llamar a Preserve
, se ha consumido la ValueTask
original. Ya no debe esperarse, Preserve
de nuevo ni convertirse en una Task
con el método AsTask
. Es probable que realizar cualquiera de esas acciones genere una InvalidOperationException
. Pero ahora tiene la representación preserved
de la misma, que se puede usar con la misma libertad que una Task
.
Mi respuesta incluía inicialmente un ejemplo práctico del uso del método Preserve
, que resultó ser incorrecto. Este ejemplo se puede encontrar en la cuarta revisión de esta respuesta.