Получить следующие 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());
}
Используйте комбинацию из двух в цикле, чтобы получить то, что вы хотите.
Так,
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);
}
Я допустил некоторые ошибки в своем первоначальном ответе, но некоторые пункты все еще остаются в силе. 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
как действие по умолчанию, но я еще не выяснил, как это сделать.