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
Доводилося вдаватися до обхідних прийомів:
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)
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 — це метод‑розширення; синтаксис запитів (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 теж обчислюватиме індекси у міру доступу до елементів, а не наперед. Це ідеально підходить для роботи з великими або навіть нескінченними послідовностями — індексація завжди буде коректною й не перевантажить оперативну пам’ять.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ