$ sudo teach IT
Модуль 5 · Коллекции

Урок 5.0 — Индексы

Нумерация с нуля, доступ по индексу, диапазоны и оператор ^ для элементов с конца.

Представь себе шеренгу людей. Если ты скажешь «третий слева» — это индекс. Только в C#, как и в большинстве языков программирования, счёт начинается не с единицы, а с нуля. Первый элемент — это элемент под номером 0. Сегодня мы научимся уверенно работать с индексами, а также освоим современный синтаксис диапазонов, который появился в C# 8.0.

🔢

Что такое индекс и почему счёт с нуля?

Индекс — это номер позиции элемента внутри коллекции. Коллекция — это набор элементов, расположенных друг за другом: массив чисел, строка текста, список покупателей. Каждый элемент коллекции имеет свой номер — индекс.

Почему счёт начинается с 0, а не с 1? Дело в том, что в памяти компьютера массив — это просто непрерывный блок ячеек. Адрес первого элемента совпадает с адресом начала массива (смещение 0). Второй элемент находится со смещением 1, третий — со смещением 2, и так далее. Нулевой индекс — это просто смещение от начала. То есть для компьютера индекс — это не «номер по порядку», а «сколько шагов нужно сделать от начала, чтобы добраться до элемента».

Поэтому:

  • Первый элемент — индекс 0
  • Второй элемент — индекс 1
  • Третий элемент — индекс 2
  • Последний элемент — индекс длина − 1

Эту особенность нужно просто запомнить. Она есть в C, C++, Java, JavaScript, Python, Go, Rust и большинстве других языков. Новички часто путаются: «Почему у меня ошибка, ведь я хочу третий элемент, а пишу индекс 2?» — потому что счёт с нуля.

C# · Визуализация индексов
Массив:  [10] [20] [30] [40] [50]
Индекс:   0    1    2    3    4
         ↑ первый           ↑ последний
         элемент             (длина - 1)
💡 Запомни-правило: Если в коллекции N элементов, то последний индекс — это N - 1. Для массива из 5 элементов последний индекс — 4. Для массива из 100 элементов — 99.
🎯

Доступ к элементу по индексу

Чтобы получить элемент коллекции, нужно после имени переменной написать индекс в квадратных скобках []:

C# · Доступ по индексу
int[] numbers = { 10, 20, 30, 40, 50 };

int first = numbers[0];     // 10  — первый элемент
int third = numbers[2];     // 30  — третий элемент
int last = numbers[4];      // 50  — последний (длина 5, последний индекс 4)

Console.WriteLine(first);    // 10
Console.WriteLine(third);   // 30
Console.WriteLine(last);    // 50

Тот же синтаксис работает для строк и списков:

C# · Индексы для строк и списков
string word = "Привет";
char firstChar = word[0];     // 'П'
char fourthChar = word[3];    // 'в'

List<string> fruits = new List<string> { "яблоко", "банан", "вишня" };
string secondFruit = fruits[1];  // "банан"

Ты можешь не только читать, но и записывать значение по индексу (если коллекция изменяема):

C# · Изменение по индексу
int[] arr = { 1, 2, 3 };
arr[0] = 100;                 // теперь [100, 2, 3]
arr[arr.Length - 1] = 999;   // теперь [100, 2, 999]
// arr[3] = 500;  // ❌ Ошибка! IndexOutOfRangeException

⚠️ IndexOutOfRangeException — самая частая ошибка у новичков. Она происходит, когда ты пытаешься обратиться к индексу, которого не существует. Например, в массиве из 3 элементов индексы 0, 1, 2. Если написать arr[3] — программа упадёт с ошибкой. Компилятор C# такую ошибку не ловит, она проявляется только во время выполнения.

📏

Свойство .Length и .Count

У каждой коллекции есть свойство, которое возвращает количество элементов. У массивов и строк это .Length, у списков .Count:

C# · Length и Count
int[] array = { 10, 20, 30, 40, 50 };
Console.WriteLine(array.Length);   // 5

string text = "Привет";
Console.WriteLine(text.Length);    // 6  (6 символов)

List<int> list = new List<int> { 1, 2, 3 };
Console.WriteLine(list.Count);     // 3

Используя Length/Count, мы можем безопасно обращаться к последнему элементу:

int[] numbers = { 10, 20, 30, 40, 50 };
int lastIndex = numbers.Length - 1;  // 4
int lastElement = numbers[lastIndex]; // 50

// Эквивалентная запись в одну строку:
int last = numbers[numbers.Length - 1];  // 50
🎩

Оператор «шляпка» (hat operator) ^

Начиная с C# 8.0 (2019 год) появился новый синтаксис для работы с индексами — оператор «шляпка» ^ (hat operator, символ циркумфлекса). Он позволяет отсчитывать элементы с конца коллекции.

Идея проста: ^1 — это последний элемент, ^2 — предпоследний, ^3 — третий с конца, и так далее. При этом ^0 — это «один за последним», то есть обращение по ^0 вызовет ошибку, так же как обращение по индексу, равному длине коллекции.

C# · Оператор ^
int[] numbers = { 10, 20, 30, 40, 50 };

int last = numbers[^1];         // 50  — последний элемент
int secondToLast = numbers[^2]; // 40  — предпоследний
int thirdToLast = numbers[^3];  // 30  — третий с конца

Console.WriteLine(last);          // 50
Console.WriteLine(secondToLast); // 40
Console.WriteLine(thirdToLast);  // 30

Как это работает «под капотом»: ^n преобразуется в индекс length - n. То есть numbers[^1] — это то же самое, что numbers[numbers.Length - 1]. Просто читается гораздо легче.

C# · Эквивалентность
numbers[^1]  ⇔  numbers[numbers.Length - 1]
numbers[^2]  ⇔  numbers[numbers.Length - 2]
numbers[^3]  ⇔  numbers[numbers.Length - 3]

Оператор ^ работает с массивами, строками, списками — любыми коллекциями, которые поддерживают индексацию:

C# · ^ со строками и списками
string word = "Привет!";
char lastChar = word[^1];          // '!'
char secondLast = word[^2];        // 'т'

List<string> names = new List<string> { "Анна", "Борис", "Вика" };
string lastName = names[^1];       // "Вика"

// Можно с переменной
int fromEnd = 2;
int element = numbers[^fromEnd];   // 40 — то же самое что numbers[^2]
💡 Оператор ^ стал настоящим спасением, когда нужно быстро получить последний элемент. Раньше приходилось писать array[array.Length - 1] — громоздко и легко ошибиться. Теперь это просто array[^1].
🔀

Диапазоны (Ranges) ..

Тот же C# 8.0 подарил нам диапазоны (ranges) — лаконичный способ взять несколько элементов подряд из коллекции. Диапазон записывается через две точки: ...

Общий синтаксис: коллекция[начало .. конец]. Результат — это новая коллекция (точнее, срез), содержащая элементы от начало включительно до конец исключительно.

Математически: [начало, конец) — то есть начало входит, конец не входит. Это то же правило, что и во всём C#: счёт от 0, конец не включается.

C# · Базовые диапазоны
int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

int[] slice1 = numbers[1..4];    // { 1, 2, 3 }     — элементы с 1 по 3 (4 не входит)
int[] slice2 = numbers[2..7];    // { 2, 3, 4, 5, 6 }
int[] slice3 = numbers[0..3];    // { 0, 1, 2 }     — первые 3 элемента

Диапазоны можно комбинировать с оператором ^:

C# · Диапазоны с ^
int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

int[] lastThree = numbers[^3..];    // { 7, 8, 9 }   — последние 3
int[] middle = numbers[3..^2];      // { 3, 4, 5, 6, 7 } — с 3-го до предпоследнего
int[] around = numbers[^5..^1];     // { 5, 6, 7, 8 }

Опускать начало или конец — тоже валидно. Если начало опущено, это означает «от самого начала». Если конец опущен — «до самого конца»:

C# · Открытые диапазоны
int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

int[] all = numbers[..];           // { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } — весь массив
int[] firstFive = numbers[..5];    // { 0, 1, 2, 3, 4 }     — с начала до 5-го (не включая)
int[] fromFive = numbers[5..];     // { 5, 6, 7, 8, 9 }     — с 5-го до конца

Срезы работают не только с массивами, но и со строками:

C# · Диапазоны строк
string fullName = "Александр Сергеевич Пушкин";

string firstName = fullName[..9];          // "Александр"
string lastName = fullName[^6..];          // "Пушкин"
string middle = fullName[10..^7];          // "Сергеевич"
string whole = fullName[..];               // вся строка

// Можно и так:
string withoutFirst = fullName[1..];       // "лександр Сергеевич Пушкин"
string withoutLast = fullName[..^1];       // "Александр Сергеевич Пушки"

Важно: срезы для массивов создают новый массив с копией элементов. Изменение среза не влияет на исходный массив:

C# · Срез — это копия
int[] original = { 1, 2, 3, 4, 5 };
int[] slice = original[1..4];    // { 2, 3, 4 }

slice[0] = 999;                  // меняем срез
Console.WriteLine(slice[0]);     // 999
Console.WriteLine(original[1]);  // 2 — оригинал не изменился!

Для списков (List<T>) диапазоны тоже работают через метод .GetRange() или через срез (начиная с C# 8.0):

C# · Диапазоны для List
List<int> list = new List<int> { 10, 20, 30, 40, 50 };

// Через срез (возвращает массив, не List!)
int[] sliceArr = list[1..4];     // { 20, 30, 40 }

// Через GetRange (возвращает List<T>)
List<int> sliceList = list.GetRange(1, 3);  // { 20, 30, 40 }

// Срез с ^
int[] lastTwo = list[^2..];      // { 40, 50 }
🧩

Таблица: индексы, ^ и диапазоны

Давайте сведём в таблицу все способы доступа к элементам для массива [10, 20, 30, 40, 50] (длина 5):

Выражение
Результат
Пояснение
arr[0]
10
Первый элемент
arr[4]
50
Последний (длина-1)
arr[^1]
50
Последний (C# 8+)
arr[^2]
40
Предпоследний
arr[1..4]
{20,30,40}
С 1 по 3 (4 не входит)
arr[..3]
{10,20,30}
Первые 3 элемента
arr[2..]
{30,40,50}
С 2 до конца
arr[..]
{10,20,30,40,50}
Весь массив (копия)
🏗️

Типы Index и Range

За синтаксисом ^1 и 1..4 стоят специальные типы: System.Index и System.Range. Ты можешь создавать их явно и использовать в своих структурах данных:

C# · Index и Range
// Явное создание Index
Index i1 = 3;           // третий элемент (с начала, 0-based)
Index i2 = ^2;           // второй с конца

int[] numbers = { 10, 20, 30, 40, 50 };
Console.WriteLine(numbers[i1]);  // 40
Console.WriteLine(numbers[i2]);  // 40

// Явное создание Range
Range r1 = 1..4;         // элементы 1,2,3
Range r2 = ..3;           // первые 3
Range r3 = ^3..;          // последние 3
Range r4 = ..;            // все

int[] slice = numbers[r1];  // { 20, 30, 40 }

Это удобно, когда тебе нужно передать индекс или диапазон как параметр в метод:

C# · Передача как параметр
static int GetElement(int[] array, Index index)
{
    return array[index];
}

static int[] GetSlice(int[] array, Range range)
{
    return array[range];
}

// Использование:
int[] arr = { 1, 2, 3, 4, 5 };
Console.WriteLine(GetElement(arr, ^1));   // 5
int[] slice = GetSlice(arr, 1..4);       // { 2, 3, 4 }
⚠️

Типичные ошибки с индексами

Давай разберём самые частые ошибки, которые допускают новички:

❌ Ошибка 1: Путаница с нумерацией

Новичок хочет получить третий элемент и пишет arr[3]. Но на самом деле третий элемент — это индекс 2.

int[] arr = { 10, 20, 30 };
Console.WriteLine(arr[3]); // ❌ IndexOutOfRangeException! Индексы: 0,1,2
Console.WriteLine(arr[2]); // ✅ 30 — третий элемент

❌ Ошибка 2: Пустой массив

Если массив пустой (длина 0), то любой доступ по индексу вызовет ошибку.

int[] empty = { };
Console.WriteLine(empty[0]);  // ❌ IndexOutOfRangeException! Массив пуст

❌ Ошибка 3: Путаница длины и последнего индекса

Если длина массива — 5, то индексы — 0,1,2,3,4. Последний индекс — 4, а не 5.

int[] arr = { 1, 2, 3, 4, 5 };
Console.WriteLine(arr[arr.Length]); // ❌ Ошибка! Length=5, но индексы до 4
Console.WriteLine(arr[arr.Length - 1]); // ✅ 5 — последний элемент
🧩

Применение к разным типам

Индексы, оператор ^ и диапазоны применимы к любым типам, которые реализуют интерфейс IList<T> или имеют индексатор. Вот какие коллекции поддерживают индексацию в C#:

✅ Массивы (T[]) — полная поддержка

arr[2], arr[^1], arr[1..4]

✅ Строки (string) — полная поддержка (только чтение)

str[3] (возвращает char), str[^1], str[2..5] (возвращает string)

✅ List<T> — полная поддержка (чтение и запись)

list[0], list[^1], list[1..4] (возвращает массив, не List)

✅ Span<T> и ReadOnlySpan<T> — полная поддержка

Используются в высокопроизводительном коде

⚠️ Dictionary<TKey, TValue> и HashSet<T> — не поддерживают

У словарей и множеств нет порядка элементов, поэтому доступ по числовому индексу не имеет смысла.

🧪

Проверь себя — мини-тест

Ответь на вопросы, чтобы убедиться, что ты усвоил тему:

Вопрос 1

Дан массив int[] a = { 5, 10, 15, 20 }. Чему равен a[2]?

Ответ: 15 (индексы: 0→5, 1→10, 2→15)

Вопрос 2

Дан массив int[] a = { 1, 2, 3, 4, 5, 6 }. Чему равен a[^2]?

Ответ: 5 (^1 → 6, ^2 → 5)

Вопрос 3

Дан массив int[] a = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }. Какие элементы содержит a[3..7]?

Ответ: { 3, 4, 5, 6 } (индексы 3,4,5,6; индекс 7 не входит)

Вопрос 4

Дан массив int[] a = { 10, 20, 30, 40, 50, 60, 70 }. Чему равен a[^3..]?

Ответ: { 50, 60, 70 } (последние 3 элемента)

Вопрос 5

Какое исключение выбросится при a[10] для массива из 5 элементов?

Ответ: IndexOutOfRangeException

📌

Что важно запомнить

1️⃣
Индексация начинается с 0. Первый элемент имеет индекс 0, а не 1. Последний — длина − 1.
2️⃣
.Length для массивов и строк, .Count для списков. Всегда используй коллекция[коллекция.Length - 1] для последнего элемента (до C# 8).
3️⃣
Оператор ^ — отсчёт с конца. ^1 — последний, ^2 — предпоследний. Доступен с C# 8.0.
4️⃣
Диапазоны .. — срез коллекции. arr[1..4] — элементы с 1 по 3 (4 не входит). arr[..3] — первые 3. arr[2..] — со 2 до конца. arr[..] — копия всего массива. Доступны с C# 8.0.
5️⃣
IndexOutOfRangeException — главная опасность. Всегда проверяй, что индекс в пределах длины коллекции. Срезы и ^ безопаснее — они сами вычисляют границы.
6️⃣
Строки, массивы, списки поддерживают индексы. Dictionary и HashSet — нет (у них нет порядка элементов).

Тест: 5.0: Индексы

5 вопросов

Индексация

Premium