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, но я получаю следующую ошибку:

  1. Ошибка CS1061 'IAsyncEnumerable<int>"не содержит определения для"GetAwaiter"и нет доступного метода расширения"GetAwaiter"принятие первого аргумента типа"IAsyncEnumerable<int>'может быть найдено (вам не хватает директивы using или ссылки на сборку?)
  2. Ошибка 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 becoms nums.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,

Другие вопросы по тегам