Full Stack Blog – C# vs Java. Indexers

23 April 2023

C# vs Java. Indexers

Indexers (C# Programming Guide)

C#

Indexers - это механизм в C#, позволяющий работать с объектом класса или структурой как с массивом, обращаясь к его элементам по индексам. Например так: plantPoolInstance[elementIndex].

Для того чтобы использовать indexer нужно определить один метод как показано ниже

    ...
    // определение indexer-a
    public Plant this[int i]
    {
        // getter, обрабатывающий запись в массив
        // plantPool - переменная в которой хранятся данные. Например массив растений. 
        //  эта переменная должны быть объявлена и инициализирована в классе или доступна из него.
        get { return plantPool[i]; }
        // setter, обрабатываюзий чтение из массива
        set { plantPool[i] = value; }
    }
    ...

this указывает на то, что это indexer.

[int i] - это параметры индексатора.

Далее, чуть более подробный пример:

internal class Program
{
    class Plant
    {
        public string Name { get; init; }
    }

    class PlantPool
    {
        private Plant[] plantPool { get; init; }
        public PlantPool(int Capacity) { this.plantPool = new Plant[Capacity]; }

        // тут определяем indexer с одним параметрам типа int
        public Plant this[int i]
        {            
            get { return plantPool[i]; }
            set { plantPool[i] = value; }
        }
    }

    static void Main(string[] args)
    {
        int Capacity = 2;
        var plantPool = new PlantPool(Capacity);
        plantPool[0] = new Plant() { Name = "Bétula péndula" };
        plantPool[1] = new Plant() { Name = "Betula papyrifera" };

        for (int i=0; i < Capacity; i++)
        {
            Console.WriteLine(plantPool[i].Name);
        }
    }
}

Идея довольно проста, мы определяем две функции для организации работы в решими массива и добавляем немного магии this[int i] чтобы компилятор понял что мы хотим.

Индексатор не может быть статических членом класса.

Аргументы indexer-a

Мы не ограничены целими числами в параметрах индекса, как в примере выше, также нет ограничений на количеством агрументов.

Немного старнный пример с вариантоми использоватья аругметов

...
    class PlantPool
    {
        private Plant[,] plantPool { get; init; }
        public PlantPool(int Capacity) { this.plantPool = new Plant[Capacity,Capacity]; }

        // - один или более пареметров
        // - разные типы параметров
        public Plant this[int i, string j]
        {
            get { return plantPool[i, int.Parse(j)]; }
            set { plantPool[i, int.Parse(j)] = value; }
        }
    }

    static void Main(string[] args)
    {
        int Capacity = 2;
        var plantPool = new PlantPool(Capacity);
        plantPool[0, "0"] = new Plant() { Name = "Bétula péndula" };
        plantPool[1, "1"] = new Plant() { Name = "Betula papyrifera" };

        for (int i=0; i < Capacity; i++)
        {
            Console.WriteLine(plantPool[i, i.ToString()].Name);
        }
    }
...

В целом, возможность использовать разные парметры предоставляет довольно большую гибкость при написании кода.

Перегрузка indexer-a

Допускается перегрузка indexer-a, что дает возможность писать такой код:

...
    // indexer for int, string
    public Plant this[int i, string j]
    {
        get { return plantPool[i, int.Parse(j)]; }
        set { plantPool[i, int.Parse(j)] = value; }
    }

    // indexer for int, int
    public Plant this[int i, int j]
    {
        get { return plantPool[i, j]; }
        set { plantPool[i, j] = value; }
    }
...
    // and then
    plantPool[0, 0] = new Plant() { Name = "Bétula péndula" };
    plantPool[1, 1] = new Plant() { Name = "Betula papyrifera" };

    for (int i=0; i < Capacity; i++)
    {
        Console.WriteLine(plantPool[i, i]);
    }

Indexer в интерфейсах

Indexer может быть объявлен в интерфейсе. Методы доступа остаются без реализации.

interface IPlantPool
{
    Plant this[int i, int j]
    {
        get;
        set;
    }
}

Реализация такого интерфейса:

class PlantPool : IPlantPool
{
    private Plant[,] plantPool { get; init; }
    public PlantPool(int Capacity) { this.plantPool = new Plant[Capacity,Capacity]; }

    public Plant this[int i, int j]
    {
        get { return plantPool[i, j]; }
        set { plantPool[i, j] = value; }
    }
}

Никто нам не мешает потребовать реализации только одного метода доступа. Например

interface IPlantPool
{
    Plant this[int i, int j]
    {
        get;
    }
}

а реализовать еще и set или ограничиться только тем что требует интерфейс.

Больше фич, поддерживаемых интерфейсами можно найти в C# vs Java. Интерфейсы в C#.

методы по умолчанию

Это возможно реализовать индексатор целиком в интерфейсе.

Приступая к следующеми примеру - стоит понимать что такое Интерфейсы в C#.

    interface IPlantPool
    {
        static int Capacity = 2;
        // мы не можем инициализировать non-static поля прям в интерфейсе
        // но со static полем все получилось
        static Plant[,] plantPool { get; set; } = new Plant[Capacity, Capacity];
        // нельзя использовать init, но можно set

        // методы по умолчанию могут реализовать indexer 
        Plant this[int i, int j]
        {
            get { return plantPool[i, j]; }
            set { plantPool[i, j] = value; }
        }
    }

    // Реализуем интерфейс
    class PlantPool : IPlantPool
    {
    }

    static void Main(string[] args)
    {
        // так как default методы будут доступны для интерфейса IPlantPool,
        // сразу приведем инстанс к типу интерфейса
        IPlantPool plantPool = new PlantPool();

        plantPool[0, 0] = new Plant() { Name = "Bétula péndula" };
        plantPool[1, 1] = new Plant() { Name = "Betula papyrifera" };

        // используем Capacity из интерфейса, чтобы узнать границы индекса
        for (int i=0; i < IPlantPool.Capacity; i++)
        {
            Console.WriteLine(plantPool[i, i]);
        }
    }


Java

В Java нет столько сахара 😉 возможности переопределять операторы, включая обращение по индексу.