如何获取一个列表(使用 LINQ)并将其分解为一个列表列表,在每 8 个条目上对原始列表进行分区?
我想这样的事情会涉及 Skip 和/或 Take,但我对 LINQ 还是很陌生。
编辑:使用 C#/.Net 3.5
Edit2:这个问题的措辞与其他“重复”问题不同。虽然问题相似,但这个问题的答案更优越:“接受”的答案非常可靠(带有yield
声明)以及 Jon Skeet 建议使用 MoreLinq(在“其他”问题中不推荐。)有时重复是好的,因为它们会强制重新检查问题。
如何获取一个列表(使用 LINQ)并将其分解为一个列表列表,在每 8 个条目上对原始列表进行分区?
我想这样的事情会涉及 Skip 和/或 Take,但我对 LINQ 还是很陌生。
编辑:使用 C#/.Net 3.5
Edit2:这个问题的措辞与其他“重复”问题不同。虽然问题相似,但这个问题的答案更优越:“接受”的答案非常可靠(带有yield
声明)以及 Jon Skeet 建议使用 MoreLinq(在“其他”问题中不推荐。)有时重复是好的,因为它们会强制重新检查问题。
使用以下扩展方法将输入分解为子集
public static class IEnumerableExtensions
{
public static IEnumerable<List<T>> InSetsOf<T>(this IEnumerable<T> source, int max)
{
List<T> toReturn = new List<T>(max);
foreach(var item in source)
{
toReturn.Add(item);
if (toReturn.Count == max)
{
yield return toReturn;
toReturn = new List<T>(max);
}
}
if (toReturn.Any())
{
yield return toReturn;
}
}
}
你最好使用像MoreLinq这样的库,但如果你真的必须使用“plain LINQ”来做到这一点,你可以使用GroupBy
:
var sequence = new[] {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
var result = sequence.Select((x, i) => new {Group = i/8, Value = x})
.GroupBy(item => item.Group, g => g.Value)
.Select(g => g.Where(x => true));
// result is: { {1,2,3,4,5,6,7,8}, {9,10,11,12,13,14,15,16} }
基本上,我们使用Select()
为正在消耗的值提供索引的版本,我们将索引除以 8 来识别每个值属于哪个组。然后我们通过这个分组键对序列进行分组。最后一个Select
只是将IGrouping<>
down 减少到 a IEnumerable<IEnumerable<T>>
(并且不是严格必要的,因为IGrouping
is a IEnumerable
)。
8
通过分解示例中的常量并将其替换为指定的参数,很容易将其转换为可重用的方法。它不一定是最优雅的解决方案,它不再是一个懒惰的流式解决方案......但它确实有效。
您还可以使用迭代器块 () 编写自己的扩展方法yield return
,这可以为您提供更好的性能并使用更少的内存GroupBy
。这就是Batch()
MoreLinq 的方法所做的 IIRC。
这根本不是最初的 Linq 设计者所想的,但请查看对 GroupBy 的这种滥用:
public static IEnumerable<IEnumerable<T>> BatchBy<T>(this IEnumerable<T> items, int batchSize)
{
var count = 0;
return items.GroupBy(x => (count++ / batchSize)).ToList();
}
[TestMethod]
public void BatchBy_breaks_a_list_into_chunks()
{
var values = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var batches = values.BatchBy(3);
batches.Count().ShouldEqual(4);
batches.First().Count().ShouldEqual(3);
batches.Last().Count().ShouldEqual(1);
}
我认为它赢得了这个问题的“高尔夫”奖。这ToList
非常重要,因为您要确保在尝试对输出进行任何操作之前实际执行了分组。如果你删除ToList
,你会得到一些奇怪的副作用。
Take 效率不会很高,因为它不会删除所取的条目。
为什么不使用一个简单的循环:
public IEnumerable<IList<T>> Partition<T>(this/* <-- see extension methods*/ IEnumerable<T> src,int num)
{
IEnumerator<T> enu=src.getEnumerator();
while(true)
{
List<T> result=new List<T>(num);
for(int i=0;i<num;i++)
{
if(!enu.MoveNext())
{
if(i>0)yield return result;
yield break;
}
result.Add(enu.Current);
}
yield return result;
}
}
from b in Enumerable.Range(0,8) select items.Where((x,i) => (i % 8) == b);
Mel给出了最简单的解决方案:
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items,
int partitionSize)
{
int i = 0;
return items.GroupBy(x => i++ / partitionSize).ToArray();
}
简洁但速度较慢。上述方法将 IEnumerable 拆分为所需的固定大小的块,块的总数不重要。要将 IEnumerable 拆分为 N 个大小相等或接近相等的块,您可以执行以下操作:
public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> items,
int numOfParts)
{
int i = 0;
return items.GroupBy(x => i++ % numOfParts);
}
为了加快速度,一个简单的方法可以做到:
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items,
int partitionSize)
{
if (partitionSize <= 0)
throw new ArgumentOutOfRangeException("partitionSize");
int innerListCounter = 0;
int numberOfPackets = 0;
foreach (var item in items)
{
innerListCounter++;
if (innerListCounter == partitionSize)
{
yield return items.Skip(numberOfPackets * partitionSize).Take(partitionSize);
innerListCounter = 0;
numberOfPackets++;
}
}
if (innerListCounter > 0)
yield return items.Skip(numberOfPackets * partitionSize);
}
这比现在地球上的任何东西都快:)这里Split
的操作的等效方法