Получить следующие N элементов из перечислимых

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

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

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

РЕДАКТИРОВАТЬ: Почему 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());
}

Skip and Take вам пригодится?

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

Так,

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

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

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

Жалуйтесь на "преждевременную оптимизацию" сколько хотите; но это просто смешно.

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

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

Посмотрим, нужна ли вам сложность 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);

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

Кажется, мы бы предпочли 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

Вы можете использовать методы Skip и Take с любым объектом Enumerable.

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

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

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

Посмотрите на Take(), TakeWhile() и Пропустить ()

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

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

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

Изменить: Dang, я пытался проверить это поведение с помощью следующего кода

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

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 не всегда побочные эффекты бесплатно.

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

var numbers = RandomNumberGenerator();

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

Два ломтика были разные.

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

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

Другие вопросы по тегам