admin管理员组

文章数量:1405549

I'm a little bit annoyed of parentheses in situations like this:

var items = (await SomeService.GetDataAsEnumerableAsync()).ToList();

So I thought of creating an extension method like:

public static class TaskEnumerableExtensions
{
    public static async Task<T> FirstAsync<T>(this Task<IEnumerable<T>> task)
    {
        return (await task).First();
    }

    public static async Task<T?> FirstOrDefaultAsync<T>(this Task<IEnumerable<T>> task)
    {
        return (await task).FirstOrDefault();
    }
}

which would allow me to write:

var items = await SomeService.GetDataAsEnumerableAsync().ToListAsync();

But I'm a little bit afraid of potential side effects.

Are extension methods like this valid or do they change some "core" TPL behavior like exception handling? Or do they add some noticeable overhead if used in a loop?

Thanks in advance!

I'm a little bit annoyed of parentheses in situations like this:

var items = (await SomeService.GetDataAsEnumerableAsync()).ToList();

So I thought of creating an extension method like:

public static class TaskEnumerableExtensions
{
    public static async Task<T> FirstAsync<T>(this Task<IEnumerable<T>> task)
    {
        return (await task).First();
    }

    public static async Task<T?> FirstOrDefaultAsync<T>(this Task<IEnumerable<T>> task)
    {
        return (await task).FirstOrDefault();
    }
}

which would allow me to write:

var items = await SomeService.GetDataAsEnumerableAsync().ToListAsync();

But I'm a little bit afraid of potential side effects.

Are extension methods like this valid or do they change some "core" TPL behavior like exception handling? Or do they add some noticeable overhead if used in a loop?

Thanks in advance!

Share Improve this question asked Mar 21 at 23:19 David RitterDavid Ritter 93 bronze badges 1
  • 4 IAsyncEnumerable<T> (without Task<T>) could seem a more appropriate solution, depending on the intent. – Marc Gravell Commented Mar 22 at 0:02
Add a comment  | 

2 Answers 2

Reset to default 2

The Task<IEnumerable<T>> abstraction doesn't make much sense. The Task<T> represents the result of an asynchronous operation that will complete in the future, and the IEnumerable<T> represents a sequence of elements that are generated one at a time, with the generation of the next element being postponed until the element is requested (deferred execution). So both abstractions represent something that will be available in the future. Combining the two abstractions results in a nested expectation, something like a promise for a gift in the Christmas, and then wait until the Christmas only to find out that the gift is a ticket for a flight scheduled for the next summer.

Moreover the two expectations are expressed differently. The operation represented by the Task<T> is asynchronous, so you can wait for it without blocking a thread, while the operations that unwind the IEnumerable<T> are synchronous, so you have to block a thread each time you wait for the next element in the sequence. So it's like being able to do other things until your Christmas gift is revealed, but then having to be at the airport continuously until the boarding of the flight. How much sense does this make?

For these reasons an abstraction better than the Task<IEnumerable<T>> has been invented, namely the IAsyncEnumerable<T>. This represents a deferred sequence whose elements are obtained asynchronously. The enumerator of this sequence has an asynchronous MoveNextAsync method that returns a ValueTask<bool>, instead of a synchronous MoveNext method that returns a bool. Usually the first MoveNextAsync() does more work than the rest, because it warms up the generator of the sequence, for example by establishing an HTTP or DB connection.

A library that offers the standard LINQ functionality for asynchronous sequences already exists: System.Linq.Async. Currently it's a separate NuGet package, but it is going to be natively available starting from .NET 10. Another existing library, System.Interactive.Async, offers more advanced operators (extension methods) for asynchronous sequences, like Merge, Retry, Using, Finally etc.

Such convenience extension methods would introduce a bit of overhead.

For one, another state machine object for the extension method could be allocated if the operation isn't already completed. This will include a bit of infrastructure objects like captured ExecutionContext which might present the biggest overhead since it's supposed to be immutable and carries a bag of AsyncLocal items. As always best to profile / benchmark the differences to see if it's a problem for you in loops.

Also, with your current definition, you cannot pass ConfigureAwait options - so, you can consider doing this as well.

public static async Task<T?> FirstOrDefaultAsync<T>(this Task<IEnumerable<T>> task, bool continueOnCapturedContext=true)
{
    return (await task.ConfigureAwait(continueOnCapturedContext)).FirstOrDefault();
}

Also note the more modern (.NET 8+) ConfigureAwait options that you might want to pass.

As for exceptions, they will likely include a bit more in the stack trace but the behavior won't change. The exception will just propagate from the extension method to your call site.

本文标签: cAny side effects when writing LINQlike extensions for TaskltIEnumerableltTgtgtStack Overflow