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.
Я сделал нечто подобное. Но я бы хотел, чтобы было проще:
//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());
}
Использование Пропустить
и Take
было бы очень плохой идеей . Вызов Skip
в индексированной коллекции может быть нормальным, но вызов его для любого произвольного IEnumerable
может привести к перечислению по количеству пропущенных элементов, что означает, что если вы Вызывая его многократно, вы перечисляете последовательность на порядок больше раз, чем нужно .
Жалуйтесь на «преждевременную оптимизацию» сколько угодно; но это просто смешно.
Я думаю, что ваш метод Slice
настолько хорош, насколько это возможно.Я собирался предложить другой подход, который обеспечит отложенное выполнение и предотвратит выделение промежуточного массива, но это опасная игра (например, если вы попробуете что-то вроде ToList
на таком результирующем ) IEnumerable
, без перечисления по внутренним коллекциям вы попадете в бесконечный цикл).
(Я удалил то, что было здесь изначально, поскольку улучшения OP после публикации вопроса с тех пор сделали мои предложения здесь излишними.)
Вы можете использовать методы 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?
Используйте комбинацию этих двух методов в цикле, чтобы получить то, что вы хотите.
Итак,
list.Skip(10).Take(10);
Пропускает первые 10 записей, а затем берет следующие 10.
Похоже, мы бы предпочли, чтобы 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
Я допустил несколько ошибок в своем первоначальном ответе, но некоторые моменты все еще остаются в силе. 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. Если ваш генератор случайных чисел не имеет состояния, я предполагаю, что каждый вызов его будет генерировать уникальные случайные числа, поэтому, возможно, этого будет достаточно:
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);
Это сохраняет экземпляр генератора, чтобы вы могли продолжать получать значения без повторного засева генератора.
Я думаю, что использование 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);
Эти два фрагмента были разными.