Пусть в нашей программе есть массив целых чисел 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
для их удобного написания завезли.