web-dev-qa-db-ja.com

非同期イテレーターTask <IEnumerable <T >>

イテレータを返す非同期関数を実装しようとしています。アイデアは次のとおりです。

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

ただし、関数がTask<IEnumerable<char>>は、イテレータインターフェイスタイプではありません。解決策はありますか?

28
user2341923

本当に探しているのは IObservable<T> のようなもので、Pushベースの非同期IEnumerable<T>のようなものです。 IObservable<T>と連携してLINQ-to-Objectsのように機能するメソッドの巨大なホストについては、 Reactive Extensions、aka Rx (Apache-2.0でライセンスされたコード)(所属なし)を参照してください。もっと。

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

C#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;
    }
}
22
Joe Amenta

以前の回答について詳しく説明するには、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());
14
John Gietzen

より多くの「バッテリーを含む」実装 言語サポートを含むこの種のものは、現在(執筆時点で) 考慮 であり、C#8.0に含まれています。

私が理解しているように、これはいつでも変更される可能性があります。

9
Joe Amenta

現在、反復子および非同期の両方であるメソッドを作成することはできません。 C#8.0の Async Streams 機能で可能になります。

現時点で可能なのは、タスクの反復子を持つことです。このようなもの:

private IEnumerable<Task<char>> GetChars(string testString)
{
    foreach (char c in testString.ToCharArray())
    {
        yield return Task.FromResult(c);
    }
}

そして、このように消費します:

foreach (var asyncChar in GetChars("Armadillo"))
{
    var c = await asyncChar;
}

このアプローチは、最も単純な場合にのみ実行可能です。たとえば、値を返す前に非同期条件をチェックする必要がある場合、運が悪いです。基本的に、手動で状態マシンを構築する必要があります。または、 AsyncEnumerable のようなライブラリを使用します。これにより、イテレータと列挙子の両方の構造を変更する必要があります。

private IAsyncEnumerable<char> GetChars(string testString)
{
    return new AsyncEnumerable<char>(async yield =>
    {
        foreach (char c in testString.ToCharArray())
        {
            await yield.ReturnAsync(c);
        }
    });
}

await GetChars("Armadillo").ForEachAsync(async c =>
{
    await Console.Out.WriteLineAsync(c);
});
0
Theodor Zoulias