如何将一个IEnumerable<String>拆分为多个IEnumerable<string>组

32

我有一个IEnumerable<string>,我想将其分成每组三个,所以如果我的输入有6个项目,我将得到一个IEnumerable<IEnumerable<string>>,其中每个包含一个IEnumerable<string> ,其中包含我的字符串内容。

我正在寻找如何使用Linq来实现此操作,而不是使用简单的for循环。

谢谢

8个回答

34

这是对该帖子的晚回复,但这里有一种不使用任何临时存储的方法:

public static class EnumerableExt
{
    public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> input, int blockSize)
    {
        var enumerator = input.GetEnumerator();

        while (enumerator.MoveNext())
        {
            yield return nextPartition(enumerator, blockSize);
        }
    }

    private static IEnumerable<T> nextPartition<T>(IEnumerator<T> enumerator, int blockSize)
    {
        do
        {
            yield return enumerator.Current;
        }
        while (--blockSize > 0 && enumerator.MoveNext());
    }
}

以下是一些测试代码:

class Program
{
    static void Main(string[] args)
    {
        var someNumbers = Enumerable.Range(0, 10000);

        foreach (var block in someNumbers.Partition(100))
        {
            Console.WriteLine("\nStart of block.");

            foreach (int number in block)
            {
                Console.Write(number);
                Console.Write(" ");
            }
        }

        Console.WriteLine("\nDone.");
        Console.ReadLine();
    }
}

然而,请注意以下评论中此方法的限制:

  1. 如果你将测试代码中的foreach更改为foreach(var block in someNumbers.Partition(100).ToArray()),那么它就无法工作。

  2. 它不是线程安全的。


这正是我寻求的与“BlockingCollection”一起使用并保留原始可枚举性的解决方案。 - Tim Rogers
你做得很好!包含测试代码可以获得额外的2分。虽然不如其他一些例子那么简洁,但在保持对IEnumerable的限制和减少集合迭代方面最准确。再次恭喜! - mtazva
4
重要提示:请注意,此代码不是线程安全的!在将结果传递给异步代码之前,您必须同步地将生成的IEnumerable转换为具体类型,以确保项目正确地按批次收集。 - mtazva
迄今为止最好的解决方案。大多数答案只是在集合上进行迭代,并将结果存储在列表中,或者更糟糕的是,多次迭代相同的项! - bouvierr
1
这段代码的问题在于,枚举器是通过引用传递的,因此当您进行宽度优先迭代时,它会崩溃:如果您将一行单元测试代码更改为foreach(var block in someNumbers.Partition(100).ToArray()),那么所有内容都会崩溃。 - realbart
正如@realbart所说,这段代码是有缺陷的。将相同的枚举器传递给nextPartition根本不安全。当惰性评估时(应该是IEnumerable),枚举器将在不可预测的时间内被耗尽。 - markonius

33
var result = sequence.Select((s, i) => new { Value = s, Index = i })
                     .GroupBy(item => item.Index / 3, item => item.Value);

请注意,这将返回一个IEnumerable<IGrouping<int,string>>,其功能与您想要的类似。但是,如果您严格需要将其类型定义为IEnumerable<IEnumerable<string>>(以便在不支持泛型变异的C#3.0中传递给期望它的方法),则应该使用Enumerable.Cast

var result = sequence.Select((s, i) => new { Value = s, Index = i })
                     .GroupBy(item => item.Index / 3, item => item.Value)
                     .Cast<IEnumerable<string>>();

1
那真是太快了,谢谢。 - Kev Hunter
2
GroupBy在输出结果前是否必须迭代整个序列,还是可以保持延迟执行的状态? - Don Kirkby
@Don Kirkby:对于LINQ to Objects,.GroupBy不会枚举序列。它会在调用.GetEnumerator时(例如在foreach或其他地方使用时)立即枚举整个序列。 - Mehrdad Afshari
2
没错 @Don,GroupBy的计算不像其他Linq方法那样懒惰。它在返回任何分组之前枚举整个序列。 - Colonel Panic
1
我认为不需要进行转换。IGrouping继承自IEnumerable,结果可以声明为IEnumerable<IEnumerable<string>> - Anupam

22

我知道这个问题已经有答案了,但是如果你经常打算对IEnumerables进行切片操作的话,我建议你创建一个像这样的通用扩展方法:

public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, int chunkSize)
{
    return source.Where((x,i) => i % chunkSize == 0).Select((x,i) => source.Skip(i * chunkSize).Take(chunkSize));
}

然后您可以使用sequence.Split(3)来获取您想要的内容。

(如果您不喜欢“split”已经为字符串定义了名称,您可以将其命名为其他名称,例如'slice'或'chunk'。 'Split'只是我随意定义的名称。)


+1. 我喜欢你能用一行代码实现与我相同的结果这一事实。 - Alex Essilfie
已经有一段时间了,我一直在使用你的代码(解决方案/答案/无论你如何称呼它),它运行得非常完美。最近,我尝试分析你的代码,但我无法理解你代码中的.Where((x,i) => i % chunkSize == 0)部分,但是它仍然能够正常工作。如果你不介意,能否向我解释一下你的代码是如何工作的呢?谢谢。 - Alex Essilfie
2
@Alex 当然可以!假设你的集合长度为9,你想把它分成3组。这个表达式实际上只是计算要生成多少组。正如你所看到的,我只对WhereSelect中的索引感兴趣。在Where中,我的索引从'0-8'变成了Select中的'0-2',因为Where子句只返回9个项目中的3个(通过检查Enumerable.Range(0,9).Select((x,i) => i % 3)的结果来证明!)。所以我首先跳过0(0 * 3)并取3,然后跳过3(1 * 3)并取3,最后跳过6(2 * 3)并取3! - diceguyd30
6
解决方案唯一的问题在于它会遍历源n+1次,其中n是块数。从性能和处理无法重新枚举的源的角度来看,这都是有问题的。 - Arne Claassen
@ArneClaassen 您完全正确。最好的chunk方法版本将使用带有yield返回的for循环... 向下滚动 ...我看到您已经发布了一个。^.^我不会撒谎,我只是因为它有多简洁而使用上述方法。我是一个短语迷。:P - diceguyd30

16

受 @dicegiuy30 实现的启发,我想创建一个版本,它只在源上迭代一次,并且不在内存中构建整个结果集以进行补偿。我想到的最好方法是这样的:

public static IEnumerable<IEnumerable<T>> Split2<T>(this IEnumerable<T> source, int chunkSize) {
    var chunk = new List<T>(chunkSize);
    foreach(var x in source) {
        chunk.Add(x);
        if(chunk.Count <= chunkSize) {
            continue;
        }
        yield return chunk;
        chunk = new List<T>(chunkSize);
    }
    if(chunk.Any()) {
        yield return chunk;
    }
}

我这样按需构建每个块。我希望我也可以避免使用 List<T>,并且也只是将其流传,但是还没有弄清楚。


1
+1 一个很棒的实现。就像Jon Skeet所做的一样: http://code.google.com/p/morelinq/source/browse/trunk/MoreLinq/Batch.cs - diceguyd30
3
+1 这个看起来非常高效,但是我认为以下这行代码有一个错误:if(chunk.Count <= chunkSize)。正确的代码应该是:if(chunk.Count < chunkSize) - Arne Lund

2
我们可以改进@Afshari的解决方案,实现真正的惰性求值。我们使用一个GroupAdjacentBy方法,该方法生成具有相同键的连续元素组:
sequence
.Select((x, i) => new { Value = x, Index = i })
.GroupAdjacentBy(x=>x.Index/3)
.Select(g=>g.Select(x=>x.Value))

由于组是逐个生成的,因此这种解决方案在处理长序列或无限序列时非常高效。

2
使用 Microsoft.Reactive,您可以轻松完成此操作,并且只需通过源迭代一次即可。
IEnumerable<string> source = new List<string>{"1", "2", "3", "4", "5", "6"};

IEnumerable<IEnumerable<string>> splited = source.ToObservable().Buffer(3).ToEnumerable();

1
你不需要使用ToObservable然后再转回ToEnumerable,你可以使用与Enumerable一起工作的交互式扩展缓冲区方法。在nuget上查找Ix-Main。 - Niall Connaughton
如果使用 Reactive,你需要使用 ToObservable。否则,你必须使用 Interactive - Emaborsa

1
Mehrdad Afshari的回答非常好。这里是一个封装它的扩展方法:
using System.Collections.Generic;
using System.Linq;

public static class EnumerableExtensions
{
    public static IEnumerable<IEnumerable<T>> GroupsOf<T>(this IEnumerable<T> enumerable, int size)
    {
        return enumerable.Select((v, i) => new {v, i}).GroupBy(x => x.i/size, x => x.v);
    }
}

0

我想到了一种不同的方法。它使用了一个while迭代器,但结果会被缓存在内存中,就像常规的LINQ一样,直到需要时才会被使用。
以下是代码:

public IEnumerable<IEnumerable<T>> Paginate<T>(this IEnumerable<T> source, int pageSize)
{
    List<IEnumerable<T>> pages = new List<IEnumerable<T>>();
    int skipCount = 0;

    while (skipCount * pageSize < source.Count) {
        pages.Add(source.Skip(skipCount * pageSize).Take(pageSize));
        skipCount += 1;
    }

    return pages;
}

1
以前从未见过 Runtime.CompilerServices.Extension,所以我查了一下MSDN,它说:“在C#中,您不需要使用此属性;您应该使用this修饰符作为第一个参数来创建扩展方法。” 换句话说,虽然它在功能上等效,但最好使用 public IEnumerable<IEnumerable<T>> Paginate<T>(this IEnumerable<T> source, int pageSize) 而不是该属性。 - Davy8
@Davy8:我在提交这个答案的时候刚开始学习C#,所以代码是从VB.NET直接移植过来的。现在我已经掌握了一些C#的技巧,知道这段代码并不是真正“正确”的。我已经更新了答案。 - Alex Essilfie
你是怎么得到 source.Count 的?它是一个 IEnumerable,对其执行 Count() 会枚举集合一次。 - nawfal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接