JavaRush /Курсы /C# SELF /Extension Members: индексаторы

Extension Members: индексаторы

C# SELF
18 уровень , 4 лекция
Открыта

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' - это новое значение
            }
        }
    }        
}
Синтаксис расширяющего индексатора (Extension Indexer)

Обратите внимание на this ТипРасширяемогоОбъекта экземпляр. Этот синтаксис совершенно аналогичен тому, что мы видели в расширяющих методах и свойствах. экземпляр — это то, как мы будем называть объект, который расширяем, внутри наших аксессоров get и set.

3. Как объявить Extension Indexer (и не взорвать мозг)?

Синтаксис похож на 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;
            }
        }
    }        
}
Extension Indexer по кличке для DogShelter

Знакомые элементы:

  • this перед первым параметром — это требование к Extension Members (расширяемый объект).
  • После имени класса идёт список параметров, которые будут использоваться внутри квадратных скобок.
Это работает почти как обычные индексаторы, только вы не меняете исходный класс!

Практика: Расширяем 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) { ... }

Теперь — через Extension Indexer:


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 });

// Используем extension-индексатор!
Dog busya = shelter["Буся"]!;
Console.WriteLine(busya.Age);

shelter["Буся"] = new Dog { Name = "Буся", Age = 4 };

Визуализация: что происходит?

Операция Как работает раньше С Extension Indexer
Поиск по кличке shelter.FindByName("X") shelter["X"]
Изменение собаки по кличке shelter.UpdateName("X", ..) shelter["X"] = ...

4. Тонкости и особенности Extension Indexers

Компилятор и область видимости

  • Extension Indexer должен быть объявлен в статическом публичном классе (как и обычные extension methods).
  • Не забывайте подключать нужный using. Если забыли — компилятор молчит, а код не компилируется.
  • Если в базовом классе уже есть такой индексатор — расширить его нельзя (сигнатуры должны отличаться).

Реализация set-аксессора

Можно объявить только get (тогда индексатор будет только для чтения). Или добавить и set (пример выше) — тогда можно будет и читать, и писать через ваш индексатор.

Передача по значению и ссылке

Extension Indexer работает с экземпляром объекта, на который вы навешиваете расширение (this перед первым параметром). Если объект — ссылочный тип, то вы изменяете его состояние.

Несколько индексаторов в одном классе

Никаких проблем — можете объявить несколько extension-индексаторов с разными наборами параметров! Например, искать по возрасту: 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. Типовые ошибки и ловушки при работе с Extension Indexers

Если в базовом классе уже есть индексатор с точно такой же сигнатурой, то extension-индексатор не будет вызван — приоритет имеет базовый индексатор.

Extension-индексатор — это всё тот же extension member, и без нужного using (подключения пространства имён) расширение не будет видно.

Не менее частая ошибка — вернуть null, не предупредив пользователя. Если кто-то по ошибке обратится к несуществующему элементу, а extension-индексатор вернёт null, это может привести к NullReferenceException в другой части кода. Хороший тон — продумать, что ваша реализация должна делать: кидать исключение, возвращать специальный объект-заглушку или просто возвращать null.

Если у вас несколько extension-индексаторов, следите за их уникальностью по типам и количеству параметров. Например, нельзя создать два индексатора с одинаковой сигнатурой — компилятор выдаст ошибку.

Extension-индексаторы работают только с экземплярами объектов, а не со статическими типами.

2
Задача
C# SELF, 18 уровень, 4 лекция
Недоступна
Добавление расширяющего индексатора для работы со списком чисел
Добавление расширяющего индексатора для работы со списком чисел
1
Опрос
Знакомство с индексаторами, 18 уровень, 4 лекция
Недоступен
Знакомство с индексаторами
Индексаторы и Extension Members
Комментарии (3)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Ilya Уровень 31
10 января 2026
В общем я не нашел как сделать индексатор через Extension. Так как тут показано - микрософт говорит делать нельзя 😄 https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/extension-declarations?f1url=%3FappId%3Droslyn%26k%3Dk(CS9282)#errors-related-to-extension-block-declarations CS9282 зы. в саппорт написал
Ilya Уровень 31
5 декабря 2025
Лекция 4 вызывает больше вопросов, чем ответов. Даже готовый ответ в IDE с включенным Net10 и C# 14 не компилируется...
Yurii N Уровень 29
7 января 2026
Соглашусь. Почему б не разделить материл на синтаксис до C# 14 и после, чтоб каши не было. Был себе C# 12 стабильный и обкатанный, на нем бы и строили подачу, и где нужно добавить новое отдельным пунктом.