ValueTask<T>
n'est pas un sous-ensemble de Task<T>
, c'est un sur-ensemble .
ValueTask<T>
est une union discriminée d'un T et d'un Task<T>
, ce qui le rend libre d'allocation pour ReadAsync<T>
renvoyer de manière synchrone une valeur T dont il dispose (contrairement à using Task.FromResult<T>
, qui doit allouer une Task<T>
instance). ValueTask<T>
est attendue, donc la plupart des consommations d'instances seront impossibles à distinguer d'un Task<T>
.
ValueTask, étant une structure, permet d'écrire des méthodes asynchrones qui n'allouent pas de mémoire lorsqu'elles s'exécutent de manière synchrone sans compromettre la cohérence de l'API. Imaginez avoir une interface avec une méthode de retour de tâche. Chaque classe implémentant cette interface doit renvoyer une tâche même si elle s'exécute de manière synchrone (avec un peu de chance en utilisant Task.FromResult). Vous pouvez bien sûr avoir 2 méthodes différentes sur l'interface, une synchrone et une asynchrone mais cela nécessite 2 implémentations différentes pour éviter «sync over async» et «async over sync».
Ainsi, il vous permet d'écrire une méthode soit asynchrone ou synchrone, plutôt que d'écrire une méthode par ailleurs identique pour chacune. Vous pouvez l'utiliser partout où vous l'utilisez, Task<T>
mais cela n'ajoute souvent rien.
Eh bien, cela ajoute une chose: cela ajoute une promesse implicite à l'appelant que la méthode utilise réellement la fonctionnalité supplémentaire ValueTask<T>
fournie. Personnellement, je préfère choisir des types de paramètres et de retours qui en disent le plus possible à l'appelant. Ne revenez pas IList<T>
si l'énumération ne peut pas fournir de décompte; ne revenez pas IEnumerable<T>
si c'est possible. Vos consommateurs ne devraient pas avoir à consulter de documentation pour savoir laquelle de vos méthodes peut raisonnablement être appelée de manière synchrone et laquelle ne le peut pas.
Je ne vois pas les futurs changements de conception comme un argument convaincant. Bien au contraire: si une méthode change sa sémantique, elle doit interrompre la compilation jusqu'à ce que tous les appels à celle-ci soient mis à jour en conséquence. Si cela est considéré comme indésirable (et croyez-moi, je suis sensible au désir de ne pas casser la construction), envisagez la gestion des versions de l'interface.
C'est essentiellement à cela que sert le typage fort.
Si certains des programmeurs qui conçoivent des méthodes asynchrones dans votre boutique ne sont pas en mesure de prendre des décisions éclairées, il peut être utile d'affecter un mentor senior à chacun de ces programmeurs moins expérimentés et de faire une révision hebdomadaire du code. S'ils se trompent, expliquez pourquoi cela devrait être fait différemment. C'est une surcharge pour les seniors, mais cela amènera les juniors à la vitesse beaucoup plus rapidement que de simplement les jeter dans le grand bain et de leur donner une règle arbitraire à suivre.
Si le type qui a écrit la méthode ne sait pas si elle peut être appelée de manière synchrone, qui sur Terre le fait?!
Si vous avez autant de programmeurs inexpérimentés qui écrivent des méthodes asynchrones, ces mêmes personnes les appellent-elles également? Sont-ils qualifiés pour déterminer eux-mêmes lesquels sont sûrs d'appeler async, ou vont-ils commencer à appliquer une règle tout aussi arbitraire à la façon dont ils appellent ces choses?
Le problème ici n'est pas vos types de retour, ce sont les programmeurs qui sont placés dans des rôles pour lesquels ils ne sont pas prêts. Cela a dû arriver pour une raison, donc je suis sûr que cela ne peut pas être trivial à réparer. Décrire ce n'est certainement pas une solution. Mais chercher un moyen de faire passer le problème au-delà du compilateur n'est pas non plus une solution.
ValueTask<T>
(en termes d'allocations) de ne pas se matérialiser pour des opérations qui sont réellement asynchrones (car dans ce cas, ilValueTask<T>
faudra toujours une allocation de tas). Il y a aussi la question d'Task<T>
avoir beaucoup d'autres supports au sein des bibliothèques.