IAsyncEnumerable не работает в C# 8.0 превью
Я играл с предварительным просмотром C# 8.0 и не могу получить IAsyncEnumerable
работать.
Я попробовал следующее
public static async IAsyncEnumerable<int> Get()
{
for(int i=0; i<10; i++)
{
await Task.Delay(100);
yield return i;
}
}
Я использовал пакет Nuget с именем AsyncEnumerator
, но я получаю следующую ошибку:
- Ошибка CS1061 '
IAsyncEnumerable<int>
"не содержит определения для"GetAwaiter
"и нет доступного метода расширения"GetAwaiter
"принятие первого аргумента типа"IAsyncEnumerable<int>
'может быть найдено (вам не хватает директивы using или ссылки на сборку?) - Ошибка CS1624 Тело '
Program.Get()
'не может быть блоком итератора, потому что'IAsyncEnumerable<int>
не является типом интерфейса итератора
Что мне здесь не хватает?
2 ответа
Это ошибка в компиляторе, которую можно исправить, добавив несколько строк кода, найденных здесь:
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; } }
}
Как объясняет Мадс Торгерсен в Take C# 8 для вращения:
Но если вы попытаетесь скомпилировать и запустить его, вы получите смущающее количество ошибок. Это потому, что мы немного запутались и не смогли полностью согласовать превью.NET Core 3.0 и Visual Studio 2019. В частности, есть тип реализации, который используют асинхронные итераторы, который отличается от того, что ожидает компилятор.
Вы можете исправить это, добавив отдельный исходный файл в ваш проект, содержащий этот мостовой код. Снова скомпилируйте, и все должно работать нормально.
Обновить
Похоже, есть еще одна ошибка, когда Enumerable.Range()
используется внутри асинхронного итератора.
GetNumbersAsync()
Метод в выпуске заканчивается только после двух итераций:
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;
}
}
Это будет печатать только:
0
1
Это не произойдет с массивом или даже с другим методом итератора:
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;
}
}
Это напечатает ожидаемое:
0
1
2
3
4
5
6
7
8
9
Обновление 2
Кажется, это также известная ошибка: Async-Streams: итерация останавливается рано в Core
Что касается связующего кода, необходимого для работы перечислимых Async, я опубликовал NuGet пару дней назад, который делает именно это: https://www.nuget.org/packages/CSharp8Beta.AsyncIteratorPrerequisites.Unofficial/
Вопреки распространенному мнению, следующий код на самом деле дает ожидаемые результаты:
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;
}
}
и это потому, что IEnumerable<int>
материализуется в int
массив. То, что фактически закончилось бы после двух итераций, повторяется по IEnumerable<int>
сам вроде так
var nums = Enumerable.Range(0, 10); // no more .ToArray()
foreach (var num in nums) {
Тем не менее, хотя превращение запросов в материализованные коллекции может показаться хитрым трюком, вы не всегда хотите буферизовать всю последовательность (таким образом теряя память и время).
Учитывая производительность, я обнаружил, что оболочка с почти нулевым распределением над IEnumerable
что бы превратить его в IAsyncEnumerable
плюс использование await foreach
вместо просто foreach
обойдет проблему.
Недавно я опубликовал новую версию пакета NuGet, который теперь включает метод расширения под названием ToAsync<T>()
за IEnumerable<T>
в общем, помещен в System.Collections.Generic
который делает именно это. Подпись метода:
namespace System.Collections.Generic {
public static class EnumerableExtensions {
public static IAsyncEnumerable<T> ToAsync<T>(this IEnumerable<T> @this)
и после добавления пакета NuGet в проект.NET Core 3 его можно использовать так:
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;
}
}
}
Обратите внимание на два изменения:
foreach
становитсяawait foreach
nums
becomsnums.ToAsync()
Оболочка настолько легка, насколько это возможно, и ее реализация основана на следующих классах (обратите внимание, что использование ValueTask<T>
в соответствии с IAsyncEnumerable<T>
а также IAsyncEnumerator<T>
допускает постоянное количество распределений кучи на 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();
}
}
}
Подвести итог:
Уметь писать методы асинхронного генератора (
async IAsyncEnumerable<int> MyMethod() ...
) и потреблять асинхронные перечислимые значения (await foreach (var x in ...
) просто установите NuGet в свой проект.Для того чтобы обойти итерацию преждевременной остановки, убедитесь, что у вас есть
System.Collections.Generic
в вашемusing
пункты, вызов.ToAsync()
на вашеIEnumerable
и включиforeach
вawait foreach
,