Хабр Курсы для всех
РЕКЛАМА
Практикум, Хекслет, SkyPro, авторские курсы — собрали всех и попросили скидки. Осталось выбрать!
Мне нравится, а вам?
либо искусственно использовать индексы за пределами массива
запись Slice(5, 5) приведёт к тому, что мы получим исходную коллекцию циклически сдвинутую на пять индексов влево — 'F', 'G', 'I', 'A', 'B', 'C', 'D', 'E' — вот чудо [...] Для того, чтобы получить пустую выборку достаточно написать Slice(5, 6),
arr.Slice(5), я ожидаю получить обертку поверх этого массива, которая нулевой элемент которой будет вести на пятый элемент исходного массива, с минимум дополнительных расходов. (Enter ArraySegment, ну да ладно...) Поверх последовательностей слайсы вообще особого смысла не имеют. А вы сначала делаете принудительную материализацию пришедшей вам коллекции в массив (даже для IList<T>, да). Мне страшно подумать, что происходит с производительностью.но сам метод Slice не создаёт никаких копий, а только лишь выполняет итерацию.
а если вас это пугает, то сделайте свою реализацию без отрицательных индексов и проверки длины
Не знаю, как у вас, но в подавляющем большинстве случаев обрабатываются конечные коллекции.
Не знаю, как у вас, но в подавляющем большинстве случаев обрабатываются конечные коллекции.
Наглядные примеры в статье.
сдвиги элементов в массивах — распостранённое явление
Тогда воспринимайте это как абстрактную математическую конструкцию, которой пока ещё не нашлось применения
arr.Slice(a,b).Slice(c,d)... сводилась бы по сложности и потребляемой памяти к одному слайсу, и чтобы оптимально работали Last(), LastOrDefault() за время O(1).Ведь что мешает вызвать Slice(100, 200) на бесконечной выборке, если я хочу получить конечное «окно»
А разве extension метод Slice(a,b) с теми же Linq.Skip(), Linq.Take() внутри не решит проблему за O(1)?
arr[i]
Slice — это не Take. Если быть совсем педантом, определять Slice поверх последовательностей вообще не надо. Но если очень хочется обобщенной версии, то надо поддерживать хотя бы ElementAt за O(1) (чего LINQ «из коробки» не делает).Было бы математически красиво, если бы любая композиция слайсов arr.Slice(a,b).Slice(c,d)… сводилась бы по сложности и потребляемой памяти к одному слайсу, и чтобы оптимально работали Last(), LastOrDefault() за время O(1).и
Я хочу, пользуясь фрейворком, вызывать Slice и дальше take/last/count на любых enumerables, и чтобы на массивах и списках работал оптимальный для них вариант. А на остальных, пусть даже бесконечных генераторах, пессимистичный, но корректный.
Оптимальность Last недостижима в общем случае. Сложность композиции Take-Skip слайсов равна сложности одного слайса. Насчет памяти сомневаюсь, тут зависит от упрощения Expression Tree, но она скорее всего это требование не важно. Сделать Slice индексированным на массивах и списках можно. В общем случае — невозможно.
достаточно один раз принять зацикленность (замкнутость) коллекции
IEnumerable легко принять, ага. if (head < tail)
{
foreach (var item in items.Skip(head).Take(tail - head))
{
yield return item;
}
}
Enumerable.Empty<T>()
а он нужен?
теперь всё становится логично?
var enumerator = i == j ? items.Take(0) : items.Slice(i, j);
[Flags]
public enum SliceOptions
{
None = 0,
Lazy = 1,
}
public static IEnumerable<T> Slice<T>(
this IEnumerable<T> collection,
int head,
int tail = 0,
SliceOptions options = SliceOptions.None)
{
var items = collection as T[] ?? collection.ToArray();
var count = items.Count();
head = head < 0 ? count + head : head;
tail = tail < 0 ? count + tail : tail;
if (head < 0 || count - 1 < head) throw new ArgumentOutOfRangeException("head");
if (tail < 0 || count - 1 < tail) throw new ArgumentOutOfRangeException("tail");
if (head == tail && (options & SliceOptions.Lazy) == SliceOptions.Lazy)
{
yield break;
}
if (head < tail)
{
foreach (var item in items.Skip(head).Take(tail - head))
{
yield return item;
}
}
else
{
foreach (var item in items.Skip(head))
{
yield return item;
}
foreach (var item in items.Skip(0).Take(tail))
{
yield return item;
}
}
}
По-моему, ничего в этом страшного нет.
Как вы, например, без флага-квантификатора разрулите в регулярном выражении «жадный» захват использовать или «ленивый»
А это по сути не одно и то же?
При создании регулярного выражения мы можем указать флаг RegexOptions.IgnoreCase, вместе с тем можем не указывать, а использовать inline character i — результат тот же.
Конечно, если вы видите другие пути к консистентности
var letters = new [] {'A', 'B, 'C', 'D'};
letters.Slice(1, 9); // BCDABCDAB
letters.Slice(-2, -9); // CBADCBADC
public static IEnumerable<T> Ring<T>(this IList<T> items, int skip)
{
var reverse = skip < 0;
var count = items.Count;
skip = reverse ? count + skip : skip;
var take = reverse ? -skip - 1 : count - skip;
return items.Ring(skip, take);
}
public static IEnumerable<T> Ring<T>(this IList<T> items, int skip, int take)
{
var reverse = take < 0;
var count = items.Count;
skip = skip < 0 ? count + skip : skip;
skip = skip < count ? skip : skip%count;
take = reverse ? -take : take;
for (var i = 0; i < take; i++)
{
var j = i < count ? i : i%count;
var index = reverse ? skip - j : skip + j;
index = index < 0 ? count + index : index;
index = index < count ? index : index%count;
yield return items[index];
}
}
private static void Main(string[] args)
{
var bodyLetters = new[] {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'I'};
var headIndexes = new[] { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 };
var tailIndexes = new[] {-8 , -7 , -6 , -5 , -4 , -3 , -2 , -1 };
// 'C', 'D', 'E', 'F', 'G', 'I', 'A', 'B',
bodyLetters.Ring(2, 8).ToList().ForEach(Console.Write);
Console.WriteLine();
// 'C', 'B', 'A', 'I', 'G', 'F', 'E', 'D',
bodyLetters.Ring(2, -8).ToList().ForEach(Console.Write);
Console.WriteLine();
// 'D', 'E', 'F', 'G'
bodyLetters.Ring(3, 4).ToList().ForEach(Console.Write);
Console.WriteLine();
// 'D', 'E', 'F', 'G'
bodyLetters.Ring(-5, 4).ToList().ForEach(Console.Write);
Console.WriteLine();
// 'D', 'C', 'B', 'A'
bodyLetters.Ring(-5, -4).ToList().ForEach(Console.Write);
Console.WriteLine();
// 'D', 'E', 'F', 'G', 'I', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'I', 'A', 'B', 'C'
bodyLetters.Ring(3, 16).ToList().ForEach(Console.Write);
Console.WriteLine();
// 'D', 'C', 'B', 'A', 'I', 'G', 'F', 'E', 'D', 'C', 'B', 'A', 'I', 'G', 'F', 'E'
bodyLetters.Ring(-5, -16).ToList().ForEach(Console.Write);
Console.WriteLine();
// 'D', 'E', 'F', 'G', 'I'
bodyLetters.Ring(3).ToList().ForEach(Console.Write);
Console.WriteLine();
// 'A', 'B', 'C', 'D'
bodyLetters.Ring(-5).ToList().ForEach(Console.Write);
Console.WriteLine();
Console.ReadKey();
}
Меня больше интересуют не сами слайсы, а обобщённые алгоритмы
IList IEnumerable — это плохой обобщенный алгоритм. Особенно когда вы потом всегда делаете ToList. И заранее закладывать в операцию Ring сдвиги — тоже плохой обобщенный алгоритм, он противоречит принципу функциональной композиции, заложенной в LINQ.)Если изложенный метод и нарушает функциональную композицию, то вы можете скрыть (инкапсулировать) его
сам алгоритм нисколько не теряет своей ценности
var index = reverse ? skip - j : skip + j;
Конечно, у вас может быть своё мнение, но я сторонник написания одного метода пригодного для решения обобщённой задачи, чем нескольких методов для каждого родственного частного случая.
arr.Ring().Skip(5).Take(2) лучше чем arr.Ring(5, 2).Понимаю, что в обобщённой версии есть несколько дополнительных булевых проверок, но всё же их влияние не столь велико на производительность.
public static IEnumerable<T> Turn<T>(this IList<T> items, int skip, int turnsCount = 0)
{
var reverse = skip < 0;
var count = items.Count;
skip = reverse ? count + skip : skip;
var take = turnsCount == 0
? reverse ? -skip - 1 : count - skip
: count*turnsCount;
return items.Ring(skip, take);
}
Решения я генерирую в реальном времени, поэтому появление недочётов вполне закономерно.
Менять тип возвращаемого значения я не вижу смысла
так как сам метод Ring внутри срабатывает за O(1), а на выходе при необходимости можно сделать как ToList(), так и ToArray().
Также придумал последнее обобщение с количеством оборотов
Так может надо подумать немножко дольше, чтобы недочетов не было?
огда бы я отвечал на ваши комментарии раз в день и прогресс шёл значительно медленнее.
К примеру, вы делаете UI, и у вас есть коллекция-заглушка с n-элементами, вдруг вы захотели проверить, как работает UI при 2n, 3n, ...mn элементах.
Items = Enumerable.Repeat(_testItems, m).SelectMany(n => n);
private static void Main(string[] args)
{
var bodyLetters = new[] {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'I'};
var headIndexes = new[] { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 };
var tailIndexes = new[] {-8 , -7 , -6 , -5 , -4 , -3 , -2 , -1 };
// CDEFGICDEF
bodyLetters.SkipByRing(18).TakeByRing(10).ToList().ForEach(Console.Write);
Console.WriteLine();
// FEDCBAFEDC
bodyLetters.SkipByRing(-18).TakeByRing(10).ToList().ForEach(Console.Write);
Console.WriteLine();
// IGFEDCIGFE
bodyLetters.SkipByRing(18).TakeByRing(-10).ToList().ForEach(Console.Write);
Console.WriteLine();
// ABCDEFABCD
bodyLetters.SkipByRing(-18).TakeByRing(-10).ToList().ForEach(Console.Write);
Console.WriteLine();
Console.WriteLine();
// CDEFGIABCD
bodyLetters.SliceByRing(18, 10).ToList().ForEach(Console.Write);
Console.WriteLine();
// GIABCDEFGI
bodyLetters.SliceByRing(-18, 10).ToList().ForEach(Console.Write);
Console.WriteLine();
// BAIGFEDCBA
bodyLetters.SliceByRing(18, -10).ToList().ForEach(Console.Write);
Console.WriteLine();
// FEDCBAIGFE
bodyLetters.SliceByRing(-18, -10).ToList().ForEach(Console.Write);
Console.WriteLine();
Console.ReadKey();
}
// ReSharper disable PossibleMultipleEnumeration
// ReSharper disable LoopCanBePartlyConvertedToQuery
public static IEnumerable<T> SkipByRing<T>(this IEnumerable<T> source, int count)
{
var originalCount = 0;
var reverse = count < 0;
count = reverse ? -count : count;
source = reverse ? source.Reverse() : source;
while (true)
{
if (originalCount > 0) count %= originalCount;
foreach (var item in source)
{
originalCount++;
if (count > 0)
{
count--;
continue;
}
yield return item;
}
if (count == 0) yield break;
}
}
public static IEnumerable<T> TakeByRing<T>(this IEnumerable<T> source, int count)
{
var reverse = count < 0;
count = reverse ? -count : count;
source = reverse ? source.Reverse() : source;
while (true)
{
foreach (var item in source)
{
if (count > 0)
{
count--;
yield return item;
}
}
if (count == 0) yield break;
}
}
public static IEnumerable<T> SliceByRing<T>(this IEnumerable<T> source, int skipCount, int takeCount)
{
var originalCount = 0;
var skipReverse = skipCount < 0;
var takeReverse = takeCount < 0;
skipCount = skipReverse ? -skipCount : skipCount;
takeCount = takeReverse ? -takeCount : takeCount;
source = takeReverse ? source.Reverse() : source;
if (skipReverse ^ takeReverse)
{
var count = source.Count();
skipCount = count - skipCount % count;
}
while (true)
{
if (originalCount > 0) skipCount %= originalCount;
foreach (var item in source)
{
originalCount++;
if (skipCount > 0)
{
skipCount--;
continue;
}
if (takeCount > 0)
{
takeCount--;
yield return item;
}
}
if (takeCount == 0) yield break;
}
}
var ring = smth.ToRing();
Выбор элементов с четвёртого по шестой включительно можно осуществить несколькими способами:Словами по шестой, а в коде по седьмой?
// хвост не включается в результат
bodyLetters.Slice(3, 7); // 'D', 'E', 'F', 'G'
bodyLetters.Slice(-5, 7); // 'D', 'E', 'F', 'G'
bodyLetters.Slice(3, -1); // 'D', 'E', 'F', 'G'
bodyLetters.Slice(-5, -1); // 'D', 'E', 'F', 'G'
Для того, чтобы получить пустую выборку достаточно написать Slice(5, 6)Почему пустая будет, а не 'F'?
Словами по шестой, а в коде по седьмой?
var bodyLetters = new[] {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'I'}; var headIndexes = new[] { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 }; var tailIndexes = new[] {-8 , -7 , -6 , -5 , -4 , -3 , -2 , -1 };
var humanizedOrder = new[] {"первый", "второй", "третий", "четвёртый", "пятый", "шестой", "седьмой", "восьмой"};
По шестой включительноДа, по седьмой. Включительно.
Хвост удобно не включать, поскольку длина выборки тогда легко вычисляется по индексам без лишнего инкремента на единицу и не вызывает путаницы (5-2 = 3 вместо 5-2+1 = 4)
Циклические срезы, сдвиги и вращения коллекций