Индексаторы


Индексаторы позволяют индексировать объекты и обращаться к данным по индексу. Фактически с помощью индексаторов мы можем работать с объектами как с массивами. По форме они напоминают свойства со стандартными блоками get и set, которые возвращают и присваивают значение.

Формальное определение индексатора:

возвращаемый_тип this [Тип параметр1, ...]
{
get { ... }
set { ... }
}
В отличие от свойств индексатор не имеет названия. Вместо него указывается ключевое слово this, после которого в квадратных скобках идут параметры. Индексатор должен иметь как минимум один параметр.

Посмотрим на примере. Допустим, у нас есть класс Person, который представляет человека, и класс Company, который представляет некоторую компанию, где работают люди. Используем индексаторы для определения класса Company:

class Person
{
public string Name { get;}
public Person(string name) => Name=name;
}
class Company
{
Person[] personal;
public Company(Person[] people) => personal = people;
// индексатор
public Person this[int index]
{
get => personal[index];
set => personal[index] = value;
}
}
Для хранения персонала компании в классе определен массив personal, который состоит из объектов Person. Для доступа к этим объектам определен индексатор:

public Person this[int index]
Индексатор в принципе подобен стандартному свойству. Во-первых, для индексатора определяется тип в данном случае тип Person. Тип индексатора определяет, какие объекты будет получать и возвращать индексатор.

Во-вторых, для индексатора определен параметр int index, через который обращаемся к элементам внутри объекта Company.

Для возвращения объекта в индексаторе определен блок get:

get => personal[index];
Поскольку индексатор имеет тип Person, то в блоке get нам надо возвратить объект этого типа с помощью оператора return. Здесь мы можем определить разнообразную логику. В данном случае просто возвращаем объект из массива personal.

В блоке set, как и в обычном свойстве, получаем через параметр value переданный объект Person и сохраняем его в массив по индексу.

set => personal[index] = value;
После этого мы можем работать с объектом Company как с набором объектов Person:

var microsoft = new Company(new[]
{
new Person("Tom"), new Person("Bob"), new Person("Sam"), new Person("Alice")
});
// получаем объект из индексатора
Person firstPerson = microsoft[0];
Console.WriteLine(firstPerson.Name); // Tom
// переустанавливаем объект
microsoft[0] = new Person("Mike");
Console.WriteLine(microsoft[0].Name); // Mike
Стоит отметить, что если индексатору будет передан некорректный индекс, который отсутствует в массиве person, то мы получим исключение, как и в случае обращения напрямую к элементам массива. В этом случае можно предусмотреть какую-то дополнительную логику. Например, проверять переданный индекс:

class Company
{
Person[] personal;
public Company(Person[] people) => personal = people;
// индексатор
public Person this[int index]
{
get
{
// если индекс имеется в массиве
if (index >= 0 && index < personal.Length)
return personal[index]; // то возвращаем объект Person по индексу
else
throw new ArgumentOutOfRangeException(); // иначе генерируем исключение
}
set
{
// если индекс есть в массиве
if (index >= 0 && index < personal.Length)
personal[index] = value; // переустанавливаем значение по индексу
}
}
}
Здесь в блоке get если переданный индекс имеется в массиве, то возвращаем объект по индексу. Если индекса нет в массиве, то генерируем исключение. Аналогично в блоке set устанавливаем значение по индексу, если индекс есть в массиве.

Индексы
Индексатор получает набор индексов в виде параметров. Однако индексы необязательно должны представлять тип int, устанавливаемые/возвращаемые значения необязательно хранить в массиве. Например, мы можем рассматривать объект как хранилище атрибутов/свойств и передавать имя атрибута в виде строки:

User tom = new User();
// устанавливаем значения
tom["name"] = "Tom";
tom["email"] = "tom@gmail.ru";
tom["phone"] = "+1234556767";

// получаем значение
Console.WriteLine(tom["name"]); // Tom

class User
{
string name = "";
string email = "";
string phone = "";
public string this[string propname]
{
get
{
switch (propname)
{
case "name": return name;
case "email": return email;
case "phone": return phone;
default: throw new Exception("Unknown Property Name");
}
}
set
{
switch (propname)
{
case "name":
name = value;
break;
case "email":
email = value;
break;
case "phone":
phone = value;
break;
}
}
}
}
В данном случае индексатор в классе User в качестве индекса получает строку, которая хранит название атрибута (в данном случае название поля класса).

В блоке get в зависимости от значения строкового индекса возвращается значение того или иного поля класса. Если передано неизвестное название, то генерируется исключение. В блоке set похожая логика - по индексу узнаем, для какого поля надо установить значение.

Применение нескольких параметров
Также индексатор может принимать несколько параметров. Допустим, у нас есть класс, в котором хранилище определено в виде двухмерного массива или матрицы:

class Matrix
{
int[,] numbers = new int[,] { { 1, 2, 4 }, { 2, 3, 6 }, { 3, 4, 8 } };
public int this[int i, int j]
{
get => numbers[i, j];
set => numbers[i, j] = value;
}
}
Теперь для определения индексатора используются два индекса - i и j. И в программе мы уже должны обращаться к объекту, используя два индекса:

Matrix matrix = new Matrix();
Console.WriteLine(matrix[0, 0]);
matrix[0, 0] = 111;
Console.WriteLine(matrix[0, 0]);
Следует учитывать, что индексатор не может быть статическим и применяется только к экземпляру класса. Но при этом индексаторы могут быть виртуальными и абстрактными и могут переопределяться в произодных классах.

Блоки get и set
Как и в свойствах, в индексаторах можно опускать блок get или set, если в них нет необходимости. Например, удалим блок set и сделаем индексатор доступным только для чтения:

class Matrix
{
int[,] numbers = new int[,] { { 1, 2, 4 }, { 2, 3, 6 }, { 3, 4, 8 } };
public int this[int i, int j]
{
get => numbers[i, j];
}
}
Также мы можем ограничивать доступ к блокам get и set, используя модификаторы доступа. Например, сделаем блок set приватным:

class Matrix
{
int[,] numbers = new int[,] { { 1, 2, 4 }, { 2, 3, 6 }, { 3, 4, 8 } };
public int this[int i, int j]
{
get => numbers[i, j];
private set => numbers[i, j] = value;
}
}
Перегрузка индексаторов
Подобно методам индексаторы можно перегружать. В этом случае также индексаторы должны отличаться по количеству, типу или порядку используемых параметров. Например:

var microsoft = new Company(new Person[] { new("Tom"), new("Bob"), new("Sam") });

Console.WriteLine(microsoft[0].Name); // Tom
Console.WriteLine(microsoft["Bob"].Name); // Bob
class Person
{
public string Name { get;}
public Person(string name) => Name=name;
}
class Company
{
Person[] personal;
public Company(Person[] people) => personal = people;
// индексатор
public Person this[int index]
{
get => personal[index];
set => personal[index] = value;
}

public Person this[string name]
{
get
{
foreach (var person in personal)
{
if (person.Name == name) return person;
}
throw new Exception("Unknown name");
}
}
}
В данном случае класс Company содержит две версии индексатора. Первая версия получает и устанавливает объект Person по индексу, а вторая - только получае объект Person по его имени.