Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Special-case empty enumerables in AsyncEnumerable #112321

Merged
merged 3 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ public static IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> AggregateBy<TSou
ThrowHelper.ThrowIfNull(keySelector);
ThrowHelper.ThrowIfNull(func);

return Impl(source, keySelector, seed, func, keyComparer, default);
return
source.IsKnownEmpty() ? Empty<KeyValuePair<TKey, TAccumulate>>() :
Impl(source, keySelector, seed, func, keyComparer, default);

static async IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> Impl(
IAsyncEnumerable<TSource> source,
Expand Down Expand Up @@ -117,7 +119,9 @@ public static IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> AggregateBy<TSou
ThrowHelper.ThrowIfNull(keySelector);
ThrowHelper.ThrowIfNull(func);

return Impl(source, keySelector, seed, func, keyComparer, default);
return
source.IsKnownEmpty() ? Empty<KeyValuePair<TKey, TAccumulate>>() :
Impl(source, keySelector, seed, func, keyComparer, default);

static async IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> Impl(
IAsyncEnumerable<TSource> source,
Expand Down Expand Up @@ -188,7 +192,9 @@ public static IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> AggregateBy<TSou
ThrowHelper.ThrowIfNull(seedSelector);
ThrowHelper.ThrowIfNull(func);

return Impl(source, keySelector, seedSelector, func, keyComparer, default);
return
source.IsKnownEmpty() ? Empty<KeyValuePair<TKey, TAccumulate>>() :
Impl(source, keySelector, seedSelector, func, keyComparer, default);

static async IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> Impl(
IAsyncEnumerable<TSource> source,
Expand Down Expand Up @@ -264,7 +270,9 @@ public static IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> AggregateBy<TSou
ThrowHelper.ThrowIfNull(seedSelector);
ThrowHelper.ThrowIfNull(func);

return Impl(source, keySelector, seedSelector, func, keyComparer, default);
return
source.IsKnownEmpty() ? Empty<KeyValuePair<TKey, TAccumulate>>() :
Impl(source, keySelector, seedSelector, func, keyComparer, default);

static async IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> Impl(
IAsyncEnumerable<TSource> source,
Expand All @@ -277,28 +285,26 @@ static async IAsyncEnumerable<KeyValuePair<TKey, TAccumulate>> Impl(
IAsyncEnumerator<TSource> enumerator = source.GetAsyncEnumerator(cancellationToken);
try
{
if (!await enumerator.MoveNextAsync().ConfigureAwait(false))
if (await enumerator.MoveNextAsync().ConfigureAwait(false))
{
yield break;
}
Dictionary<TKey, TAccumulate> dict = new(keyComparer);

Dictionary<TKey, TAccumulate> dict = new(keyComparer);

do
{
TSource value = enumerator.Current;
TKey key = await keySelector(value, cancellationToken).ConfigureAwait(false);
do
{
TSource value = enumerator.Current;
TKey key = await keySelector(value, cancellationToken).ConfigureAwait(false);

dict[key] = await func(
dict.TryGetValue(key, out TAccumulate? acc) ? acc : await seedSelector(key, cancellationToken).ConfigureAwait(false),
value,
cancellationToken).ConfigureAwait(false);
}
while (await enumerator.MoveNextAsync().ConfigureAwait(false));
dict[key] = await func(
dict.TryGetValue(key, out TAccumulate? acc) ? acc : await seedSelector(key, cancellationToken).ConfigureAwait(false),
value,
cancellationToken).ConfigureAwait(false);
}
while (await enumerator.MoveNextAsync().ConfigureAwait(false));

foreach (KeyValuePair<TKey, TAccumulate> countBy in dict)
{
yield return countBy;
foreach (KeyValuePair<TKey, TAccumulate> countBy in dict)
{
yield return countBy;
}
}
}
finally
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ public static IAsyncEnumerable<TResult> Cast<TResult>( // satisfies the C# query
{
ThrowHelper.ThrowIfNull(source);

return source is IAsyncEnumerable<TResult> result ?
result :
return
source.IsKnownEmpty() ? Empty<TResult>() :
source as IAsyncEnumerable<TResult> ??
Impl(source, default);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe parenthesize?


static async IAsyncEnumerable<TResult> Impl(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ public static IAsyncEnumerable<TSource[]> Chunk<TSource>(
ThrowHelper.ThrowIfNull(source);
ThrowHelper.ThrowIfNegativeOrZero(size);

return Chunk(source, size, default);
return
source.IsKnownEmpty() ? Empty<TSource[]>() :
Chunk(source, size, default);

async static IAsyncEnumerable<TSource[]> Chunk(
IAsyncEnumerable<TSource> source,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ public static IAsyncEnumerable<TSource> Concat<TSource>(
ThrowHelper.ThrowIfNull(first);
ThrowHelper.ThrowIfNull(second);

return Impl(first, second, default);
return
first.IsKnownEmpty() ? second :
second.IsKnownEmpty() ? first :
Impl(first, second, default);

static async IAsyncEnumerable<TSource> Impl(
IAsyncEnumerable<TSource> first,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ public static IAsyncEnumerable<KeyValuePair<TKey, int>> CountBy<TSource, TKey>(
ThrowHelper.ThrowIfNull(source);
ThrowHelper.ThrowIfNull(keySelector);

return Impl(source, keySelector, keyComparer, default);
return
source.IsKnownEmpty() ? Empty<KeyValuePair<TKey, int>>() :
Impl(source, keySelector, keyComparer, default);

static async IAsyncEnumerable<KeyValuePair<TKey, int>> Impl(
IAsyncEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey>? keyComparer, [EnumeratorCancellation] CancellationToken cancellationToken)
Expand Down Expand Up @@ -83,7 +85,9 @@ public static IAsyncEnumerable<KeyValuePair<TKey, int>> CountBy<TSource, TKey>(
ThrowHelper.ThrowIfNull(source);
ThrowHelper.ThrowIfNull(keySelector);

return Impl(source, keySelector, keyComparer, default);
return
source.IsKnownEmpty() ? Empty<KeyValuePair<TKey, int>>() :
Impl(source, keySelector, keyComparer, default);

static async IAsyncEnumerable<KeyValuePair<TKey, int>> Impl(
IAsyncEnumerable<TSource> source, Func<TSource, CancellationToken, ValueTask<TKey>> keySelector, IEqualityComparer<TKey>? keyComparer, [EnumeratorCancellation] CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ public static IAsyncEnumerable<TSource> Distinct<TSource>(
{
ThrowHelper.ThrowIfNull(source);

return Impl(source, comparer, default);
return
source.IsKnownEmpty() ? Empty<TSource>() :
Impl(source, comparer, default);

static async IAsyncEnumerable<TSource> Impl(
IAsyncEnumerable<TSource> source,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ public static IAsyncEnumerable<TSource> DistinctBy<TSource, TKey>(
ThrowHelper.ThrowIfNull(source);
ThrowHelper.ThrowIfNull(keySelector);

return Impl(source, keySelector, comparer, default);
return
source.IsKnownEmpty() ? Empty<TSource>() :
Impl(source, keySelector, comparer, default);

static async IAsyncEnumerable<TSource> Impl(
IAsyncEnumerable<TSource> source,
Expand Down Expand Up @@ -86,7 +88,9 @@ public static IAsyncEnumerable<TSource> DistinctBy<TSource, TKey>(
ThrowHelper.ThrowIfNull(source);
ThrowHelper.ThrowIfNull(keySelector);

return Impl(source, keySelector, comparer, default);
return
source.IsKnownEmpty() ? Empty<TSource>() :
Impl(source, keySelector, comparer, default);

static async IAsyncEnumerable<TSource> Impl(
IAsyncEnumerable<TSource> source,
Expand Down
21 changes: 19 additions & 2 deletions src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/Empty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ public static partial class AsyncEnumerable
/// <returns>An empty <see cref="IAsyncEnumerable{T}"/> whose type argument is <typeparamref name="TResult"/>.</returns>
public static IAsyncEnumerable<TResult> Empty<TResult>() => EmptyAsyncEnumerable<TResult>.Instance;

private sealed class EmptyAsyncEnumerable<TResult> : IAsyncEnumerable<TResult>, IAsyncEnumerator<TResult>
/// <summary>Determines whether <paramref name="source"/> is known to be an always-empty enumerable.</summary>
private static bool IsKnownEmpty<TResult>(this IAsyncEnumerable<TResult> source) =>
ReferenceEquals(source, EmptyAsyncEnumerable<TResult>.Instance);

private sealed class EmptyAsyncEnumerable<TResult> :
IAsyncEnumerable<TResult>, IAsyncEnumerator<TResult>, IOrderedAsyncEnumerable<TResult>
{
public static EmptyAsyncEnumerable<TResult> Instance { get; } = new EmptyAsyncEnumerable<TResult>();
public static readonly EmptyAsyncEnumerable<TResult> Instance = new();

public IAsyncEnumerator<TResult> GetAsyncEnumerator(CancellationToken cancellationToken = default) => this;

Expand All @@ -27,6 +32,18 @@ private sealed class EmptyAsyncEnumerable<TResult> : IAsyncEnumerable<TResult>,
public TResult Current => default!;

public ValueTask DisposeAsync() => default;

public IOrderedAsyncEnumerable<TResult> CreateOrderedAsyncEnumerable<TKey>(Func<TResult, TKey> keySelector, IComparer<TKey>? comparer, bool descending)
{
ThrowHelper.ThrowIfNull(keySelector);
return this;
}

public IOrderedAsyncEnumerable<TResult> CreateOrderedAsyncEnumerable<TKey>(Func<TResult, CancellationToken, ValueTask<TKey>> keySelector, IComparer<TKey>? comparer, bool descending)
{
ThrowHelper.ThrowIfNull(keySelector);
return this;
}
}
}
}
37 changes: 27 additions & 10 deletions src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/Except.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,44 @@ public static IAsyncEnumerable<TSource> Except<TSource>(
ThrowHelper.ThrowIfNull(first);
ThrowHelper.ThrowIfNull(second);

return Impl(first, second, comparer, default);
return
first.IsKnownEmpty() ? Empty<TSource>() :
Impl(first, second, comparer, default);

async static IAsyncEnumerable<TSource> Impl(
IAsyncEnumerable<TSource> first,
IAsyncEnumerable<TSource> second,
IEqualityComparer<TSource>? comparer,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
HashSet<TSource> set = new(comparer);

await foreach (TSource element in second.WithCancellation(cancellationToken).ConfigureAwait(false))
IAsyncEnumerator<TSource> firstEnumerator = first.GetAsyncEnumerator(cancellationToken);
try
{
set.Add(element);
}
if (!await firstEnumerator.MoveNextAsync().ConfigureAwait(false))
{
yield break;
}

await foreach (TSource element in first.WithCancellation(cancellationToken).ConfigureAwait(false))
{
if (set.Add(element))
HashSet<TSource> set = new(comparer);

await foreach (TSource element in second.WithCancellation(cancellationToken).ConfigureAwait(false))
{
yield return element;
set.Add(element);
}

do
{
TSource firstElement = firstEnumerator.Current;
if (set.Add(firstElement))
{
yield return firstElement;
}
}
while (await firstEnumerator.MoveNextAsync().ConfigureAwait(false));
}
finally
{
await firstEnumerator.DisposeAsync().ConfigureAwait(false);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ public static IAsyncEnumerable<TSource> ExceptBy<TSource, TKey>(
ThrowHelper.ThrowIfNull(second);
ThrowHelper.ThrowIfNull(keySelector);

return Impl(first, second, keySelector, comparer, default);
return
first.IsKnownEmpty() ? Empty<TSource>() :
Impl(first, second, keySelector, comparer, default);

static async IAsyncEnumerable<TSource> Impl(
IAsyncEnumerable<TSource> first,
Expand All @@ -42,19 +44,34 @@ static async IAsyncEnumerable<TSource> Impl(
IEqualityComparer<TKey>? comparer,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
HashSet<TKey> set = new(comparer);

await foreach (TKey key in second.WithCancellation(cancellationToken).ConfigureAwait(false))
IAsyncEnumerator<TSource> firstEnumerator = first.GetAsyncEnumerator(cancellationToken);
try
{
set.Add(key);
}
if (!await firstEnumerator.MoveNextAsync().ConfigureAwait(false))
{
yield break;
}

await foreach (TSource element in first.WithCancellation(cancellationToken).ConfigureAwait(false))
{
if (set.Add(keySelector(element)))
HashSet<TKey> set = new(comparer);

await foreach (TKey key in second.WithCancellation(cancellationToken).ConfigureAwait(false))
{
yield return element;
set.Add(key);
}

do
{
TSource firstElement = firstEnumerator.Current;
if (set.Add(keySelector(firstElement)))
{
yield return firstElement;
}
}
while (await firstEnumerator.MoveNextAsync().ConfigureAwait(false));
}
finally
{
await firstEnumerator.DisposeAsync().ConfigureAwait(false);
}
}
}
Expand Down Expand Up @@ -82,7 +99,9 @@ public static IAsyncEnumerable<TSource> ExceptBy<TSource, TKey>(
ThrowHelper.ThrowIfNull(second);
ThrowHelper.ThrowIfNull(keySelector);

return Impl(first, second, keySelector, comparer, default);
return
first.IsKnownEmpty() ? Empty<TSource>() :
Impl(first, second, keySelector, comparer, default);

static async IAsyncEnumerable<TSource> Impl(
IAsyncEnumerable<TSource> first,
Expand All @@ -91,19 +110,34 @@ static async IAsyncEnumerable<TSource> Impl(
IEqualityComparer<TKey>? comparer,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
HashSet<TKey> set = new(comparer);

await foreach (TKey key in second.WithCancellation(cancellationToken).ConfigureAwait(false))
IAsyncEnumerator<TSource> firstEnumerator = first.GetAsyncEnumerator(cancellationToken);
try
{
set.Add(key);
}
if (!await firstEnumerator.MoveNextAsync().ConfigureAwait(false))
{
yield break;
}

await foreach (TSource element in first.WithCancellation(cancellationToken).ConfigureAwait(false))
{
if (set.Add(await keySelector(element, cancellationToken).ConfigureAwait(false)))
HashSet<TKey> set = new(comparer);

await foreach (TKey key in second.WithCancellation(cancellationToken).ConfigureAwait(false))
{
yield return element;
set.Add(key);
}

do
{
TSource firstElement = firstEnumerator.Current;
if (set.Add(await keySelector(firstElement, cancellationToken).ConfigureAwait(false)))
{
yield return firstElement;
}
}
while (await firstEnumerator.MoveNextAsync().ConfigureAwait(false));
}
finally
{
await firstEnumerator.DisposeAsync().ConfigureAwait(false);
}
}
}
Expand Down
Loading
Loading