Получить следующие N элементов из enumerable

Контекст: 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);
}

Я определил метод среза, но я чувствую, что он должен быть уже определен. Вот мой метод среза, просто для справки:

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

вопрос: есть ли более простой способ выполнить то, что я пытаюсь сделать? Возможно В LINQ?

Примечание: выше пример является упрощением, в моей программе У меня есть итератор, который сканирует данную матрицу нелинейным образом.

EDIT: почему 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 ответов


Я сделал что-то подобное. Но я хотел бы, чтобы это было проще:

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

Are пропустить и взять никакой пользы для вас?

использовать комбинацию из двух в цикле, чтобы получить то, что вы хотите.

и

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

пропускает первые 10 записей, а затем принимает следующие 10.


используя Skip и Take будет очень плохая идея. Зову Skip на индексированной коллекции может быть хорошо, но вызов его на любом произвольном IEnumerable<T> может привести к перечислению по количеству пропущенных элементов, что означает, что если вы вызываете его повторно, вы перечисляете последовательность на порядок больше раз, чем вы должны быть.

жаловаться на "преждевременную оптимизацию" все, что вы хотите; но это просто нелепый.

Я думаю, что ваш Slice метод так же хорошо, как он получает. Я собирался предложить другой подход, который обеспечит отложенное выполнение и устранит промежуточное распределение массива, но это опасная игра (т. е., если вы попробуете что-то вроде ToList на такой результат IEnumerable<T> реализация, без перечисления по внутренним коллекциям, вы окажетесь в бесконечном цикле).

(Я удалил то, что было первоначально здесь, как OP улучшения с момента публикации вопроса с тех пор сделали мои предложения здесь избыточными.)


давайте посмотрим, если вам даже нужна сложность среза. если ваше случайное число генерирует без гражданства, я бы предположил, что каждый вызов к нему будет генерировать уникальные случайные числа, поэтому, возможно, этого будет достаточно:

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

это поддерживает экземпляр генератора, так что вы можете продолжать получать значения без повторного заполнения генератора.


можно использовать пропустить и взять методы с любым перечисляемым объектом.

для редактирования :

Как насчет функции, которая принимает номер среза и размер среза в качестве параметра?

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

похоже, мы предпочли бы для 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();
        }
    }
}

учитывая, что класс, мы могли бы реализовать срез с

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> is как правило, опасный класс, если потребляющий код не ожидает его. Например,

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

взгляните на Take (), TakeWhile () и Skip ()


я думаю, что использование Slice() будет немного вводит в заблуждение. Я думаю об этом как о средстве, чтобы дать мне Чак массива в новый массив и не вызывая побочных эффектов. В этом случае вы фактически переместите перечисляемый вперед 10.

возможный лучший подход-просто использовать расширение Linq Take(). Я не думаю, что вам нужно будет использовать Skip() с генератором.

Edit: Данг, я пытался проверить это поведение с следующий код

Примечание: это было не совсем правильно, я оставляю его здесь, чтобы другие не попадали в ту же ошибку.

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() результат отличается каждый раз.

так снова с генератором, использующим Skip() не требуется, так как результат будет отличаться. Петля над IEnumerable не всегда побочными эффектами.

Edit: я оставлю последнее редактирование, чтобы никто не попал в ту же ошибку, но это сработало хорошо для меня, просто делая это:

var numbers = RandomNumberGenerator();

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

два куска были разными.


Я сделал несколько ошибок в своем первоначальном ответе, но некоторые из пунктов все еще стоят. 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;
    }

я получил это решение для той же проблемы:

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 как действие по умолчанию, но я не нашел, как это сделать.