Получить следующие N элементов из перечислимых - PullRequest
10 голосов
/ 19 августа 2010

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

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

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

12 голосов
/ 19 августа 2010

Есть Пропустить и Возьмите ли какой-либо пользы для вас?

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

Итак,

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

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

6 голосов
/ 19 августа 2010

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

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

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

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

6 голосов
/ 19 августа 2010

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

//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 голосов
/ 19 августа 2010

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

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

1 голос
/ 19 августа 2010

Кажется, мы бы предпочли, чтобы 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
1 голос
/ 19 августа 2010

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

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

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

private static IEnumerable<T> Slice<T>(IEnumerable<T> enumerable, int sliceSize, int sliceNumber) {
    return enumerable.Skip(sliceSize * sliceNumber).Take(sliceSize);
}
0 голосов
/ 21 июня 2012

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

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

0 голосов
/ 19 августа 2010

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

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

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

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

var numbers = RandomNumberGenerator();

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

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

0 голосов
/ 19 августа 2010

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

...