1. Вступ
Уявіть: у нас є улюблений клас DogShelter, який зберігає колекцію собак. На попередніх лекціях ми вже додали індексатор, щоб отримувати собаку за її номером у притулку: Dog firstDog = myShelter[0];. Це чудово!
Але що, якщо користувачі хочуть отримувати собаку не за номером, а, скажімо, за кличкою? Або за породою? Або навіть за комбінацією ознак? Звісно, ми могли б додати методи на кшталт GetDogByName("Buddy") чи GetDogByBreedAndAge("Labrador", 5). І це цілком нормальний підхід.
Іноді ж хочеться, щоб доступ був більш «масивоподібним» і інтуїтивним. Щоб можна було написати: Dog buddy = myShelter["Buddy"]; або Dog oldLab = myShelter["Labrador", 8];.
Якщо DogShelter — це наш власний клас, ми можемо просто додати нові індексатори всередину нього. Але що, як DogShelter — це клас зі сторонньої бібліотеки, яку ми не можемо змінювати? Або, можливо, ви хочете додати дуже специфічний спосіб доступу, який не має «захаращувати» основний клас?
Саме тут на сцену виходять індексатори розширення (Extension Indexers)!
2. «Квадратні дужки» ззовні
Пам’ятаєте, як ми на минулій лекції додали властивість розширення DisplayName для Dog? З індексаторами все дуже схоже!
Індексатор розширення — це статичний індексатор, оголошений у статичному класі, який дозволяє вам використовувати синтаксис obj[індекс] для об’єктів наявних типів, навіть якщо ці типи спочатку не мали такого індексатора, або якщо ви хочете додати індексатор з іншим типом параметра.
Це ніби ви купили холодильник, а потім придумали, як зробити так, щоб, стукнувши по ньому в певному місці, він видавав пляшку коли. Холодильник залишився тим самим, але функціональність додалася «ззовні»!
Синтаксис індексатора розширення
public static class MyExtensionClass
{
extension(ObjectType екземпляр)
{
public static ReturnType this[ТипІндексу index ]
{
get
{
// Логіка читання, використовуючи екземпляр і index
return ...;
}
set
{
// Використовуючи екземпляр, index і ключове слово 'value'
// 'value' - це нове значення
}
}
}
}
Зверніть увагу на this ТипРозширюваногоОб’єкта екземпляр. Цей синтаксис повністю аналогічний до того, що ми бачили у методах і властивостях розширення. екземпляр — це назва об’єкта, який ми розширюємо, всередині наших аксесорів get і set.
3. Як оголосити індексатор розширення (і не зламати голову)?
Синтаксис подібний до Extension Properties, які ми обговорювали на минулій лекції, тільки з параметрами індексу. Ось мінімальний приклад:
public static class DogShelterExtensions
{
extension(DogShelter shelter)
{
public static Dog this[string name]
{
get
{
foreach (var dog in shelter)
{
if (dog.Name == name)
return dog;
}
return null;
}
}
}
}
Знайомі елементи:
- this перед першим параметром — обов’язкова ознака члена розширення (розширюваний об’єкт).
- Після імені класу йде список параметрів, які використовуватимуться всередині квадратних дужок.
Практика: розширюємо DogShelter індексатором за кличкою
Давайте модифікуємо наш навчальний проєкт. Уявіть, що у нас є притулок для собак, а кожна собака унікальна за кличкою:
Клас DogShelter (бібліотека/чужий код)
public class Dog
{
public string Name { get; set; }
public int Age { get; set; }
}
public class DogShelter : IEnumerable<Dog>
{
private List<Dog> dogs = new List<Dog>();
public void AddDog(Dog dog) => dogs.Add(dog);
// Старий індексатор за номером
public Dog this[int index]
{
get => dogs[index];
set => dogs[index] = value;
}
public IEnumerator<Dog> GetEnumerator() => dogs.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Хочемо: shelter["Буся"]
Раніше — тільки через метод:
// До C# 14:
public static Dog? FindByName(this DogShelter shelter, string name) { ... }
Тепер — через індексатор розширення:
public static class DogShelterExtensions
{
extension(DogShelter shelter)
{
public static Dog? this[string name]
{
get
{
foreach (var dog in shelter)
if (dog.Name == name)
return dog;
return null;
}
set
{
for (int i = 0; i < shelter.Count; i++)
{
if (shelter[i].Name == name)
{
shelter[i] = value!;
return;
}
}
throw new ArgumentException("Dog not found");
}
}
}
}
Тепер наш основний код виглядає значно лаконічніше:
var shelter = new DogShelter();
shelter.AddDog(new Dog { Name = "Буся", Age = 3 });
shelter.AddDog(new Dog { Name = "Тузик", Age = 5 });
// Використовуємо індексатор розширення!
Dog busya = shelter["Буся"]!;
Console.WriteLine(busya.Age);
shelter["Буся"] = new Dog { Name = "Буся", Age = 4 };
Візуалізація: що відбувається?
| Операція | Як працювало раніше | Із індексатором розширення |
|---|---|---|
| Пошук за кличкою | shelter.FindByName("X") | shelter["X"] |
| Зміна собаки за кличкою | shelter.UpdateName("X", ..) | shelter["X"] = ... |
4. Тонкощі та особливості індексаторів розширення
Компілятор і область видимості
- Індексатор розширення має бути оголошений у статичному публічному класі (як і звичайні методи розширення).
- Не забудьте додати потрібний using. Якщо його немає, компілятор мовчатиме, а код не компілюється.
- Якщо в базовому класі вже є такий індексатор — розширити його не можна (сигнатури мають відрізнятися).
Реалізація set-аксесора
Можна оголосити лише get (тоді індексатор буде тільки для читання). Або додати й set (приклад вище) — тоді через індексатор можна і читати, і записувати.
Передача за значенням і посиланням
Індексатор розширення працює з екземпляром об’єкта, до якого ви додаєте розширення (this перед першим параметром). Якщо об’єкт — посилальний тип, ви змінюєте його стан.
Кілька індексаторів у одному класі
Жодних проблем — можете оголосити кілька індексаторів розширення з різними наборами параметрів! Наприклад, шукати за віком: shelter[5] (старий), shelter["Буся"] (новий), shelter[age: 3] (ще один, якщо захочете).
Приклад: додаємо два індексатори до DogShelter
public static class DogShelterExtensions
{
extension(DogShelter shelter)
{
// За кличкою
public static Dog? this[string name]
{
get => shelter.FirstOrDefault(d => d.Name == name);
set
{
for (int i = 0; i < shelter.Count; i++)
if (shelter[i].Name == name)
shelter[i] = value!;
}
}
// За віком — поверне першу-ліпшу собаку такого віку
public static Dog? this[int age]
{
get => shelter.FirstOrDefault(d => d.Age == age);
}
}
}
Тепер можна писати:
var youngDog = shelter[1]; // За віком
var tony = shelter["Тони"]; // За кличкою
shelter["Тузик"] = new Dog { Name = "Тузик", Age = 9 };
Реальні сценарії
- Зовнішні бібліотеки: Ви хочете додати додаткові способи індексування до стороннього класу, не чіпаючи вихідники. Наприклад, працювати з колекцією замовлень, знаходячи їх за номером, датою, статусом тощо — без дублювання обгорткових методів.
- Шаблон «Адаптер»: Ви перетворюєте стару колекцію з «простим» API на сучасний, лаконічний, більш «C#‑подібний», не ламаючи зворотну сумісність.
- Міграція застарілого коду: Додаєте до вже написаних типів нові можливості, не торкаючись наявного коду й тестів.
- Зручність для тестування: Можете навішувати тимчасові індексатори для власних потреб (наприклад, пошук за унікальними для тесту ознаками), не ризикуючи захаращити основний клас.
5. Типові помилки та пастки при роботі з індексаторами розширення
Якщо в базовому класі вже є індексатор із точно такою самою сигнатурою, індексатор розширення не викликатиметься — пріоритет має базовий індексатор.
Індексатор розширення — це той самий член розширення, і без потрібного using (підключення простору імен) розширення не буде видно.
Ще одна типова помилка — повертати null, не попередивши користувача. Якщо хтось помилково звернеться до неіснуючого елемента, а індексатор розширення поверне null, це може призвести до NullReferenceException в іншій частині коду. Гарний тон — продумати, що має робити ваша реалізація: кидати виняток, повертати спеціальний об’єкт‑заглушку чи просто повертати null.
Якщо у вас кілька індексаторів розширення, стежте за їхньою унікальністю за типами та кількістю параметрів. Наприклад, не можна оголосити два індексатори з однаковою сигнатурою — компілятор повідомить про помилку.
Індексатори розширення працюють лише з екземплярами об’єктів, а не зі статичними типами.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ