Всем привет. Начиная с этой статьи начинаю цикл статей про нововведения в C# 8 версии.

Сейчас мы рассмотрим работу с индексами (Indexes). Забегая вперед, скажу, что теперь мы, C# разработчики, можем работать с индексами как в Python.

Пристегнитесь. Начинаем????

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

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

Перед нами стоит задача получить последний элемент массива.

Мы с вами знаем, что доступ к элементам массива осуществляется по индексу. Следует помнить, что индексация начинается с нуля – первый элемент массива имеет индекс 0, а последний – n - 1, где n – размер массива: 

Наглядный вид массива
Наглядный вид массива

Если мы заранее знаем количество элементов в массиве, то можно чуть упростить себе жизнь ???? Для нашего примера, мы знаем, что в массиве 6 элементов, следовательно, чтобы получить последний элемент, нужно обратиться к элементу с индексом 5, то есть написать следующим образом:

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

Это конечно же временное решение. Не всегда мы заранее знаем количество элементов в массиве. Например, есть некоторый метод Test, внутри которого для получения результата нужно получить последний элемент переданного массива: 

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

static void Test(int[] numbers)
{
  // Так уже не напишешь. 
  // Мы внутри метода не знаем какое количество элементов в массиве
  var lastNumber = numbers[5];
}

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

Как мы до этого уже говорили, можно рассчитать последний элемент с помощью длины массива, так как последний элемент имеет индекс – n - 1, где n – размер/длина массива. Для того, чтобы получить размерность массива, нужно воспользоваться функцией Length:

static void Test(int[] numbers)
{   
	// Получаем длину массива 
	var length = numbers.Length;
	
  // Получаем последний элемент</span>
	var lastNumber = numbers[length - 1];
}

Какие есть в  этом коде минусы:  

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

  2. Код становится менее читаемым. 

Решение

В C# 8 версии добавили дополнительную функциональность для работы с индексами. Я напомню, что была индексация слева направо, начинающаяся с 0. Теперь добавили новую индексацию справа налево (начинается с конца массива), начинающаяся  с 1. Для лучшего понимания рассмотрим таблицу: 

Индексация слева направо

0

1

2

3

4

5

Массив

5

1

4

2

3

7

Индексация справа налево

6

5

4

3

2

1

Жирным выделен сам массив. Над ним привычная нам индексация, а под ним новая индексация. 

Для использования новой индексации необходимо перед значением индекса поставить символ ^. Пишется данный символ на английской раскладке с помощью комбинации клавиш shift + 6:

static void Main()
{
	var numbers = new int[] { 5, 1, 4, 2, 3, 7 };
  Console.WriteLine(numbers[^1]); // 7
  Console.WriteLine(numbers[^5]); // 1
  Console.WriteLine(numbers[^6]); // 5
}

Таблица получения каждого элемента двумя способами: 

Выражение

Выражение

Результат

Пояснение

numbers[0]

numbers[^6]

5

Первый элемент

numbers[1]

numbers[^5]

1

Второй элемент

numbers[2]

numbers[^4]

4

Третий элемент

numbers[3]

numbers[^3]

2

Четвертый элемент

numbers[4]

numbers[^2]

3

Пятый элемент

numbers[5]

numbers[^1]

7

Шестой элемент

Запомните: 

Теперь появился универсальный, удобный и логически понятный способ получения последнего элемента массива. В массиве любого размера для получения последнего элемента нужно обратиться к индексу ^1, например, numbers[^1]

Теперь зная про новый способ индексации перепишем нашу функцию Test:

static void Test(int[] numbers)
{   
  // Получаем последний элемент</span>
	var lastNumber = numbers[^1];
}

ВНИМАНИЕ!

Отсчет индексации справа налево начинается с 1, а не с 0. Если указать значение меньше 1 или больше индекса первого элемента, то возникнет исключение - ArgumentOutOfRangeException:

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

	Console.WriteLine(numbers[^0]); // ArgumentOutOfRangeException
	Console.WriteLine(numbers[^7]); // ArgumentOutOfRangeException
}

Что там под капотом?

На самом деле любой индекс с появлением C# 8 версии можно хранить в новом типе данных Index. Он находится в пространстве имен (namespaceSystem, следовательно, никакой дополнительный using при его использовании не нужно писать. 

У Index существует два конструктора:

  • Index() – создает индекс, указывающий на первый элемент массива (с индексом 0). 

  • Index(int value, bool fromEnd = false) – позволяет задать значение индекса (value) и определить ведется ли отчет от начала (fromEnd = false) или от конца массива (fromEnd = true). Соответственно важно помнить, что для индексов с отсчетом от конца массива счет начинается с 1 (а не с 0 как в обычном случае). Заметьте, что второй параметр по умолчанию имеет значение false.

Рассмотрим на примерах:

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

	var indexFirst = new Index(0); // индекс первого элемента
	Console.WriteLine(numbers[indexFirst]); // 5

	indexFirst = new Index(); // индекс первого элемента
	Console.WriteLine(numbers[indexFirst]); // 5

	var indexLast = new Index(1, fromEnd: true); // индекс последнего элемента
	Console.WriteLine(numbers[indexLast]); // 7

	var index1 = new Index(3); // индекс 3 элемента
	Console.WriteLine(numbers[index1]); // 2

	index1 = new Index(3, fromEnd: false); // индекс 3 элемента
	Console.WriteLine(numbers[index1]); // 2

	var index2 = new Index(5, fromEnd: true); // индекс 5 элемента с конца
	Console.WriteLine(numbers[index2]); // 1
}

Заметьте, что объект типа Index передается в качестве индекса в квадратные скобки ([]). 

Проведем соответствие между двумя разными записями:

Укороченная версия

Версия c Index

numbers[2]

numbers[new Index(2)] 

или 

numbers[new Index(2, fromEnd: false)]

numbers[^2]

numbers[new Index(2, fromEnd: true)]

В Index реализовано неявное преобразование целого числа (int) к Index. Вот как это работает: 

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

	Index index1 = 2;
	Console.WriteLine(numbers[index1]); // 4

	Index index2 = ^2;
	Console.WriteLine(numbers[index2]); // 3
}

У Index переопределен метод Equals:

static void Main()
{
	var index1 = new Index(3);
	var index2 = new Index(3);
	Console.WriteLine(index1.Equals(index2)); // true
}

А можно вообще вот так: 

static void Main()
{
	var index1 = new Index(3);
	Console.WriteLine(index1.Equals(3)); // true
}

Здесь сначала происходит неявное преобразование 3 к Index, а потом вызов Equals

У Index переопределен также метод ToString:

static void Main()
{
	var index1 = new Index(3);
	var index2 = new Index(3, fromEnd: true);
	Console.WriteLine(index1.ToString()); // 3
	Console.WriteLine(index2.ToString()); // ^3
}

Заметьте, что для индексации с конца выводится ^ перед индексом. 

Так как теперь мы можем индекс хранить в переменной типа Index, то можно чуть переписать нашу функцию Test:

static void Test(int[] numbers)
{
	// Сохраняем последний индекс
	Index lastIndex = ^1;
	
	// Получаем последний элемент
	var lastNumber = numbers[lastIndex];

	// Какие то действия, которые меняют элементы массива

	lastNumber = numbers[lastIndex];

	// Какие то действия, которые меняют элементы массива

	lastNumber = numbers[lastIndex];
}

Мы сохранили в переменной lastIndex индекс последнего элемента и использовали во всех местах.

Также теперь мы можем в методы передавать индекс:

static void Test(int[] numbers, Index index)
{
	// логика
}

Выводы

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

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

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


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

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


  1. bm13kk
    01.02.2022 15:38
    +2

    Я ожидал что-то про срезы[start:end:step]


    1. JosefDzeranov Автор
      01.02.2022 17:56
      +1

      Скоро выйдет статья про срезы


  1. diogen4212
    01.02.2022 16:32
    +7

    Отсчет индексации справа налево начинается с 1, а не с 0

    Сколько лабораторок и собесов на джуниора провалится…


    1. Deosis
      02.02.2022 07:24

      В статье стоит упомянуть, что индексы ввели одновременно с диапазонами, а там ^0 может использоваться и имеет довольно логичное значение.

      В c++ аналогом будет итератор, возвращаемый методом end.


      1. bm13kk
        02.02.2022 11:12

        Или написать сразу про все фичи связанные индексами массивов. Потому что текущая статья вроде и имеет обьем, но информации маловато.


  1. z0ic
    01.02.2022 16:36

    А индексацию булевыми значениями array[array > 0] можно делать ?


    1. AgentFire
      01.02.2022 18:31

      Заодно и стринговыми, потому что почему бы и нет, да?


    1. DarthLexus
      01.02.2022 19:51

      а это уже Range (https://docs.microsoft.com/ru-ru/dotnet/api/system.range?view=net-6.0)

      int[] afterZero = array[1..^0];

      ну или LINQ:

      IEnumerable<int> tail = array.Where((x, i) => i > 0);


  1. Myxach
    01.02.2022 17:51

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

    Как можно забыть, если ты по идее, должен array.len-1 сохранить в отдельном переменной, если array.len не меняется


    1. JosefDzeranov Автор
      01.02.2022 17:57

      Поверьте. Начинающие программисты часто забывают про это.


  1. sergey_prokofiev
    01.02.2022 17:54
    +1

    абсолютно ненужная свистоперделка, жаль что таки включили....


    1. JosefDzeranov Автор
      01.02.2022 17:57

      Есть и плюсы и минусы.


  1. truthfinder
    01.02.2022 18:33
    +6

    Правильно indices.


  1. alexander222
    01.02.2022 21:00
    +1

    То есть от начала мы считаем с 0, а от конца с 1. Читаемость кода теми, кто не использует эту фичу постоянно очень усложняется


    1. enabokov
      03.02.2022 12:02

      Я тоже разочарован этим фактом. Была какая-то причина так делать?


      1. alexander222
        03.02.2022 12:19

        Логика в этом есть- беря первый элемент от начала мы "пропускаем" 0 элементов, и читаем один. Беря первый от конца, мы должны сначала "пропустить" один, а потом его считать. Но для человека такая логика не сразу интуитивна


      1. bm13kk
        03.02.2022 12:19

        Если не использовать спец символов (как в питоне, с которым идет сравнение в этой статье) - отрицательные индексы начинаются с -1, потому что 0 - уже занят в положительных индексах.


  1. NeoCode
    01.02.2022 23:05
    +1

    Мне кажется, лучше всего сделано в D - там используется контекстный символ $, означающий размер индексируемого массива. В результате получается

    int arr[] = {1,2,3,4,5};
    int x = arr[0];   // 1
    int y = arr[$-1]; // 5

    т.е. да, мы видим что последний элемент имеет индекс "$-1" (т.е. понятно откуда в C#8 растут ноги индексации с единицы). Запись на 1 символ больше чем в C#8, зато никаких специальных типов, можно по прежнему использовать обычные целые числа.

    А вот идея индексации с конца отрицательными числами, как делается в некоторых языках - не самая лучшая. Индекс может оказаться отрицательным случайно, по ошибке.


    1. Naf2000
      01.02.2022 23:26

      Но и за контекст не вытащить. А в шарпе это System.Index может быть параметром функции, например.


    1. sophist
      02.02.2022 10:54

      А можно написать, например, так: arr[$ / 2]?


      1. NeoCode
        02.02.2022 22:16

        Да, компилируется и работает как и ожидается.


  1. sophist
    02.02.2022 10:45
    +3

    Следует помнить, что индексация начинается с нуля…

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

    Теперь появился универсальный, удобный и логически понятный способ получения последнего элемента массива.

    …важно помнить, что для индексов с отсчетом от конца массива счет начинается с 1 (а не с 0, как в обычном случае).

    {
    // логика
    }

    Простите, не удержался :)


  1. camelCaps
    03.02.2022 13:04

    // Получаем длину массива
    var length = numbers.Length;
    // Получаем последний элемент
    var lastNumber = numbers[length - 1];

    Какие есть в  этом коде минусы:

    1. Можно забыть отнять единицу и получить ошибку во время выполнения работы.

    2. Код становится менее читаемым

    Не согласен с обоими пунктами. Первое: с таким же успехом можно забыть и все остальное - даже то, как создать новый проект в VS. Второе: читаемость отличная; этот код можно дословно перевести на русский язык и его поймет половина НЕпрограммистов.

    А вот что забыл сам автор, так это проверку length на 0.


    1. JosefDzeranov Автор
      04.02.2022 00:18

      Все таки наше общество программистов ещё не доросли до общества западных программистов.

      Нам бы лишь бы найти за что упрекнуть. Откуда это все идёт не понимаю


      1. camelCaps
        04.02.2022 03:27

        Не думайте, что западные программисты в своих сообществах только и делают, что обмазывают друг друга восхвалениями. Дискуссии на фоне несогласий у них случаются не менее пылкие, знаете ли. Безусловно, я мог бы формулировать свои мысли более деликатно (в стиле "да, но" или как-то подобно тому), но, к своему стыду, не посчитал, что отказ от этого принесет кому-либо душевных травм. Вполне возможно, представители "сообщества западных программистов" более склонны не задеть чувства окружающих, но, скорее того, отличия между "ими" и "нами" в другом: воспринимать критику как акт агрессии - это да, это наше, родное.


  1. inklesspen
    03.02.2022 13:04

    Напрягает лишь, что есть возможность хранить индекс объектом, и что возможность в [] указывать new Index() позволяет и поощрает создавать отдельный объект для обращения к элементу по индексу, а создавать лишние объекты - не есть хорошо. Но если данная фича также имеет поддержку compile-time развертки a[^N] в a[a.lenght - N], то вопросов меньше


  1. kawena54
    03.02.2022 13:05

    а рэнжи то в C# завезли?

    var part = myarray[1-4];


    1. JosefDzeranov Автор
      04.02.2022 00:16

      Да


  1. DadeMurphyZC
    03.02.2022 13:37
    +1

    Лично мне нравится, что появилась такая функция (индексы). Пригодится.


  1. lenyaplay
    03.02.2022 15:11

    Где то под капотом: ^number=.length-number