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

Комментарии (15)


  1. NeoCode
    13.02.2022 01:34

    А эта фича связана только с числовыми индеками массивов, или она более общая, типа обобщения двух итераторов в произвольной коллекции, как это попытались сделать в С++?

    А ведь можно двигаться еще дальше и получить "генераторы списков"...


    1. Flux
      13.02.2022 05:05
      +4

      В шарпе не итераторы а энумераторы, они похожи но семантически разные. Энумераторы не представляют собой позицию в контейнере, они могут только выдавать текущий элемент и смещаться на следующий.
      По контракту энумератор не должен уметь сравнивать себя с другим энумератором той же коллекции, так что фича про числовые индексы, большего и не надо.


      А генераторы списков в шарпе с самого начала есть, даже yield return для их удобного написания завезли.


  1. yung6lean9
    13.02.2022 09:32

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

    а не справа налево?


    1. JosefDzeranov Автор
      13.02.2022 09:32

      Да. Конечно. Поправил. Спасибо за обратную связь


  1. dmitrysvd
    13.02.2022 10:06
    +5

    Очень похоже на срезы в питоне, только нет возможности указания шага


    1. JosefDzeranov Автор
      15.02.2022 02:54

      Да. Именно так. Пока не подвезли. Думаю скоро добавят


  1. KGeist
    13.02.2022 10:48
    +2

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

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


    1. 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];


      1. KGeist
        13.02.2022 22:09
        +1

        Так и получится. Чудес нет

        Согласен. Но в данной статье (и других похожих) делается вывод, что ranges являются (помимо прочего) решением проблем с памятью. Хотя по факту это просто синтаксический сахар, и ситуация мало поменялась, только добавилось новых подводных камней :) Я на это хотел обратить внимание.


  1. kawena54
    13.02.2022 13:00
    -1

    "Вызовы ToListToArray проходятся по коллекции и выделяют новую память.  "

    Уже нет , мы это оптимизировали , теперь мы работает с абстракциями объектов в памяти , тут выделяется память только на алокацию типа (копейка), а потом мы присваеваем ссылку на объект.

    (понятное дело эта оптимизация работает до первого изменения)


    1. navferty
      13.02.2022 17:11

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


      1. kawena54
        13.02.2022 18:43
        -1

        не там смотрите , CLR


        1. navferty
          13.02.2022 18:51

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


  1. 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);
    }

    Имхо половина аргументов в статье высосона из пальца


    1. JosefDzeranov Автор
      15.02.2022 02:56

      Вы не работали с начинающими программистами