现有的 Linq 函数有没有办法从项目列表中创建任意大小的组?
例如:
[1,2,3,4,5,6,7]
当执行 list.Group(3) 之类的操作时,会产生一个 IEnumerable 的 IEnumberable,如下所示。
[[1,2,3],[4,5,6],[7]]
我们在MoreLINQ 中将其作为Batch
.
var batch = source.Batch(3);
正如您从代码中看到的那样,使用“标准”LINQ 运算符高效实现并不是一件容易的事,但它显然是可行的。请注意,它涉及缓冲输入,因为结果序列需要是独立的。
如果您确实想仅使用标准运算符来执行此操作,那么效率较低的实现将是:
// Assume "size" is the batch size
var query = source.Select((value, index) => new { value, index })
.GroupBy(pair => pair.index / size, pair => pair.value);
编辑:只是为了说明为什么这比约翰费舍尔的答案更安全,这里有一个简短但完整的程序来显示差异:
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main(String[] args)
{
int[] source = { 1, 2, 3, 4, 5, 6, 7 };
var skeet = SkeetAnswer(source, 3);
var fisher = FisherAnswer(source, 3);
Console.WriteLine("Size of the first element of Skeet's solution:");
Console.WriteLine(skeet.First().Count());
Console.WriteLine(skeet.First().Count());
Console.WriteLine(skeet.First().Count());
Console.WriteLine("Size of the first element of Fisher's solution:");
Console.WriteLine(fisher.First().Count());
Console.WriteLine(fisher.First().Count());
Console.WriteLine(fisher.First().Count());
}
static IEnumerable<IEnumerable<int>> SkeetAnswer(IEnumerable<int> source,
int size)
{
return source.Select((value, index) => new { value, index })
.GroupBy(pair => pair.index / size, pair => pair.value);
}
static IEnumerable<IEnumerable<int>> FisherAnswer(IEnumerable<int> source,
int size)
{
int index = 0;
return source.GroupBy(x => (index++ / size));
}
}
结果:
Size of the first element of Skeet's solution:
3
3
3
Size of the first element of Fisher's solution:
3
2
1
虽然您可以在最后调用ToList()
,但此时您已经失去了该方法的效率收益 - 基本上,John 的方法避免为每个成员创建一个匿名类型的实例。这可以通过使用等效于 的值类型来缓解Tuple<,>
,这样就不会创建更多的对象,只是包装在另一个值中的值对。进行投影然后分组所需的时间仍然略有增加。
这很好地说明了为什么在 LINQ 查询中产生副作用(在这种情况下,修改捕获的变量index
)是一个坏主意。
另一种选择是编写一个实现,该实现为键投影GroupBy
提供每个元素的 。index
这就是 LINQ 的优点 - 有很多选择!
我不认为有任何内置方法可以做到这一点,但实现起来并不难。您所指的 group 方法更像是一个 SQL 组。您所说的通常称为分块。
这应该这样做。
//var items = new int[] { 1, 2, 3, 4, 5, 6, 7 };
int index = 0;
var grouped = items.GroupBy(x => (index++ / 3));
无需为其他答案中的额外选择步骤而烦恼。仅仅创建具有额外索引值的一次性对象会浪费内存和时间。
编辑:
正如 Jon Skeet 所提到的,迭代两次分组可能会导致问题(当被分组的项目没有完全划分为组大小时,在本例中为 3)。
为了缓解这种情况,您可以使用他的建议,或者您可以在结果上调用ToList()。(从技术上讲,您也可以在每次迭代组时将索引重置为零,但这是一种令人讨厌的代码气味。)
您可以将此代码重构为扩展方法并将其用于 List:
int i = 0;
var result = list
.Select(p => new { Counter = i++, Item = p })
.Select(p => new { Group = p.Counter / 3, Item = p.Item })
.GroupBy(p => p.Group)
.Select(p=>p.Select(q=>q.Item))
.ToList();