从ienumerable获取下一个N个元素

时间:2010-08-19 17:22:17

标签: c# .net linq enumerator

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

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

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

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

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

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

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

注意:上面的例子是一个简化,在我的程序中我有一个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 个答案:

答案 0 :(得分:12)

SkipTake对您有用吗?

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

所以,

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

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

答案 1 :(得分:6)

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

//Remove "this" if you don't want it to be a extension method
public static IEnumerable<IList<T>> Chunks<T>(this IEnumerable<T> xs, int size)
{
    var curr = new List<T>(size);

    foreach (var x in xs)
    {
        curr.Add(x);

        if (curr.Count == size)
        {
            yield return curr;
            curr = new List<T>(size);
        }
    }
}

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

添加:数组版本:

public static IEnumerable<T[]> Chunks<T>(this IEnumerable<T> 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<T[]> Chunks<T>(this IEnumerable<T> xs, int size)
{
    return xs.Select((x, i) => new { x, i })
             .GroupBy(xi => xi.i / size, xi => xi.x)
             .Select(g => g.ToArray());
}

答案 2 :(得分:6)

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

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

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

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

答案 3 :(得分:2)

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

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);

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

答案 4 :(得分:1)

您可以将SkipTake方法与任何Enumerable对象一起使用。

对于您的修改:

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

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

答案 5 :(得分:1)

好像我们希望IEnumerable<T>有一个固定的位置计数器,以便我们可以做到

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

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

public class StickyEnumerable<T> : IEnumerable<T>, IDisposable
{
    private IEnumerator<T> innerEnumerator;

    public StickyEnumerable( IEnumerable<T> items )
    {
        innerEnumerator = items.GetEnumerator();
    }

    public IEnumerator<T> GetEnumerator()
    {
        return innerEnumerator;
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return innerEnumerator;
    }

    public void Dispose()
    {
        if (innerEnumerator != null)
        {
            innerEnumerator.Dispose();
        }
    }
}

鉴于该课程,我们可以用

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

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

using (var sticky = new StickyEnumerable<int>(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

答案 6 :(得分:0)

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

答案 7 :(得分:0)

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

更好的方法是使用Linq扩展Take()。我认为您不需要将Skip()与生成器一起使用。

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

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

var numbers = RandomNumberGenerator();
var slice = numbers.Take(10);

public static IEnumerable<int> RandomNumberGenerator()
{
    yield return random.Next();
}

但是Count()的{​​{1}}总是1.我也尝试通过slice循环运行它,因为我知道Linq扩展通常是懒惰的评估,它只循环一次。我最终做了下面的代码,而不是foreach,它起作用了:

Take()

如果您注意到我每次都会将public static IEnumerable<int> Slice(this IEnumerable<int> enumerable, int size) { var list = new List<int>(); foreach (var count in Enumerable.Range(0, size)) list.Add(enumerable.First()); return list; } 添加到列表中,但由于传入的可枚举是来自First()的生成器,因此每次结果都不同。

因此,不需要使用RandomNumberGenerator()的生成器,因为结果会有所不同。循环Skip()并不总是无副作用。

编辑:我会离开最后一个编辑,所以没有人遇到同样的错误,但这对我来说这样做很好:

IEnumerable

两片不同。

答案 8 :(得分:0)

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

    public static IEnumerable<int> RandomNumberGenerator()
    {
        while(true) yield return random.Next();
    }

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

答案 9 :(得分:0)

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

int[] ints = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
IEnumerable<IEnumerable<int>> chunks = Chunk(ints, 2, t => t.Dump());
//won't enumerate, so won't do anything unless you force it:
chunks.ToList();

IEnumerable<T> Chunk<T, R>(IEnumerable<R> src, int n, Func<IEnumerable<R>, T> action){
  IEnumerable<R> head;
  IEnumerable<R> 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作为默认操作,但我还没有找到如何做到这一点。