Диапазоны (Ranges) в C# 8

от автора

​Пусть в нашей программе есть массив целых чисел numbers:

static void Main() { var numbers = new int[] { 5, 1, 4, 2, 3, 7 }; }

Перед нами стоит задача: получить новый массив, вырезав из массива numbers элементы от индекса 2 до индекса 4 включительно, то есть должен получится массив [4, 2, 3].

Решение 1

Самое первое и простое решение, которое приходит в голову — это решение в лоб: 

  1. Создадим результирующий массив целых чисел result размером 3

    static void Main() { var numbers = new int[] { 5, 1, 4, 2, 3, 7 };  var result = new int[3]; }
  2. Пройдемся циклом по нужным индексам массива 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++) {  } }

     

  3. Запишем в результирующий массив 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]; } }

     

  4. Выведем массив 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 }

С задачей мы справились. Но есть некоторые недостатки: 

  1. Для решения такой маленькой задачи, пришлось пройтись циклом.  

  2. По коду не сразу понятно, что он делает. Таким образом страдает читаемость.  

  3. Также можно ошибиться с индексами (относится к начинающим программистам). 

Следовательно, такое решение нас не устраивает.  

Решение 2

Немногие знают, что у списка (List) есть готовый метод GetRange(int index, int count), который получает из списка нужный диапазон элементов. Метод первым параметром принимаем index — индекс начала диапазона, а вторым параметром count — количество элементов, которые нужно получить. Например: 

  • GetRange(0, 5) — получает 5 элементов, начиная с индекса 0.  

  • GetRange(3, 10) — получает 10 элементов, начиная с индекса 3.

Тогда сделаем следующее: 

  1. Для того чтобы мы воспользовались готовым методом GetRange, преобразуем массив в список с помощью метода ToList:

    static void Main() { var numbers = new int[] { 5, 1, 4, 2, 3, 7 };  var list = numbers.ToList(); }

     

  2. Воспользуемся методом 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); }

     

  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(); }

     

  4. Выведем массив 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 }

С задачей мы справились. Но есть некоторые недостатки: 

  1. Для решения такой маленькой задачи, пришлось воспользоваться тремя дополнительными методами.  

  2. По коду не сразу понятно, что он делает. Таким образом страдает читаемость.  

  3. Также можно ошибиться при передаче параметров в метод GetRange (относится к начинающим программистам).   

  4. Данные преобразования ресурсоемкие по памяти и производительности. Вызовы ToList, ToArray проходятся по коллекции и выделяют новую память. 

Следовательно, такое решение нас не устраивает. 

Решение 3

Можно еще воспользоваться технологией LINQ, а именно двумя методами: 

  1. Skip(int count) — возвращает все элементы коллекции, кроме первых count.   

  2. 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 }

С задачей мы справились. Но есть некоторые недостатки: 

  1. Для решения такой маленькой задачи, пришлось воспользоваться тремя дополнительными методами.  

  2. Можно ошибиться при передаче параметров в методы Skip и  Take (относится к начинающим программистам).  

  3. Данные преобразования ресурсоемкие по памяти и производительности. 

Следовательно, такое решение нас не устраивает. 

Решение 4

Есть еще статический метод Copy у класса Array:

Copy(Array sourceArray, int sourceIndex, Array destinationArray, int destinationIndex, int length)

Данный метод копирует элементы из одного массива в другой. Давайте поясним каждый параметр:

  1. Array sourceArray — массив, с которого копируем элементы.  

  2. int sourceIndex — с какого индекса из массива sourceArray начинаем копировать элементы.  

  3. Array destinationArray — массив, в который копируются элементы.  

  4. int destinationIndex — начиная с какого индекса в результирующем массиве destinationArray вставляются элементы.  

  5. int length — количество элементов, которое нужно скопировать.

Давайте воспользуемся данным методом: 

  1. Создадим результирующий массив целых чисел result размером 3

    static void Main() { var numbers = new int[] { 5, 1, 4, 2, 3, 7 };  var result = new int[3]; }
  2. Вызываем метод 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); }

     

  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 }

С задачей мы справились. Но есть некоторые недостатки: 

  1. Легко можно ошибиться при передаче параметров в метод Copy (относится к начинающим программистам).   

  2. Не сразу понятно как использовать метод 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. Он находится в пространстве имен (namespaceSystem, следовательно, никакой дополнительный 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

numbers[2..5]

numbers[new Range(2, 5)]

numbers[^6..^2]

numbers[new Range(^6, ^2)]

В 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) { // логика }

Выводы:

  1. Структура Range позволяет создать экземпляр, к которому можно обращаться многократно.

  2. Код становится более короткий и  читаемый.

  3. Увеличивается производительность без обращения к лишним методам.

  4. Меньше нагрузка на память.


PS. Написано с любовью вместе со своими учениками. Они у меня лучшие ?

ы

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Понравилась данная фича?
0% Нет 0
0% Да 0
0% Даже не знаю 0
Никто еще не голосовал. Воздержался 1 пользователь.

ссылка на оригинал статьи https://habr.com/ru/post/651171/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *