Получить следующие n элементов от перечисленного

StackOverflow https://stackoverflow.com/questions/3524415

  •  29-09-2019
  •  | 
  •  

Вопрос

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

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

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

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

Это было полезно?

Решение

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

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

Другие советы

Находятся Пропускать и Брать какого -либо применения для вас?

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

Так,

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

Учитывая этот класс, мы могли бы внедрить 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

Взгляните на Take (), Takewhile () и Skip ()

Я думаю, что использование 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);

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top