2

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?

Xenoprimate
  • 7,691
  • 15
  • 58
  • 95
  • 3
    With the `IEnumerable>`, there's nothing to stop someone calling `delayedSequence.ToArray()` -- it will need to give you a full collection of pending tasks, which might make no sense or be downright impossible to create (what if you need to evaluate the previous item to know whether there *is* a next item, for example?). `IAsyncEnumerable` enforces that the next item can only be fetched once the previous one is fully available – canton7 Jan 21 '21 at 16:54
  • 2
    What happens after one of your `Task.Delay`s you conclude that there are *no more items to yield*? You're assuming you always know how many items to yield in advance. – Damien_The_Unbeliever Jan 21 '21 at 16:54
  • related: https://stackoverflow.com/questions/57126271/whats-the-difference-between-iasyncenumerablet-vs-ienumerabletaskt – nawfal Oct 29 '22 at 06:58

1 Answers1

8

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810