Search code examples
c#yield-returniasyncenumerable

What's the difference between IAsyncEnumerable<T> and an iterator-generated IEnumerable<Task<T>>?


I'm trying to work out the advantage that IAsyncEnumerable<T> brings over something like an IEnumerable<Task<T>>.

I wrote the following class that allows me to wait for a sequence of numbers with a defined delay between each one:

class DelayedSequence : IAsyncEnumerable<int>, IEnumerable<Task<int>> {
    readonly int _numDelays;
    readonly TimeSpan _interDelayTime;

    public DelayedSequence(int numDelays, TimeSpan interDelayTime) {
        _numDelays = numDelays;
        _interDelayTime = interDelayTime;
    }

    public IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default) {
        async IAsyncEnumerable<int> ConstructEnumerable() {
            for (var i = 0; i < _numDelays; ++i) {
                await Task.Delay(_interDelayTime, cancellationToken);
                yield return i;
            }
        }

        return ConstructEnumerable().GetAsyncEnumerator();
    }

    public IEnumerator<Task<int>> GetEnumerator() {
        IEnumerable<Task<int>> ConstructEnumerable() {
            for (var i = 0; i < _numDelays; ++i) {
                yield return Task.Delay(_interDelayTime).ContinueWith(_ => i);
            }
        }

        return ConstructEnumerable().GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

This class implements both IAsyncEnumerable<int> and IEnumerable<Task<int>>. I can iterate over it using both await foreach and foreach and get an identical result:

var delayedSequence = new DelayedSequence(5, TimeSpan.FromSeconds(1d));

await foreach (var i in delayedSequence) {
    Console.WriteLine(i);
}

foreach (var t in delayedSequence) {
    Console.WriteLine(await t);
}

Both iterations display the numbers 0 to 4 with a second's delay between each line.

Is the only advantage relating to the ability to cancel (i.e. the passed-in cancellationToken)? Or is there some scenario I'm not seeing here?


Solution

  • wait for a sequence of numbers with a defined delay between each one

    The delay happens at different times. IEnumerable<Task<T>> immediately returns the next element, which is then awaited. IAsyncEnumerable<T> awaits the next element.

    IEnumerable<Task<T>> is a (synchronous) enumeration where each element is asynchronous. This is the proper type to use when you have a known number of actions to perform and then each item asynchronously arrives independently.

    For example, this type is commonly used when sending out multiple REST requests simultaneously. The number of REST requests to make is known at the start, and each request is Selected into an asynchronous REST call. The resulting enumerable (or collection) is then usually passed to await Task.WhenAll to asynchronously wait for them all to complete.

    IAsyncEnumerable<T> is an asynchronous enumeration; i.e., its MoveNext is actually an asynchronous MoveNextAsync. This is the proper type to use when you have an unknown number of items to iterate, and getting the next (or next batch) is (potentially) asynchronous.

    For example, this type is commonly used when paging results from an API. In this case, you have no idea how many elements will eventually be returned. In fact, you may not even know if you are at the end until after retrieving the next page of results. So even determining whether there is a next item is an asynchronous operation.