Урок 5.0 — Индексы
Нумерация с нуля, доступ по индексу, диапазоны и оператор ^ для элементов с конца.
Представь себе шеренгу людей. Если ты скажешь «третий слева» — это индекс. Только в C#, как и в большинстве языков программирования, счёт начинается не с единицы, а с нуля. Первый элемент — это элемент под номером 0. Сегодня мы научимся уверенно работать с индексами, а также освоим современный синтаксис диапазонов, который появился в C# 8.0.
Что такое индекс и почему счёт с нуля?
Индекс — это номер позиции элемента внутри коллекции. Коллекция — это набор элементов, расположенных друг за другом: массив чисел, строка текста, список покупателей. Каждый элемент коллекции имеет свой номер — индекс.
Почему счёт начинается с 0, а не с 1? Дело в том, что в памяти компьютера массив — это просто непрерывный блок ячеек. Адрес первого элемента совпадает с адресом начала массива (смещение 0). Второй элемент находится со смещением 1, третий — со смещением 2, и так далее. Нулевой индекс — это просто смещение от начала. То есть для компьютера индекс — это не «номер по порядку», а «сколько шагов нужно сделать от начала, чтобы добраться до элемента».
Поэтому:
- Первый элемент — индекс 0
- Второй элемент — индекс 1
- Третий элемент — индекс 2
- Последний элемент — индекс длина − 1
Эту особенность нужно просто запомнить. Она есть в C, C++, Java, JavaScript, Python, Go, Rust и большинстве других языков. Новички часто путаются: «Почему у меня ошибка, ведь я хочу третий элемент, а пишу индекс 2?» — потому что счёт с нуля.
Массив: [10] [20] [30] [40] [50]
Индекс: 0 1 2 3 4
↑ первый ↑ последний
элемент (длина - 1)
N - 1. Для массива из 5 элементов последний индекс — 4. Для массива из 100 элементов — 99.
Доступ к элементу по индексу
Чтобы получить элемент коллекции, нужно после имени переменной написать индекс в квадратных скобках []:
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
Тот же синтаксис работает для строк и списков:
string word = "Привет";
char firstChar = word[0]; // 'П'
char fourthChar = word[3]; // 'в'
List<string> fruits = new List<string> { "яблоко", "банан", "вишня" };
string secondFruit = fruits[1]; // "банан"
Ты можешь не только читать, но и записывать значение по индексу (если коллекция изменяема):
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:
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 вызовет ошибку, так же как обращение по индексу, равному длине коллекции.
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]. Просто читается гораздо легче.
numbers[^1] ⇔ numbers[numbers.Length - 1]
numbers[^2] ⇔ numbers[numbers.Length - 2]
numbers[^3] ⇔ numbers[numbers.Length - 3]
Оператор ^ работает с массивами, строками, списками — любыми коллекциями, которые поддерживают индексацию:
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, конец не включается.
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 элемента
Диапазоны можно комбинировать с оператором ^:
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 }
Опускать начало или конец — тоже валидно. Если начало опущено, это означает «от самого начала». Если конец опущен — «до самого конца»:
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-го до конца
Срезы работают не только с массивами, но и со строками:
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]; // "Александр Сергеевич Пушки"
Важно: срезы для массивов создают новый массив с копией элементов. Изменение среза не влияет на исходный массив:
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):
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):
Типы Index и Range
За синтаксисом ^1 и 1..4 стоят специальные типы: System.Index и System.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 }
Это удобно, когда тебе нужно передать индекс или диапазон как параметр в метод:
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.коллекция[коллекция.Length - 1] для последнего элемента (до C# 8).^1 — последний, ^2 — предпоследний. Доступен с C# 8.0.arr[1..4] — элементы с 1 по 3 (4 не входит). arr[..3] — первые 3. arr[2..] — со 2 до конца. arr[..] — копия всего массива. Доступны с C# 8.0.Тест: 5.0: Индексы
5 вопросов