JavaRush /Курси /C# SELF /Новий метод LINQ: Index

Новий метод LINQ: Index

C# SELF
Рівень 32 , Лекція 3
Відкрита

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 теж обчислюватиме індекси у міру доступу до елементів, а не наперед. Це ідеально підходить для роботи з великими або навіть нескінченними послідовностями — індексація завжди буде коректною й не перевантажить оперативну пам’ять.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ