У меня есть этот запрос, который беспокоит меня; это инкапсулируется как новый оператор запроса, я сделал две версии из него, пытаясь видеть, какой работает лучше. Оба работают ужасно.
Первая попытка; декларативный стиль
public static IEnumerable<IEnumerable<α>> Section<α>(this IEnumerable<α> source, int length)
{
return source.Any()
? source.Take(length).Cons(source.Skip(length).Section(length))
: Enumerable.Empty<IEnumerable<α>>();
}
Вторая попытка: императив "приводит к возврату" стиль
public static IEnumerable<IEnumerable<α>> Section<α>(this IEnumerable<α> source, int length)
{
var fst = source.Take(length);
var rst = source.Skip(length);
yield return fst;
if (rst.Any())
foreach (var section in rst.Section(length))
yield return section;
}
На самом деле вторая попытка хуже, и с точки зрения удобочитаемости, compositionality и с точки зрения скорости.
Какие-либо подсказки относительно того, как оптимизировать это?
Я подозреваю, что проблема, с которой вы столкнулись, связана с тем, что перечисление конечного результата является, по крайней мере, операцией O(n^2), а возможно, и хуже; я еще не все продумал в своей голове.
Почему это так? Ну, предположим, у вас есть [1, 2, 3, 4, 5, 6], и вы разбиваете их на то, что вы считаете { { 1, 2 }, {3, 4}, {5, 6}
Это не то, что вы сделали. На самом деле вы разбили это на { взять первые два, взять первые два и отбросить их, а затем взять следующие два, взять первые два и отбросить их, а затем взять следующие два и отбросить их, а затем взять третьи два }
Заметили, как каждый шаг на этом пути пересчитывает результат? Это потому, что массив может меняться между обращениями к перечислению. LINQ был разработан, чтобы всегда получать актуальные результаты; вы пишете запрос, который означает "пропустить первые четыре и итерировать следующие два", именно это вы и получите - запрос, выполняющий этот код при перечислении.
Является ли исходная последовательность достаточно маленькой и быстрой, чтобы вы могли считать ее целиком в память и разбить на части сразу, а не пытаться сделать это лениво? В качестве альтернативы, является ли последовательность индексируемой? Если вы получаете только прямой доступ к последовательности, и она слишком велика или медленна, чтобы прочитать ее в память всю сразу, тогда вы мало что можете сделать. Но если у вас есть одно или оба этих свойства, то вы можете сделать это по крайней мере линейно.
Везде, где это возможно, я пытаюсь выполнить итерацию по источнику только один раз внутри оператора. Если источник похож на результат оператора Reverse ()
, вызов Any
, Take
и Skip
может вызвать множество неприятная производительность.
Не совсем понятно, что пытается сделать ваш оператор, но если вы можете сделать это, не читая источник несколько раз, это вполне может помочь - хотя это будет во многом зависеть от того, что вводится.
Вот еще один подход, не использующий linq, который был намного быстрее, чем ваш второй метод:
public static IEnumerable<IEnumerable<a>> Section<a>(this IEnumerable<a> source, int length)
{
var enumerator = source.GetEnumerator();
var continueLoop = true;
do
{
var list = new List<a>();
var index = 0;
for (int i = 0; i < length; i++)
{
if (enumerator.MoveNext())
{
list.Add(enumerator.Current);
index++;
}
else
{
continueLoop = false;
break;
}
}
if (list.Count > 0)
{
yield return list;
}
} while (continueLoop);
}
Если я правильно понял ваш вопрос, вы пытаетесь создать ленивую реализацию перечислителя, который разбивает большую коллекцию элементов на меньшие перечисляемые коллекции элементов.
Например, последовательность из миллиона чисел может быть разбита на «секции», каждая из которых дает только 100 из них, и вы хотите, чтобы все это делалось лениво, т.е. не собирать 100 предметов в список перед их созданием.
Во-первых, ваши попытки будут повторяться по коллекции много раз, что плохо, отсюда и проблема с производительностью.
Если вы пытаетесь создать чисто ленивую реализацию, вам следует рассмотреть следующие проблемы:
Правка : Прежде чем я перейду к своему упрощенному решению, вот несколько предостережений по этому поводу:
collection.Sequence (10) .ToArray ()
, чтобы получить массив разделов. В основном: Мое решение не общего назначения . Вы должны пойти с комментарием @LBushkin о MoreLinq Batch, если вам это нужно, и я бы не стал помещать свой код в библиотеку классов, он должен быть локальным, где он необходимо или переименовано во что-то, что четко предупреждает вас о проблемах с ним.
Вот упрощенная реализация, я почти уверен, что здесь есть ошибки, так что вы можете посмотреть на реализацию тонны модульного тестирования для пограничных ситуаций:
using System;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleApplication20
{
class SectionEnumerable<T> : IEnumerable<T>
{
private readonly IEnumerator<T> _Enumerator;
public SectionEnumerable(IEnumerator<T> enumerator, int sectionSize)
{
_Enumerator = enumerator;
Left = sectionSize;
}
public IEnumerator<T> GetEnumerator()
{
while (Left > 0)
{
Left--;
yield return _Enumerator.Current;
if (Left > 0)
if (!_Enumerator.MoveNext())
break;
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public int Left { get; private set; }
}
static class SequenceExtensions
{
public static IEnumerable<IEnumerable<T>> Section<T>(this IEnumerable<T> collection, int sectionSize)
{
if (collection == null)
throw new ArgumentNullException("collection");
if (sectionSize < 1)
throw new ArgumentOutOfRangeException("sectionSize");
using (IEnumerator<T> enumerator = collection.GetEnumerator())
{
while (enumerator.MoveNext())
{
SectionEnumerable<T> enumerable = new SectionEnumerable<T>(enumerator, sectionSize);
yield return enumerable;
for (int index = 0; index < enumerable.Left; index++)
if (!enumerator.MoveNext())
yield break;
}
}
}
}
class Program
{
static void Main(string[] args)
{
var sequence = Enumerable.Range(0, 100);
var sections = sequence.Section(10);
foreach (var section in sections)
{
Console.WriteLine(
String.Join(", ",
section.Take(5).ToArray().Select(i => i.ToString()).ToArray()));
}
Console.ReadLine();
}
}
}
Вывод:
0, 1, 2, 3, 4
10, 11, 12, 13, 14
20, 21, 22, 23, 24
30, 31, 32, 33, 34
40, 41, 42, 43, 44
50, 51, 52, 53, 54
60, 61, 62, 63, 64
70, 71, 72, 73, 74
80, 81, 82, 83, 84
90, 91, 92, 93, 94
То, что вы должны тестировать модулем:
Это быстрее ? Так и должно быть, потому что для этого требуется только одна итерация исходной последовательности.
public static IEnumerable<IEnumerable<T>> Section<T>(
this IEnumerable<T> source, int length)
{
return source
.Select((x, i) => new { Value = x, Group = i / length })
.GroupBy(x => x.Group, y => y.Value);
}
У меня сегодня появилась идея, посмотрите
public static IEnumerable<α> Take<α>(this IEnumerator<α> iterator, int count)
{
for (var i = 0; i < count && iterator.MoveNext(); i++)
yield return iterator.Current;
}
public static IEnumerable<IEnumerable<α>> Section<α>(this IEnumerator<α> iterator, int length)
{
var sct = Enumerable.Empty<α>();
do
{
sct = iterator.Take(length).ToArray();
if (sct.Any())
yield return sct;
}
while (sct.Any());
}
Это все еще не супер-элегантно, но, по крайней мере, реализация очень короткая и читабельная.
Это может быть довольно интересным исследованием операторов запросов над IEnumerator.
И для удобства
public static IEnumerable<IEnumerable<α>> Section<α>(this IEnumerable<α> source, int length)
{
using (var iterator = source.GetEnumerator())
foreach (var e in iterator.Section(length))
yield return e;
}
Нужно ли вам по какой-то причине сохранять исходный текст по мере продвижения? Если нет, то почему бы вам не использовать рекурсию и использовать стиль hd :: tl для извлечения головы, передавать tl в рекурсивный вызов, и при любой четной рекурсии объединять две имеющиеся секции в одну?
С обновленным выпуском экспериментальных расширений Ix вы можете использовать операторы Window
или Buffer
для создания скользящего окна, что должно достичь того, что вам нужно.
Как насчет метода расширения
public static class IEnumerableExtensions
{
public static IEnumerable<List<T>> InSetsOf<T>(this IEnumerable<T> source, int max)
{
List<T> toReturn = new List<T>();
foreach(var item in source)
{
toReturn.Add(item);
if (toReturn.Count == max)
{
yield return toReturn;
toReturn = new List<T>();
}
}
if (toReturn.Any())
{
yield return toReturn;
}
}
}
некоторые тесты:
[TestFixture]
public class When_asked_to_return_items_in_sets
{
[Test]
public void Should_return_the_correct_number_of_sets_if_the_input_contains_a_multiple_of_the_setSize()
{
List<string> input = "abcdefghij".Select(x => x.ToString()).ToList();
var result = input.InSetsOf(5);
result.Count().ShouldBeEqualTo(2);
result.First().Count.ShouldBeEqualTo(5);
result.Last().Count.ShouldBeEqualTo(5);
}
[Test]
public void Should_separate_the_input_into_sets_of_size_requested()
{
List<string> input = "abcdefghijklm".Select(x => x.ToString()).ToList();
var result = input.InSetsOf(5);
result.Count().ShouldBeEqualTo(3);
result.First().Count.ShouldBeEqualTo(5);
result.Last().Count.ShouldBeEqualTo(3);
}
}