Пусть в нашей программе есть массив целых чисел numbers:
static void Main()
{
var numbers = new int[] { 5, 1, 4, 2, 3, 7 };
}
Перед нами стоит задача: получить новый массив, вырезав из массива numbers элементы от индекса 2 до индекса 4 включительно, то есть должен получится массив [4, 2, 3].
Решение 1
Самое первое и простое решение, которое приходит в голову — это решение в лоб:
-
Создадим результирующий массив целых чисел
resultразмером3:static void Main() { var numbers = new int[] { 5, 1, 4, 2, 3, 7 }; var result = new int[3]; } -
Пройдемся циклом по нужным индексам массива
numbers, а именно с2до4включительно:static void Main() { var numbers = new int[] { 5, 1, 4, 2, 3, 7 }; var result = new int[3]; for (int i = 2; i <= 4; i++) { } } -
Запишем в результирующий массив
resultнужные значения:static void Main() { var numbers = new int[] { 5, 1, 4, 2, 3, 7 }; var result = new int[3]; for (int i = 2; i <= 4; i++) { result[i - 2] = numbers[i]; } } -
Выведем массив
resultи убедимся, что все ОК:static void Main() { var numbers = new int[] { 5, 1, 4, 2, 3, 7 }; var result = new int[3]; for (int i = 2; i <= 4; i++) { result[i - 2] = numbers[i]; } Console.WriteLine(string.Join(" ", result)); // 4 2 3 }
С задачей мы справились. Но есть некоторые недостатки:
Для решения такой маленькой задачи, пришлось пройтись циклом.
По коду не сразу понятно, что он делает. Таким образом страдает читаемость.
Также можно ошибиться с индексами (относится к начинающим программистам).
Следовательно, такое решение нас не устраивает.
Решение 2
Немногие знают, что у списка (List) есть готовый метод GetRange(int index, int count), который получает из списка нужный диапазон элементов. Метод первым параметром принимает index — индекс начала диапазона, а вторым параметром count — количество элементов, которые нужно получить. Например:
GetRange(0, 5)— получает 5 элементов, начиная с индекса 0.GetRange(3, 10)— получает 10 элементов, начиная с индекса 3.
Тогда сделаем следующее:
-
Для того чтобы мы воспользовались готовым методом
GetRange, преобразуем массив в список с помощью методаToList:static void Main() { var numbers = new int[] { 5, 1, 4, 2, 3, 7 }; var list = numbers.ToList(); } -
Воспользуемся методом
GetRange. Нам нужно взять3элемента, начиная с индекса2:static void Main() { var numbers = new int[] { 5, 1, 4, 2, 3, 7 }; var list = numbers.ToList(); var resultList = list.GetRange(2, 3); } -
Метод
GetRangeвернул результат в виде списка (List<int>). Для того чтобы преобразовать его в массив, воспользуемся методомToArray:static void Main() { var numbers = new int[] { 5, 1, 4, 2, 3, 7 }; var list = numbers.ToList(); var resultList = list.GetRange(2, 3); var result = resultList.ToArray(); } -
Выведем массив
resultи убедимся, что все ОК:static void Main() { var numbers = new int[] { 5, 1, 4, 2, 3, 7 }; var list = numbers.ToList(); var resultList = list.GetRange(2, 3); var result = resultList.ToArray(); Console.WriteLine(string.Join(" ", result)); // 4 2 3 }
С задачей мы справились. Но есть некоторые недостатки:
Для решения такой маленькой задачи, пришлось воспользоваться тремя дополнительными методами.
По коду не сразу понятно, что он делает. Таким образом страдает читаемость.
Также можно ошибиться при передаче параметров в метод
GetRange(относится к начинающим программистам).Данные преобразования ресурсоемкие по памяти и производительности. Вызовы
ToList,ToArrayпроходятся по коллекции и выделяют новую память.
Следовательно, такое решение нас не устраивает.
Решение 3
Можно еще воспользоваться технологией LINQ, а именно двумя методами:
Skip(int count)— возвращает все элементы коллекции, кроме первыхcount.Take(int count)— возвращает первыеcountэлементов коллекции.
В нашем случае, для того, чтобы взять элементы массива от индекса 2 до индекса 4 включительно, нужно пропустить 2 элемента последовательности, а затем взять первые 3 элемента. Как раз получатся элементы с индексами от 2 до 4.
Посмотрим в коде:
static void Main()
{
var numbers = new int[] { 5, 1, 4, 2, 3, 7 };
var temp = numbers.Skip(2).Take(3);
var result = temp.ToArray();
Console.WriteLine(string.Join(" ", result)); // 4 2 3
}
С задачей мы справились. Но есть некоторые недостатки:
Для решения такой маленькой задачи, пришлось воспользоваться тремя дополнительными методами.
Можно ошибиться при передаче параметров в методы
SkipиTake(относится к начинающим программистам).Данные преобразования ресурсоемкие по памяти и производительности.
Следовательно, такое решение нас не устраивает.
Решение 4
Есть еще статический метод Copy у класса Array:
Copy(Array sourceArray, int sourceIndex, Array destinationArray, int destinationIndex, int length)
Данный метод копирует элементы из одного массива в другой. Давайте поясним каждый параметр:
Array sourceArray— массив, с которого копируем элементы.int sourceIndex— с какого индекса из массиваsourceArrayначинаем копировать элементы.Array destinationArray— массив, в который копируются элементы.int destinationIndex— начиная с какого индекса в результирующем массивеdestinationArrayвставляются элементы.int length— количество элементов, которое нужно скопировать.
Давайте воспользуемся данным методом:
-
Создадим результирующий массив целых чисел
resultразмером3:static void Main() { var numbers = new int[] { 5, 1, 4, 2, 3, 7 }; var result = new int[3]; } -
Вызываем метод
Copy. Передаем массивnumbersи индекс2— откуда начинаем вырезать элементы. Затем передаем результирующий массивresultи индекс0— с какого индекса вставляются элементы. А затем передаем3— количество элементов, которое нужно скопировать:static void Main() { var numbers = new int[] { 5, 1, 4, 2, 3, 7 }; var result = new int[3]; Array.Copy(numbers, 2, result, 0, 3); } -
Выведем массив
resultи убедимся, что все ОК:static void Main() { var numbers = new int[] { 5, 1, 4, 2, 3, 7 }; var result = new int[3]; Array.Copy(numbers, 2, result, 0, 3); Console.WriteLine(string.Join(" ", result)); // 4 2 3 }
С задачей мы справились. Но есть некоторые недостатки:
Легко можно ошибиться при передаче параметров в метод
Copy(относится к начинающим программистам).Не сразу понятно как использовать метод
Copy, ведь он ничего не возвращает. Нужно понять, что результат возвращается в массиве, который был передан третьим параметром.
Следовательно, такое решение нас не устраивает.
Решение 5
В C# 8 версии добавили дополнительную функциональность для работы с диапазонами (Range). Теперь для решения нашей задачи можно написать вот так:
static void Main()
{
var numbers = new int[] { 5, 1, 4, 2, 3, 7 };
var result = numbers[2..5];
Console.WriteLine(string.Join(" ", result)); // 4 2 3
}
То есть, для того чтобы получить некоторый диапазон из коллекции, нужно в квадратных скобках указать начальный индекс, затем .. и наконец индекс конца (!!! НЕ включительно !!!).
Например:
numbers[3..10]— вырезает элементы, начиная с индекса3и заканчивая индексом9. Напоминаю, что правая граница не включается.numbers[1..7]— вырезает элементы, начиная с индекса1и заканчивая индексом6.
Если индексы будут равны между собой, то в результате получится массив нулевой длины:
static void Main()
{
var numbers = new int[] { 5, 1, 4, 2, 3, 7 };
var result = numbers[2..2];
Console.WriteLine(result.Length); // 0
}
Если первый индекс будет больше второго индекса, то возникнет исключение ArgumentOutOfRangeException во время выполнения программы:
static void Main()
{
var numbers = new int[] { 5, 1, 4, 2, 3, 7 };
var result = numbers[5..2]; // ArgumentOutOfRangeException
}
Можно использовать также индексацию справа налево (Indices), введенную тоже в C# 8 версии, про которую говорили совсем недавно:
static void Main()
{
var numbers = new int[] { 5, 1, 4, 2, 3, 7 };
var result = numbers[^5..^2];
Console.WriteLine(string.Join(" ", result)); // 1 4 2
}
Можно делать еще более веселые штучки:
Например, для получения первых
nэлементов с помощью диапазонов, нужно написатьnumbers[0..n]. Так вот, специально для случаев, когда вы хотите взять диапазон с начала массива (когда первый индекс равен0), придумали упрощение: можно индекс равный0опускать, то есть написать вот так:numbers[..n]. Такая запись более предпочтительна.Например, для получения всех элементов, кроме первых
nс помощью диапазонов, нужно написатьnumbers[n..numbers.Length]. Специально для случаев, когда вы хотите взять все элементы, кроме первыхn(начиная с индексаnи до конца массива), придумали упрощение. Так как второй индекс всегда равен длине массива, то его можно опустить, то есть написать вот так:numbers[n..]. Такая запись более предпочтительна.Ну и комбинация этих двух подходов. Для получения полной копии массива, можно написать вот так:
numbers[..], то есть опустить оба индекса. Это означает взять диапазон от начала массива до конца.
Что там под капотом?
На самом деле любой диапазон в C# 8 версии можно хранить в новом типе данных Range. Он находится в пространстве имен (namespace) System, следовательно, никакой дополнительный using при его использовании не нужно писать.
У Range существует два конструктора:
Range()– создает пустой диапазон.Range(Index start, Index end)– создает диапазон от индексаstart(включительно) и до индексаend(НЕ включительно).
Рассмотрим на примерах:
static void Main()
{
var numbers = new int[] { 5, 1, 4, 2, 3, 7 };
var range1 = new Range();
var result = numbers[range1]; // пустой массив
var range2 = new Range(2, 5);
result = numbers[range2]; // 4 2 3
var range3 = new Range(1, 3);
result = numbers[range3]; // 1 4
}
Заметьте, что объект типа Range передается в качестве индекса в квадратные скобки ([]).
Проведем соответствие между двумя разными записями:
Укороченная версия |
Версия с |
|---|---|
|
|
|
|
В Range реализовано неявное преобразование укороченной записи (например 2..5) к Range. Вот как это работает:
static void Main()
{
var numbers = new int[] { 5, 1, 4, 2, 3, 7 };
Range range = 2..5;
var result = numbers[range]; // 4 2 3
}
У Range переопределен метод Equals:
static void Main()
{
Range range1 = 2..5;
Range range2 = 2..5;
Range range3 = 1..6;
Console.WriteLine(range1.Equals(range2)); // True
Console.WriteLine(range1.Equals(range3)); // False
}
А можно вообще вот так:
static void Main()
{
Range range1 = 2..5;
Console.WriteLine(range1.Equals(2..5)); // True
Console.WriteLine(range1.Equals(1..6)); // False
}
Здесь сначала происходит неявное преобразование укороченной записи к Range, а потом вызов Equals.
У Range переопределен также метод ToString:
static void Main()
{
Range range1 = 2..5;
Range range2 = ^6..^3;
Console.WriteLine(range1.ToString()); // 2..5
Console.WriteLine(range2.ToString()); // ^6..^3
}
Заметьте, что для индексации с конца выводится ^ перед индексом.
Также теперь мы можем в методы передавать диапазон:
static void Test(int[] numbers, Range range)
{
// логика
}
Выводы:
Структура
Rangeпозволяет создать экземпляр, к которому можно обращаться многократно.Код становится более короткий и читаемый.
Увеличивается производительность без обращения к лишним методам.
Меньше нагрузка на память.
PS. Написано с любовью вместе со своими учениками. Они у меня лучшие ????
Комментарии (15)

yung6lean9
13.02.2022 09:32Можно использовать также индексацию слева направо (
Indices), введенную тоже вC#8версии, про которую говорили совсем недавно:а не справа налево?

KGeist
13.02.2022 10:48+2Меньше нагрузка на память.
Почитал документацию - range на массивы создаёт копию массива в нужном срезе, т.е. я не понял, почему нагрузка на память меньше. Тут ведь всё ещё хуже - новый синтаксис скрывает аллокации потенциально больших данных. Согласно той же документации, копирования нет только на Span и Memory. Т.е. нужно обращать внимание на тип коллекции, чтобы понять, будет выделение памяти или нет. И со Span'ом тоже спорный вопрос. Допустим, у меня есть массив в мегабайт, далее я создал range на 3 элемента и собираюсь пользоваться только им - не получится ли так, что в памяти будет болтаться 1 мегабайт бесхозной памяти, потому что Span удерживает ссылку на массив в целом?

kefirr
13.02.2022 12:24+3Да, нагрузка на память в Решении 5 ровно такая же, как в Решении 1. Под капотом вызывается
RuntimeHelpers.GetSubArray, который создаёт новый массив.есть массив в мегабайт, далее я создал range на 3 элемента и собираюсь пользоваться только им - не получится ли так, что в памяти будет болтаться 1 мегабайт бесхозной памяти, потому что Span удерживает ссылку на массив в целом
Так и получится. Чудес нет - либо выделяем новый массив из 3 элементов, и старый будет удалён сборщиком мусора, либо делаем
AsMemory/AsSpan- очень быстро и не выделяет память, но удерживает большой массив.Вообще в подобной статье было бы здорово упомянуть
AsMemory&AsSpan, они тоже работают сRange:var numbers = new int[] { 5, 1, 4, 2, 3, 7 }; Memory<int> result = numbers.AsMemory()[2..5];
KGeist
13.02.2022 22:09+1Так и получится. Чудес нет
Согласен. Но в данной статье (и других похожих) делается вывод, что ranges являются (помимо прочего) решением проблем с памятью. Хотя по факту это просто синтаксический сахар, и ситуация мало поменялась, только добавилось новых подводных камней :) Я на это хотел обратить внимание.

kawena54
13.02.2022 13:00-1"Вызовы
ToList,ToArrayпроходятся по коллекции и выделяют новую память. "Уже нет , мы это оптимизировали , теперь мы работает с абстракциями объектов в памяти , тут выделяется память только на алокацию типа (копейка), а потом мы присваеваем ссылку на объект.
(понятное дело эта оптимизация работает до первого изменения)

navferty
13.02.2022 17:11А поделитесь пожалуйста ссылкой на пруфы. Не вижу подобного ни в RangeIterator, ни в EnumerableHelpers, ни в Enumerable.ToList

kawena54
13.02.2022 18:43-1не там смотрите , CLR

navferty
13.02.2022 18:51Так репозиторий по ссылке и есть runtime. Поделитесь своей ссылкой, если не трудно?

KislyFan
13.02.2022 13:13+8Можно ошибиться при передаче параметров в методы
SkipиTake(относится к начинающим программистам).Где тут можно ошибиться когда методы так и называются "пропустить" и "взять", и передать только целоцисленное значение
https://referencesource.microsoft.com/#system.core/system/linq/Enumerable.cs,591
public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> source, int count) { if (source == null) throw Error.ArgumentNull("source"); return TakeIterator<TSource>(source, count); }Имхо половина аргументов в статье высосона из пальца
NeoCode
А эта фича связана только с числовыми индеками массивов, или она более общая, типа обобщения двух итераторов в произвольной коллекции, как это попытались сделать в С++?
А ведь можно двигаться еще дальше и получить "генераторы списков"...
Flux
В шарпе не итераторы а энумераторы, они похожи но семантически разные. Энумераторы не представляют собой позицию в контейнере, они могут только выдавать текущий элемент и смещаться на следующий.
По контракту энумератор не должен уметь сравнивать себя с другим энумератором той же коллекции, так что фича про числовые индексы, большего и не надо.
А генераторы списков в шарпе с самого начала есть, даже
yield returnдля их удобного написания завезли.