Get next N elements from enumerable

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

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

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

РЕДАКТИРОВАТЬ: Почему Пропустить + Взять не годится.

Эффективно, что я хочу:

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.

11
задан THX-1138 19 August 2010 в 19:00
поделиться

9 ответов

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

//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());
}
7
ответ дан 3 December 2019 в 03:03
поделиться

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

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

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

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

6
ответ дан 3 December 2019 в 03:03
поделиться

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

Для правки:

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

private static IEnumerable<T> Slice<T>(IEnumerable<T> enumerable, int sliceSize, int sliceNumber) {
    return enumerable.Skip(sliceSize * sliceNumber).Take(sliceSize);
}
1
ответ дан 3 December 2019 в 03:03
поделиться

Полезны ли для вас Skip и Take?

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

Итак,

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

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

12
ответ дан 3 December 2019 в 03:03
поделиться

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

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
1
ответ дан 3 December 2019 в 03:03
поделиться

Рассмотрите Take(), TakeWhile() и Skip()

0
ответ дан 3 December 2019 в 03:03
поделиться

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

Давайте посмотрим, нужна ли вам вообще сложность 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);

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

2
ответ дан 3 December 2019 в 03:03
поделиться

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

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

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

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

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

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

, но Count () для среза всегда 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 () в список, но поскольку enumerable, который передается, является генератором из RandomNumberGenerator () , результат каждый раз отличается.

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

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

var numbers = RandomNumberGenerator();

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

Эти два фрагмента были разными.

0
ответ дан 3 December 2019 в 03:03
поделиться
Другие вопросы по тегам:

Похожие вопросы: