Индексаторы позволяют индексировать объекты и обращаться к данным по индексу. Фактически с помощью индексаторов
мы можем работать с объектами как с массивами. По форме они напоминают свойства со стандартными блоками 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, если в них нет необходимости. Например, удалим блок 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 по его имени.