c#中使用lambda/linq从对象列表中创建任意大小的组
本文关键字:创建 列表 任意大 对象 lambda linq | 更新日期: 2023-09-27 18:15:03
是否有一种方法可以使用预先存在的Linq函数从项目列表中创建任意大小的组?
例如:[1,2,3,4,5,6,7]
当执行类似list.Group(3)的操作时,将生成IEnumerables的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的好处——有这么多的选项!
我不认为有任何内置的方法来做到这一点,但它并不太难实现。您所指的组方法更像SQL组。你所谈论的通常被称为分块。
您可以将此代码重构为一个扩展方法,并将其用于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();
应该可以了。
//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()。(从技术上讲,您也可以在每次遍历该组时将索引重置为零,但这是一种令人讨厌的代码味道。)