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]]

c#中使用lambda/linq从对象列表中创建任意大小的组

我们已经在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()。(从技术上讲,您也可以在每次遍历该组时将索引重置为零,但这是一种令人讨厌的代码味道。)