Асинхронный итератор Задача <IEnumerable <T>>

Im пытается реализовать асинхронную функцию, которая возвращает итератор. Идея такова:

    private async Task<IEnumerable<char>> TestAsync(string testString)
    {
        foreach (char c in testString.ToCharArray())
        {
            // do other work
            yield return c;
        }
    }

Однако есть сообщение об ошибке, что функция не может быть блоком итератора, потому что Task<IEnumerable<char>> не является типом интерфейса итератора. Есть ли решение?

Ответ 1

Звучит так, как будто вы действительно ищете что-то вроде IObservable<T>, что-то вроде асинхронного IEnumerable<T> основе push. См. Reactive Extensions, также известный как Rx (код, лицензированный под Apache-2.0) (без присоединения), для огромного множества методов, которые работают с IObservable<T> чтобы заставить его работать как LINQ-to-Objects и многое другое.

Проблема с IEnumerable<T> состоит в том, что нет ничего, что действительно делает само перечисление асинхронным. Если вы не хотите добавлять зависимость от Rx (что на самом деле делает IObservable<T>), эта альтернатива может работать для вас:

public async Task<IEnumerable<char>> TestAsync(string testString)
{
    return GetChars(testString);
}

private static IEnumerable<char> GetChars(string testString)
{
    foreach (char c in testString.ToCharArray())
    {
        // do other work
        yield return c;
    }
}

хотя я хотел бы отметить, что, не зная, что на самом деле делается асинхронно, может быть гораздо лучший способ достичь ваших целей. Ни один из кода, который вы разместили, на самом деле ничего не делает асинхронно, и я не знаю, является ли что-нибудь в //do other work асинхронным (в этом случае это не решение вашей основной проблемы, хотя это сделает ваш код компиляции).

Обновление: IAsyncEnumerable

После С# 8 и .Net Standard 2.1. IAsyncEnumerable<T> может использоваться для этого, когда чтение из источника должно быть асинхронным (например, чтение из облачного потока).

public async void Run(string path)
{
    IAsyncEnumerable<string> lines = TestAsync(new StreamReader(path));
    await foreach (var line in lines)
    {
        Console.WriteLine(line);
    }
}

private async IAsyncEnumerable<string> TestAsync(StreamReader sr)
{
    while (true)
    {
        string line = await sr.ReadLineAsync();
        if (line == null)
            break;
        yield return line;
    }
}

Ответ 2

Чтобы разработать предыдущие ответы, вы можете использовать семейство методов Reactive Extensions 'Observable.Create<TResult>, чтобы сделать именно то, что вы хотите.

Вот пример:

var observable = Observable.Create<char>(async (observer, cancel) =>
{
    for (var i = 0; !cancel.IsCancellationRequested && i < 100; i++)
    {
        observer.OnNext(await GetCharAsync());
    }
});

Здесь вы можете использовать его в LINQPad, например:

// Create a disposable that keeps the query running.
// This is necessary, since the observable is 100% async.
var end = Util.KeepRunning();

observable.Subscribe(
    c => Console.WriteLine(c.ToString()),
    () => end.Dispose());