Je jouais avec l'aperçu C # 8.0 et je ne peux pas faire fonctionner IAsyncEnumerable
.
J'ai essayé le suivant
public static async IAsyncEnumerable<int> Get()
{
for(int i=0; i<10; i++)
{
await Task.Delay(100);
yield return i;
}
}
J'ai fini par utiliser un paquet Nuget nommé AsyncEnumerator
, mais j'obtiens l'erreur suivante:
IAsyncEnumerable<int>
' ne contient pas de définition pour 'GetAwaiter
' et aucune méthode d'extension accessible 'GetAwaiter
' acceptant un premier argument de type 'IAsyncEnumerable<int>
' n'a pu être trouvée (il manque une directive using ou une référence Assembly?)Program.Get()
' ne peut pas être un bloc itérateur, car 'IAsyncEnumerable<int>
' n'est pas un type d'interface itérateur.Qu'est-ce que j'oublie ici?
C'est un bug dans le compilateur qui peut être corrigé en ajoutant en ajoutant quelques lignes de code trouvé ici :
namespace System.Threading.Tasks
{
using System.Runtime.CompilerServices;
using System.Threading.Tasks.Sources;
internal struct ManualResetValueTaskSourceLogic<TResult>
{
private ManualResetValueTaskSourceCore<TResult> _core;
public ManualResetValueTaskSourceLogic(IStrongBox<ManualResetValueTaskSourceLogic<TResult>> parent) : this() { }
public short Version => _core.Version;
public TResult GetResult(short token) => _core.GetResult(token);
public ValueTaskSourceStatus GetStatus(short token) => _core.GetStatus(token);
public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags);
public void Reset() => _core.Reset();
public void SetResult(TResult result) => _core.SetResult(result);
public void SetException(Exception error) => _core.SetException(error);
}
}
namespace System.Runtime.CompilerServices
{
internal interface IStrongBox<T> { ref T Value { get; } }
}
Comme l'explique Mads Torgersen dans Prenez C # 8 pour un tour :
Mais si vous essayez de le compiler et de l'exécuter, vous obtenez un nombre embarrassant d'erreurs. C’est parce que nous nous sommes un peu égarés et que les aperçus de .NET Core 3.0 et de Visual Studio 2019 n’étaient pas parfaitement alignés. Plus précisément, il existe un type d’implémentation que les itérateurs asynchrones exploitent, différent de ce à quoi le compilateur s’attend.
Vous pouvez résoudre ce problème en ajoutant à votre projet un fichier source distinct contenant ce code de pontage . Compilez à nouveau et tout devrait fonctionner correctement.
Mettre à jour
Il semble y avoir un autre bogue lorsque Enumerable.Range()
est utilisé dans l’itérateur asynchrone.
La méthode GetNumbersAsync()
du numéro se termine après seulement deux itérations:
static async Task Main(string[] args)
{
await foreach (var num in GetNumbersAsync())
{
Console.WriteLine(num);
}
}
private static async IAsyncEnumerable<int> GetNumbersAsync()
{
var nums = Enumerable.Range(0, 10);
foreach (var num in nums)
{
await Task.Delay(100);
yield return num;
}
}
Cela n'imprimera que:
0
1
Cela n'arrivera pas avec un tableau ou même une autre méthode d'itérateur:
private static async IAsyncEnumerable<int> GetNumbersAsync()
{
foreach (var num in counter(10))
{
await Task.Delay(100);
yield return num;
}
}
private static IEnumerable<int> counter(int count)
{
for(int i=0;i<count;i++)
{
yield return i;
}
}
Cela imprimera le résultat attendu:
0
1
2
3
4
5
6
7
8
9
Mise à jour 2
Cela semble également être un bogue connu: Async-Streams: l’itération s’arrête de bonne heure sur Core
En ce qui concerne le code de pontage nécessaire au bon fonctionnement des éléments énumérables Async, j’ai publié il y a quelques jours un NuGet qui fait exactement cela: CSharp8Beta.AsyncIteratorPrerequisites.Unofficial
Contrairement à la croyance populaire, le code suivant produit les résultats attendus:
private static async IAsyncEnumerable<int> GetNumbersAsync()
{
var nums = Enumerable.Range(0, 10).ToArray();
foreach (var num in nums)
{
await Task.Delay(100);
yield return num;
}
}
et c'est parce que le IEnumerable<int>
est en cours de matérialisation dans un tableau int
. Ce qui se terminerait réellement après deux itérations est une itération sur le IEnumerable<int>
lui-même, comme ceci:
var nums = Enumerable.Range(0, 10); // no more .ToArray()
foreach (var num in nums) {
Néanmoins, bien que transformer des requêtes en collections matérialisées puisse sembler une astuce, il n’est pas toujours souhaitable de mettre en mémoire tampon la séquence entière (perdant ainsi de la mémoire et du temps).
En ce qui concerne les performances, ce que j’ai trouvé, c’est que j’ai trouvé un wrapper presque zéro sur IEnumerable
qui le transformerait en IAsyncEnumerable
plus utilisant await foreach
au lieu de foreach
pour contourner le problème.
J'ai récemment publié une nouvelle version du paquet NuGet, qui inclut maintenant une méthode d'extension appelée ToAsync<T>()
pour IEnumerable<T>
en général, placée dans System.Collections.Generic
, qui ne fait que cela. La signature de la méthode est:
namespace System.Collections.Generic {
public static class EnumerableExtensions {
public static IAsyncEnumerable<T> ToAsync<T>(this IEnumerable<T> @this)
et lors de l'ajout du package NuGet à un projet .NET Core 3, on pourrait l'utiliser de la manière suivante:
using System.Collections.Generic;
...
private static async IAsyncEnumerable<int> GetNumbersAsync() {
var nums = Enumerable.Range(0, 10);
await foreach (var num in nums.ToAsync()) {
await Task.Delay(100);
yield return num;
}
}
}
Notez les deux changements:
foreach
devient await foreach
nums
becoms nums.ToAsync()
Le wrapper est aussi léger que possible et son implémentation est basée sur les classes suivantes (notez que l'utilisation de ValueTask<T>
comme imposé par IAsyncEnumerable<T>
et IAsyncEnumerator<T>
permet un nombre constant d'allocations de tas par foreach
):
public static class EnumerableExtensions {
public static IAsyncEnumerable<T> ToAsync<T>(this IEnumerable<T> @this) => new EnumerableAdapter<T>(@this);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IAsyncEnumerator<T> ToAsync<T>(this IEnumerator<T> @this) => new EnumeratorAdapter<T>(@this);
private sealed class EnumerableAdapter<T> : IAsyncEnumerable<T> {
private readonly IEnumerable<T> target;
public EnumerableAdapter(IEnumerable<T> target) => this.target = target;
public IAsyncEnumerator<T> GetAsyncEnumerator() => this.target.GetEnumerator().ToAsync();
}
private sealed class EnumeratorAdapter<T> : IAsyncEnumerator<T> {
private readonly IEnumerator<T> enumerator;
public EnumeratorAdapter(IEnumerator<T> enumerator) => this.enumerator = enumerator;
public ValueTask<bool> MoveNextAsync() => new ValueTask<bool>(this.enumerator.MoveNext());
public T Current => this.enumerator.Current;
public ValueTask DisposeAsync() {
this.enumerator.Dispose();
return new ValueTask();
}
}
}
Résumer:
Pour pouvoir écrire des méthodes de générateur asynchrone (async IAsyncEnumerable<int> MyMethod() ...
) et utiliser des éléments énumérés asynchrones (await foreach (var x in ...
), il suffit d’installer le NuGet dans votre projet.
Afin de contourner également l'arrêt prématuré de l'itération, assurez-vous que vous avez System.Collections.Generic
dans vos clauses using
, appelez .ToAsync()
sur votre IEnumerable
et transformez votre foreach
en un await foreach
.