1. Эволюция боли
Вы наверняка такое встречали: нужно не просто пройтись по коллекции, а ещё знать номер каждого элемента. Чтобы, скажем, вывести нумерованный список, поменять элементы с ненулевыми индексами или сделать что-то с каждым третьим. В обычном for — легко:
// Всё привычно, классика C#:
for (int i = 0; i < list.Count; i++)
{
Console.WriteLine($"{i}: {list[i]}");
}
Но если вы начинали писать на LINQ, то вдруг… вы теряли индекс! Все эти красивые .Where, .Select, .OrderBy внезапно не дают вам номер элемента — только сам элемент. Конечно же, хотелось бы делать так:
list.SelectWithIndex((item, index) => ...);
Но стандартного метода вроде SelectWithIndex никогда не было. Хотя с помощью перегрузки Select и можно получить индекс, но только… когда хочется использовать индекс без трансформации или без проекции, приходилось городить велосипед с дополнительными .Select, а это делало красивый LINQ-код менее понятным.
Как выживали до .NET 9
В былые времена у программистов C# были свои хакерские трюки:
var result = list.Select((item, index) => new { item, index });
А ещё — комбинации с другими LINQ-методами, но всегда как-то не очень "встроено", и не совсем то, чего хотелось бы для красоты.
2. Новый метод LINQ: Index — что это и зачем?
Официальное описание
В .NET 9 команда разработчиков учла стенания миллионов программистов (ну ладно, десятков тысяч в Twitter, что тоже немало) — и добавила в LINQ новый метод-расширение — Index. На официальной странице документации .NET 9 можно найти следующее:
Index() добавляет к каждому элементу последовательности его порядковый номер, начиная с нуля, и возвращает пары (значение, индекс), без необходимости явно создавать анонимные объекты или писать .Select((item, idx) => new { item, idx }).
Это мощно — теперь Index просто возвращает вам для каждого элемента пару: сам элемент и его индекс.
Сигнатура метода
public static IEnumerable<(T Element, int Index)> Index<T>(this IEnumerable<T> source);
Перевод на человеческий: Для каждого элемента коллекции ты получаешь специальный "кортеж" — Element и его Index. Всё. Больше тебе не нужно городить анонимные типы.
3. Примеры использования метода Index
Очень простой пример
Давайте возьмём список любимых фруктов.
var fruits = new List<string> { "Яблоко", "Банан", "Апельсин", "Киви" };
foreach (var (fruit, idx) in fruits.Index())
{
Console.WriteLine($"{idx}: {fruit}");
}
Вывод:
0: Яблоко
1: Банан
2: Апельсин
3: Киви
Вот так просто! Теперь вы можете элегантно получить пару "индекс-значение" и использовать их внутри любого LINQ-запроса.
Интеграция с остальными LINQ-методами
Index — это полноценный участник LINQ-семьи! Его можно спокойно вставлять в "цепочки".
Пример 1: Отфильтровать нечётные элементы по индексу
var numbers = Enumerable.Range(10, 10); // 10, 11, ... 19
var oddIndexes = numbers.Index()
.Where(pair => pair.Index % 2 == 1)
.Select(pair => pair.Element);
Console.WriteLine(string.Join(", ", oddIndexes));
Вывод:
11, 13, 15, 17, 19
Пример 2: Модифицировать элементы с чётными индексами
var users = new List<string> { "Анна", "Игорь", "Катя", "Денис" };
var modified = users.Index()
.Select(pair => pair.Index % 2 == 0 ? pair.Element.ToUpper() : pair.Element.ToLower());
foreach (var name in modified)
Console.WriteLine(name);
Вывод:
АННА
игорь
КАТЯ
денис
Пример 3: Объединение с другими коллекциями по индексу (zip-like)
var ids = new[] { 101, 102, 103 };
var names = new[] { "Alice", "Bob", "Charlie" };
var merged = names.Index()
.Join(ids.Index(),
namePair => namePair.Index,
idPair => idPair.Index,
(namePair, idPair) => (idPair.Element, namePair.Element));
foreach (var (id, name) in merged)
Console.WriteLine($"{id}: {name}");
Вывод:
101: Alice
102: Bob
103: Charlie
4. Index в реальном приложении
Давайте добавим в наше "вечное" учебное приложение новый функционал: выведем список всех студентов из нашей коллекции с их порядковым номером (например, чтобы пользователь мог выбрать нужного студента по номеру).
Пример: Выводим список студентов с номерами
Допустим, у нас уже есть класс Student из предыдущих примеров:
public class Student
{
public string Name { get; set; }
public int Grade { get; set; }
}
Создадим небольшой список студентов:
var students = new List<Student>
{
new Student { Name = "Даша", Grade = 5 },
new Student { Name = "Петя", Grade = 3 },
new Student { Name = "Вова", Grade = 4 },
new Student { Name = "Оля", Grade = 5 }
};
Теперь, используя Index, красиво выводим всех с номерами:
foreach (var (student, idx) in students.Index())
{
Console.WriteLine($"{idx + 1}. {student.Name} — Оценка: {student.Grade}");
}
Здесь
idx + 1 — чтобы индексация шла привычно с единицы, а не с нуля.
Результат:
1. Даша — Оценка: 5
2. Петя — Оценка: 3
3. Вова — Оценка: 4
4. Оля — Оценка: 5
Практическая польза: Теперь, если пользователь захочет выбрать студента по номеру — всё готово! Код стал проще, никаких "ручных" счетчиков, максимум читаемости — минимум багов.
5. Сравнение: чем Index круче старых подходов?
До .NET 9: старый стиль
Раньше для получения вместе элемента и его индекса приходилось использовать перегрузку .Select((item, index) => ...) и конструировать анонимные типы:
var withIndexes = students.Select((student, index) => new { student, index });
Чтобы получить нужные поля, постоянно приходилось писать .student, .index — да ещё и тип анонимный, никакого красивого имёнованного кортежа.
С .NET 9: стиль XXI века
Теперь — не нужно заботиться о полях, анонимных типах. Всё прозрачно:
foreach (var (student, idx) in students.Index())
{
// работает из коробки, интуитивно, просто, красиво
}
Код стал чище. Меньше кода, меньше ошибок. Всё идеально подходит для больших цепочек LINQ, где не хочется думать о дополнительных перегрузках.
6. Тонкости и особенности использования
Область применения
Index работает с любым объектом, реализующим интерфейс IEnumerable<T>. То есть — со всеми обычными коллекциями, массивами, результатами других LINQ-запросов.
Какой тип возвращает Index?
Он возвращает перечисление кортежей, где первый элемент — сам объект (обычно называют Element), второй — Index (тип int). Благодаря современному синтаксису кортежей в C# мы можем прямо в цикле писать foreach (var (element, index) in ...) и получать оба значения в нужные переменные сразу.
Можно ли использовать с Query Syntax?
Нет, Index — это метод-расширение, query syntax (SQL-подобный LINQ) его не поддерживает напрямую. То есть вот так не выйдет:
// Не работает!
var query = from s in students.Index() select ...;
Если хочется совместить, просто заключите нужный метод в скобки и работайте с результатом как с обычной коллекцией:
var query = from pair in students.Index()
where pair.Index > 1
select pair.Element;
Индексация: всегда с нуля
Index всегда начинает отсчёт с нуля, как и большинство подобных штук в программировании C#. Если вам нужно начинать с единицы — просто добавьте 1 в нужном месте.
8. Ошибки и особенности — на что обратить внимание
Многие студенты на первых порах путаются между Index и перегрузкой .Select((item, index) => ...). Ошибки чаще всего такие:
— Пытаются использовать Index в query syntax: "Почему не работает?" — а он работает только как метод-расширение.
— Ожидают, что индекс начнётся с 1, а он, разумеется, начинается с 0.
— Думают, что Index изменяет исходную коллекцию — но, как и все LINQ-методы, он возвращает новую последовательность, не модифицируя исходную (immutable-подход).
Ещё одна интересная особенность: если коллекция "ленивая" (напоминаю: LINQ-запросы по умолчанию ленивы), метод Index тоже будет вычислять индексы по мере доступа к элементам, а не заранее. Это отлично подходит для работы с большими или даже бесконечными последовательностями — индексация всегда будет корректной и не "взорвёт" вашу оперативку.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ