Диапазоны (Ranges) в C# 8
Пусть в нашей программе есть массив целых чисел 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
). Для того чтобы преобразовать его в массив, воспользуемся методом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. Написано с любовью вместе со своими учениками. Они у меня лучшие