从枚举中获取下一个N个元素

上下文:C#3.0,.Net 3.5
假设我有一个生成随机数的方法(永远):

private static IEnumerable RandomNumberGenerator() { while (true) yield return GenerateRandomNumber(0, 100); } 

我需要将这些数字分组为10组,所以我想要像:

 foreach (IEnumerable group in RandomNumberGenerator().Slice(10)) { Assert.That(group.Count() == 10); } 

我已经定义了Slice方法,但我觉得应该已经定义了一个。 这是我的Slice方法,仅供参考:

  private static IEnumerable Slice(IEnumerable enumerable, int size) { var result = new List(size); foreach (var item in enumerable) { result.Add(item); if (result.Count == size) { yield return result.ToArray(); result.Clear(); } } } 

问题:有没有更简单的方法来完成我想要做的事情? 也许是Linq?

注意:上面的例子是一个简化,在我的程序中我有一个Iterator,它以非线性的方式扫描给定的矩阵。

编辑:为什么Skip + Take不好。

实际上我想要的是:

 var group1 = RandomNumberGenerator().Skip(0).Take(10); var group2 = RandomNumberGenerator().Skip(10).Take(10); var group3 = RandomNumberGenerator().Skip(20).Take(10); var group4 = RandomNumberGenerator().Skip(30).Take(10); 

没有再生数(10 + 20 + 30 + 40)次的开销。 我需要一个能产生40个数字的解决方案,然后将4个组中的数字分成10个。

我做了类似的事情。 但我希望它更简单:

 //Remove "this" if you don't want it to be a extension method public static IEnumerable> Chunks(this IEnumerable xs, int size) { var curr = new List(size); foreach (var x in xs) { curr.Add(x); if (curr.Count == size) { yield return curr; curr = new List(size); } } } 

我认为你的有缺陷。 为所有块/切片返回相同的数组,因此只有最后一个块/切片才能获得正确的数据。

增加:arrays版本:

 public static IEnumerable Chunks(this IEnumerable xs, int size) { var curr = new T[size]; int i = 0; foreach (var x in xs) { curr[i % size] = x; if (++i % size == 0) { yield return curr; curr = new T[size]; } } } 

增加: Linq版本(不是C#2.0)。 正如所指出的那样,它不会对无限序列起作用,并且会比替代方案慢得多:

 public static IEnumerable Chunks(this IEnumerable xs, int size) { return xs.Select((x, i) => new { x, i }) .GroupBy(xi => xi.i / size, xi => xi.x) .Select(g => g.ToArray()); } 

跳过并对你有用吗?

在循环中使用两者的组合来获得您想要的。

所以,

 list.Skip(10).Take(10); 

跳过前10条记录然后接下来的10条记录。

使用SkipTake会是一个非常糟糕的主意 。 在索引集合上调用Skip可能没问题,但是在任意IEnumerable上调用它都会导致枚举超过跳过的元素数量,这意味着如果你反复调用它,你就会对序列进行枚举比您需要的次数多一个数量级

抱怨所有你想要的“过早优化”; 但那太荒谬了。

我认为你的Slice方法和它一样好。 我打算提出一种不同的方法来提供延迟执行并避免中间数组分配,但这是一个危险的游戏(即,如果你在这样的结果IEnumerable实现上尝试类似ToList ,而不是枚举内部集合,你将最终陷入无尽的循环)。

(我已经删除了原来的内容,因为自从发布问题后OP的改进已经使我的建议变得多余了。)

让我们看看你是否需要Slice的复杂性。 如果您生成的随机数是无状态的,我会假设每次调用它都会产生唯一的随机数,所以这可能就足够了:

 var group1 = RandomNumberGenerator().Take(10); var group2 = RandomNumberGenerator().Take(10); var group3 = RandomNumberGenerator().Take(10); var group4 = RandomNumberGenerator().Take(10); 

每次调用Take返回一组10个数字。

现在,如果您的随机数生成器每次迭代时都会使用特定值重新播种 ,这将无效。 您只需为每个组获得相同的10个值。 所以相反,你会使用:

 var generator = RandomNumberGenerator(); var group1 = generator.Take(10); var group2 = generator.Take(10); var group3 = generator.Take(10); var group4 = generator.Take(10); 

这将维护生成器的实例,以便您可以继续检索值而无需重新生成生成器。

您可以对任何Enumerable对象使用Skip和Take方法。

为了您的编辑:

将切片编号和切片大小作为参数的函数怎么样?

 private static IEnumerable Slice(IEnumerable enumerable, int sliceSize, int sliceNumber) { return enumerable.Skip(sliceSize * sliceNumber).Take(sliceSize); } 

看起来我们更喜欢IEnumerable有一个固定的位置计数器,以便我们可以做到

 var group1 = items.Take(10); var group2 = items.Take(10); var group3 = items.Take(10); var group4 = items.Take(10); 

并获得连续的切片而不是每次获得前10个项目。 我们可以使用IEnumerable的新实现来实现,它保留了Enumerator的一个实例,并在每次调用GetEnumerator时返回它:

 public class StickyEnumerable : IEnumerable, IDisposable { private IEnumerator innerEnumerator; public StickyEnumerable( IEnumerable items ) { innerEnumerator = items.GetEnumerator(); } public IEnumerator GetEnumerator() { return innerEnumerator; } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return innerEnumerator; } public void Dispose() { if (innerEnumerator != null) { innerEnumerator.Dispose(); } } } 

鉴于该类,我们可以实现Slice

 public static IEnumerable> Slices(this IEnumerable items, int size) { using (StickyEnumerable sticky = new StickyEnumerable(items)) { IEnumerable slice; do { slice = sticky.Take(size).ToList(); yield return slice; } while (slice.Count() == size); } yield break; } 

这适用于这种情况,但StickyEnumerable通常是一个危险的类,如果消费代码不期望它。 例如,

 using (var sticky = new StickyEnumerable(Enumerable.Range(1, 10))) { var first = sticky.Take(2); var second = sticky.Take(2); foreach (int i in second) { Console.WriteLine(i); } foreach (int i in first) { Console.WriteLine(i); } } 

版画

 1 2 3 4 

而不是

 3 4 1 2 

看看Take(),TakeWhile()和Skip()

我认为使用Slice()会有点误导。 我认为这是一种方法,可以让我把一个数组放入一个新的数组,而不会引起副作用。 在这种情况下,您实际上会移动可枚举的前锋10。

一种可能更好的方法是使用Linq扩展Take() 。 我不认为你需要使用Skip()和生成器。

编辑: Dang,我一直在尝试使用以下代码测试此行为

注意:这不是真的正确,我把它放在这里,所以别人不会犯同样的错误。

 var numbers = RandomNumberGenerator(); var slice = numbers.Take(10); public static IEnumerable RandomNumberGenerator() { yield return random.Next(); } 

但是sliceCount()总是1.我也尝试通过foreach循环运行它,因为我知道Linq扩展通常被懒惰地评估并且它只循环一次。 我最终做了下面的代码,而不是Take() ,它的工作原理如下:

 public static IEnumerable Slice(this IEnumerable enumerable, int size) { var list = new List(); foreach (var count in Enumerable.Range(0, size)) list.Add(enumerable.First()); return list; } 

如果您注意到我每次都将First()添加到列表中,但由于传入的可枚举是来自RandomNumberGenerator()的生成器,因此每次结果都不同。

因此,不需要使用Skip()的生成器,因为结果将是不同的。 循环使用IEnumerable并不总是免费副作用。

编辑:我将离开最后一个编辑,所以没有人陷入同样的​​错误,但它对我来说很好,只是这样做:

 var numbers = RandomNumberGenerator(); var slice1 = numbers.Take(10); var slice2 = numbers.Take(10); 

两片不同。

我在原来的答案中犯了一些错误,但有些观点仍然存在。 Skip()和Take()与生成器的工作方式不同于列表。 循环使用IEnumerable并不总是免费副作用。 无论如何,这是我获取切片列表的看法。

  public static IEnumerable RandomNumberGenerator() { while(true) yield return random.Next(); } public static IEnumerable> Slice(this IEnumerable enumerable, int size, int count) { var slices = new List>(); foreach (var iteration in Enumerable.Range(0, count)){ var list = new List(); list.AddRange(enumerable.Take(size)); slices.Add(list); } return slices; } 

我得到了同样问题的解决方案:

 int[] ints = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; IEnumerable> chunks = Chunk(ints, 2, t => t.Dump()); //won't enumerate, so won't do anything unless you force it: chunks.ToList(); IEnumerable Chunk(IEnumerable src, int n, Func, T> action){ IEnumerable head; IEnumerable tail = src; while (tail.Any()) { head = tail.Take(n); tail = tail.Skip(n); yield return action(head); } } 

如果你只是想要返回的块,不要对它们做任何事情,请使用chunks = Chunk(ints, 2, t => t) 。 我真正想要的是必须将t=>t作为默认动作,但我还没有发现如何做到这一点。