代码之家  ›  专栏  ›  技术社区  ›  THX-1138

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

  •  10
  • THX-1138  · 技术社区  · 14 年前

    上下文: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方法,但是我觉得应该已经定义了一个。下面是我的切片方法,仅供参考:

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

    有没有更简单的方法来完成我想做的事?也许是林克?

    编辑: 为什么? 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。

    10 回复  |  直到 14 年前
        1
  •  7
  •   Lasse Espeholt    14 年前

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

    //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
  •  12
  •   Mike    14 年前

    Skip Take 对你有用吗?

    在一个循环中使用这两者的组合来得到你想要的。

    所以,

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

    跳过前10条记录,然后再跳下10条。

        3
  •  7
  •   Dan Tao    14 年前

    使用 Skip Take 会是 . 打电话 跳过 在索引集合上调用可以,但在任意 IEnumerable<T> 很容易导致枚举跳过的元素数,这意味着如果重复调用它,则会枚举整个序列 .

    抱怨“过早的优化”你想要的一切,但那只是可笑的。

    Slice 这个方法已经很好了。我打算提出一种不同的方法,提供延迟执行并避免中间数组分配,但这是一个危险的游戏(即,如果您尝试 ToList IEnumerable<T> 实现时,如果不枚举内部集合,则最终将进入一个无休止的循环)。

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

        4
  •  2
  •   LBushkin    14 年前

    让我们看看你是否需要切片的复杂性。

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

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

        5
  •  1
  •   Hugo Migneron    14 年前

    你可以用这个 Skip Take

    供您编辑:

    private static IEnumerable<T> Slice<T>(IEnumerable<T> enumerable, int sliceSize, int sliceNumber) {
        return enumerable.Skip(sliceSize * sliceNumber).Take(sliceSize);
    }
    
        6
  •  1
  •   stevemegson    14 年前

    看来我们更喜欢 IEnumerable<T> 有一个固定位置的计数器

    var group1 = items.Take(10);
    var group2 = items.Take(10);
    var group3 = items.Take(10);
    var group4 = items.Take(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();
            }
        }
    }
    

    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
    
        7
  •  0
  •   epitka    14 年前

        8
  •  0
  •   Sean Copenhaver    14 年前

    Slice() 会有点误导。我认为这是一种手段,让我把一个数组扔进一个新的数组,而不是造成副作用。在这个场景中,您实际上会将可枚举项向前移动10。

    一种可能更好的方法是只使用Linq扩展 Take() . 我不认为你需要 Skip()

    编辑: 该死,我一直在用下面的代码测试这个行为

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

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

    但是 Count() slice 总是1。我还试着通过一个 foreach 循环,因为我知道Linq扩展通常是延迟计算的,它只循环一次。我最终完成了下面的代码而不是 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() 每次的结果都不一样。

    同样的,发电机使用 跳过() 不需要,因为结果会有所不同。在一个 IEnumerable

    编辑: 我将保留最后一次编辑,这样就不会有人犯同样的错误,但这样做对我来说效果很好:

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

        9
  •  0
  •   Sean Copenhaver    14 年前

    我在原来的回答中犯了一些错误,但有些观点仍然有效。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;
        }
    
        10
  •  0
  •   Martijn    12 年前

    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