C# Take(k) 扩展方法会依次执行一个完整的前一个GroupBy吗?
Will C# Take(k) extension method execute a complete previous GroupBy in sequence?
我 "playing" 使用 LINQ 并测试了一些东西,一些东西引起了我的注意。
假设我有 GroupBy
扩展方法的 "lazy" 实现:
public static IEnumerable<IGrouping<TKey, TSource>> GroupByA<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector)
{
//To avoid duplicate groups
List<TKey> grouping = new List<TKey>();
foreach (var item in source)
{
if (!grouping.Contains(keySelector(item)))
{
grouping.Add(keySelector(item));
Group<TKey, TSource> g = new Group<TKey, TSource>(
keySelector(item),
source.Where(x => keySelector(x).Equals(keySelector(item)))
);
Console.WriteLine("Returning group");
yield return g; //yield returning a complete group
}
}
}
注意:假设 Group<TKey, TSource>
实施 IGrouping<TKey, TSource
我在想,如果执行这个会发生什么?
var groups = students.GroupByA(x => x.Group).Take(2);
注意:students
是 List<Student>
。
.Take(2)
会强制执行完整的 .GroupByA(x=>x.Group)
还是会以某种方式一次消耗一组直到它计数 2
? 要么方式 为什么?
PS:我尝试使用我自己的实现:
public static IEnumerable<T> TakeA<T>(this IEnumerable<T> source, int count)
像这样:
public static IEnumerable<T> TakeA<T>(this IEnumerable<T> source, int count)
{
int iter = 0;
foreach (var item in source)
{
if (iter == count)
yield break;
yield return item;
iter++;
}
}
但我很确定这种方式会导致 GroupBy
在调用 TakeA
之前完全执行。我不知道这是我的实现方式还是以某种方式 original Take
做了一些不同的事情。
C# 编译器将您的代码转换为状态机。也就是说,它在幕后创建了一个新的 class,其中包含迭代学生列表所需的状态和行为。每次调用代码时,您都会获得此 class.
的一个实例
Will .Take(2) force the complete .GroupByA(x=>x.Group) execution
查看完整的 students.GroupByA(x => x.Group).Take(2)
表达式,.Net 能够使用由 GroupByA()
创建的新 class 实例和 Take()
函数,您可以将其视为执行仅持续到您的代码第二次到达 yield
行,但不会继续执行。
但是,GROUP BY 操作的本质是您必须遍历整个数据集才能了解组的属性,这意味着即使您只看到第二个 yield
表达式,source.Where()
调用仍然需要查看您的整个数据集并至少进行一次 O(n*m)
操作...每次您识别一个新组时,您都会再次检查整个数据集。
应该可以使用字典而不是列表来编写 O(n)
GROUP BY 操作,以查找新组并在字典值中累积聚合信息。你可能想看看你是否能做到这一点。当然,问题在于 n
的值较小(源列表大小较小),哈希计算和查找的成本可能高于序列迭代。
我 "playing" 使用 LINQ 并测试了一些东西,一些东西引起了我的注意。
假设我有 GroupBy
扩展方法的 "lazy" 实现:
public static IEnumerable<IGrouping<TKey, TSource>> GroupByA<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector)
{
//To avoid duplicate groups
List<TKey> grouping = new List<TKey>();
foreach (var item in source)
{
if (!grouping.Contains(keySelector(item)))
{
grouping.Add(keySelector(item));
Group<TKey, TSource> g = new Group<TKey, TSource>(
keySelector(item),
source.Where(x => keySelector(x).Equals(keySelector(item)))
);
Console.WriteLine("Returning group");
yield return g; //yield returning a complete group
}
}
}
注意:假设 Group<TKey, TSource>
实施 IGrouping<TKey, TSource
我在想,如果执行这个会发生什么?
var groups = students.GroupByA(x => x.Group).Take(2);
注意:students
是 List<Student>
。
.Take(2)
会强制执行完整的 .GroupByA(x=>x.Group)
还是会以某种方式一次消耗一组直到它计数 2
? 要么方式 为什么?
PS:我尝试使用我自己的实现:
public static IEnumerable<T> TakeA<T>(this IEnumerable<T> source, int count)
像这样:
public static IEnumerable<T> TakeA<T>(this IEnumerable<T> source, int count)
{
int iter = 0;
foreach (var item in source)
{
if (iter == count)
yield break;
yield return item;
iter++;
}
}
但我很确定这种方式会导致 GroupBy
在调用 TakeA
之前完全执行。我不知道这是我的实现方式还是以某种方式 original Take
做了一些不同的事情。
C# 编译器将您的代码转换为状态机。也就是说,它在幕后创建了一个新的 class,其中包含迭代学生列表所需的状态和行为。每次调用代码时,您都会获得此 class.
的一个实例Will .Take(2) force the complete .GroupByA(x=>x.Group) execution
查看完整的 students.GroupByA(x => x.Group).Take(2)
表达式,.Net 能够使用由 GroupByA()
创建的新 class 实例和 Take()
函数,您可以将其视为执行仅持续到您的代码第二次到达 yield
行,但不会继续执行。
但是,GROUP BY 操作的本质是您必须遍历整个数据集才能了解组的属性,这意味着即使您只看到第二个 yield
表达式,source.Where()
调用仍然需要查看您的整个数据集并至少进行一次 O(n*m)
操作...每次您识别一个新组时,您都会再次检查整个数据集。
应该可以使用字典而不是列表来编写 O(n)
GROUP BY 操作,以查找新组并在字典值中累积聚合信息。你可能想看看你是否能做到这一点。当然,问题在于 n
的值较小(源列表大小较小),哈希计算和查找的成本可能高于序列迭代。