JavaRush /Курси /C# SELF /Знайомство з індексаторами

Знайомство з індексаторами

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

1. Кілька слів про колекції

Досі ми працювали з масивами — це найпростіший спосіб зберігати кілька елементів одного типу. Але у C# є багато зручних і потужних колекцій, які розв’язують різні завдання.

Колекція — це об’єкт, що зберігає групу інших об’єктів. На відміну від масивів, колекції часто динамічно змінюють розмір, надають зручні методи для роботи з даними й оптимізовані під різні сценарії використання.

Основні типи колекцій (коротко):

List<T> — динамічний масив, який може зростати й зменшуватися:

List<string> names = new List<string>();
names.Add("Олекс");
names.Add("Марія");
Console.WriteLine(names[0]); // Олекс
Console.WriteLine(names.Count); // 2

Dictionary<TKey, TValue> — колекція пар «ключ-значення», де кожному ключу відповідає одне значення:

Dictionary<string, int> ages = new Dictionary<string, int>();
ages["Олекс"] = 25;
ages["Марія"] = 30;
Console.WriteLine(ages["Олекс"]); // 25

Зверніть увагу: у прикладах вище ми використовуємо квадратні дужки [] для доступу до елементів колекцій — так само, як із масивами! Це можливо завдяки індексаторам, про які ми сьогодні й поговоримо.

Це лише перше знайомство з колекціями — докладно їхні можливості, відмінності та застосування ми розглянемо у наступних лекціях. Зараз важливо зрозуміти, що колекції дають змогу звертатися до своїх елементів через квадратні дужки — і це не магія, а спеціальний механізм мови.

2. Індексатори

Звичайні об’єкти C# працюють через властивості та методи. Але що робити, якщо ваш об’єкт — це своєрідна мініколекція? Уявімо:

  • Ви пишете клас Week, який має повертати назву дня тижня за номером: week[0] → "Понеділок".
  • Або клас Library, де можна звертатися до книги за її номером: library[3] → "У бік Свана".

Зручно, чи не так? Було б дивно писати library.GetBookByIndex(3) щоразу — хочеться звертатися до свого об’єкта, як до масиву!

Саме тут і з’являються індексатори.

Індексатор — це особливий член класу, який дає змогу використовувати об’єкти цього класу з синтаксисом квадратних дужок, як масиви: obj[0], obj["ключ"] тощо.

Зовні індексатор нагадує властивість, але замість імені приймає параметри в квадратних дужках. Це як писати .Name, тільки тут — [i].

3. Проста колекція з індексатором

Зберімо мініклас, який зберігатиме улюблені кольори. Для зберігання використаємо масив рядків. Без індексатора довелося б робити метод на кшталт GetColor(int i). Але з індексатором:


using System;

public class FavoriteColors
{
   // Приватне поле для зберігання кольорів
   private string[] colors = new string[5];

   // Індексатор:
   public string this[int index]
   {
       get
       {
           // Контроль меж масиву (капсуляція в дії!)
           if (index < 0 || index >= colors.Length)
               throw new IndexOutOfRangeException("Некоректний індекс кольору!");

           return colors[index];
       }
       set
       {
           if (index < 0 || index >= colors.Length)
               throw new IndexOutOfRangeException("Некоректний індекс кольору!");

           colors[index] = value ?? throw new ArgumentNullException(nameof(value));
       }
   }
}

class Program
{
   static void Main()
   {
       FavoriteColors favorites = new FavoriteColors();

       favorites[0] = "Зелений";
       favorites[1] = "Синій";
       favorites[2] = "Червоний";
       favorites[10] = "Фіолетовий"; // Викидає виняток!

       Console.WriteLine(favorites[1]); // Синій
   }
}

Що тут відбувається?

  • Ми створюємо приватний масив, щоб ніхто ззовні не міг із ним втручатися безпосередньо.
  • Індексатор визначено як public string this[int index], де this — ключове слово, що вказує на те, що індексатор належить об’єкту.
  • У get і set ми перевіряємо діапазон і не дозволяємо виходити за межі або записувати null.
  • У результаті ми можемо звертатися favorites[0], як до звичайного масиву.

4. Синтаксис індексатора: деталі

Синтаксис нагадує властивість, але замість імені (наприклад, Age) вказують ключове слово this з параметрами:


// Сигнатура індексатора (загальний шаблон)
[модифікатор] ТипРезультату this[ТипІндексу ім'яІндексу]
{
   get { ... }
   set { ... }
}

Приклад: Класичний

public class MyCollection
{
   private int[] data = new int[10];

   // Індексатор для читання і запису
   public int this[int index]
   {
       get { return data[index]; }
       set { data[index] = value; }
   }
}

Індексатори — не лише з типом int

Головна особливість: індексатор не обов’язково приймає лише int. Ви можете задати будь-який тип (головне, щоб звернення за ключем мало сенс):

public string this[string colorName]
{
   get { /* ... */ }
   set { /* ... */ }
}

Наприклад, у класі телефонної книги логічно шукати за ім’ям:

public class PhoneBook
{
   private Dictionary<string, int> entries = new Dictionary<string, int>();

   public int this[string name]
   {
       get
       {
           if (entries.ContainsKey(name))
               return entries[name];
           return null;
       }
       set
       {
           entries[name] = value;
       }
   }
}

Про колекції та як працює Dictionary<string, string> я розповім у майбутніх лекціях.

5. Практичний приклад: Лічильник слів у тексті

Продовжімо розвивати наш застосунок. Припустімо, тепер у нас є клас, який рахує, скільки разів кожне слово зустрілося в тексті. Зручно, коли користувач може звертатися до об’єкта через квадратні дужки, щоб отримати кількість за словом:


using System.Collections.Generic;

public class WordCounter
{
   private Dictionary<string, int> counter = new Dictionary<string, int>();

   // Індексатор за рядком (словом)
   public int this[string word]
   {
       get
       {
           if (counter.ContainsKey(word))
               return counter[word];
           return 0; // Якщо такого слова немає, повертаємо 0.
       }
       set
       {
           counter[word] = value;
       }
   }

   // Метод для підрахунку слів із рядка
   public void AddWords(string text)
   {
       foreach (var word in text.Split(' ', System.StringSplitOptions.RemoveEmptyEntries))
       {
           if (counter.ContainsKey(word))
               counter[word]++;
           else
               counter[word] = 1;
       }
   }
}

// У Main:
var wc = new WordCounter();
wc.AddWords("мама мила раму мила мама тато");
Console.WriteLine($"'мама' зустрічається {wc["мама"]} раз(ів)");
Console.WriteLine($"'рама' зустрічається {wc["рама"]} раз(ів)");
Console.WriteLine($"'кіт' зустрічається {wc["кіт"]} раз(ів)"); // 0

Для чого це потрібно на практиці? Такий підхід часто застосовують для реалізації власних колекцій, бібліотек пам’яті, відображень (словники та індекси) і навіть DSL (спеціалізованих мов усередині C#).

6. Обмеження та нюанси

Індексатори — потужний інструмент, але є кілька правил і підводних каменів.

Назви в індексатора немає

На відміну від властивості, в індексатора немає імені, лише сигнатура виду this[тип параметра]. Якщо ви напишете public int MyIndexer[int i] — компілятор здивується. Використовуйте лише this.

Статичних індексаторів не буває

Індексатори завжди створюються для екземпляра класу, а не для статичних членів. Тобто не можна оголосити static int this[int i], оскільки this завжди вказує на конкретний екземпляр об’єкта.

Перевантаження за типом або кількістю параметрів

Ви можете створити кілька індексаторів в одному класі, якщо їхні параметри різняться за типом або кількістю. Наприклад:

public string this[int i] { get { ... } set { ... } }
public string this[string key] { get { ... } set { ... } }

Це легально, компілятор не заплутається і нагадає вам, якщо параметри перетинаються.

Працюють лише з аксесорами

Не можна оголосити індексатор без аксесорів get або set. Якщо потрібен доступ лише для читання — приберіть set, лише для запису — get. Зазвичай використовують обидва.

7. Практична користь і навіщо це потрібно

  • Індексатори активно застосовуються в колекціях даних. Багато класів .NET їх підтримують: наприклад, List<T>, Dictionary<TKey,TValue>. Коли ви пишете list[2], ви використовуєте саме індексатор!
  • Індексатори дають змогу приховати внутрішню реалізацію (інкапсуляція!), але надати зручний і звичний інтерфейс. Користувач вашого класу не замислюється, як ви зберігаєте дані, — він просто використовує звичні [index].
  • Ваш код стає лаконічним і інтуїтивно зрозумілим — це особливо цінують на співбесідах і ваші майбутні колеги.

Властивості та індексатори: порівняння

Властивість Індексатор
Імʼя Так (наприклад, Name) Ні (замість імені — this[параметр])
Доступ За іменем За індексом (або іншим ключем)
Статика Може бути static Лише для екземпляра
Кількість у класі Так, будь-яка кількість Так, але з різними сигнатурами параметрів
Застосування Зберігання й доступ до даних Мініколекції, асоціативні дані

8. Типові помилки та поради

Помилка № 1: спроба оголосити індексатор як static.
Так не можна — this[...] працює лише з об’єктом.

Помилка № 2: пропущено перевірку індексу.
Якщо не перевірити межі масиву в get або set, програма може аварійно завершитися.

Помилка № 3: плутанина з типами параметрів.
Якщо створити два індексатори з однаковими параметрами, компілятор видасть помилку.

Помилка № 4: пропущено реалізацію get або set.
Якщо потрібен доступ і на читання, і на запис — обидва мають бути реалізовані.

Порада: якщо ваш клас обгортає масив, просто делегуйте звернення до масиву через індексатор. Це пришвидшить роботу й зробить код інтуїтивним.

9. Навіщо все це знати

Індексатори роблять інтерфейс об’єкта простим, зрозумілим і зручним. Вони дають змогу приховати внутрішню реалізацію, але залишити розробнику зручний спосіб доступу до даних.

Саме так реалізовані вбудовані колекції: string, List<T>, Dictionary<TKey, TValue>, Span<T> і багато інших. Коли ви пишете array[2] або text[0], ви вже використовуєте індексатор.

А найголовніше — тепер ви можете писати свої класи, які працюють так само гнучко й лаконічно. А отже, ви на крок ближче до написання професійного й читабельного коду.

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