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], ви вже використовуєте індексатор.
А найголовніше — тепер ви можете писати свої класи, які працюють так само гнучко й лаконічно. А отже, ви на крок ближче до написання професійного й читабельного коду.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ