1. Индексаторы с несколькими параметрами
Простой индексатор (например, this[int index]) полезен, когда вам нужен доступ к элементам по целочисленному индексу — почти как в массиве. Но C# разрешает делать куда больше: можно использовать несколько параметров, параметры с разными типами, задавать разные модификаторы доступа для get и set, реализовывать несколько индексаторов в одном классе (если сигнатуры различаются).
Также в новых версиях C# появились дополнительные возможности, упрощающие жизнь при работе с индексаторами, включая синтаксический сахар для лаконичного кода.
Индексатор вовсе не обязан принимать на вход только один индекс и только типа int. Его параметры определяете вы — и можно иметь несколько параметров разного типа, если вашей задаче это нужно.
public class ChessBoard
{
private string[,] board = new string[8, 8];
// Индексатор с двумя параметрами!
public string this[int row, int col]
{
get { return board[row, col]; }
set { board[row, col] = value; }
}
}
Теперь мы можем писать код так:
ChessBoard chess = new ChessBoard();
chess[0, 0] = "Ладья";
chess[7, 7] = "Король";
string piece = chess[0, 0]; // "Ладья"
Такой подход отлично подходит для матриц, двумерных карт, настольных игр, сложных коллекций.
2. Индексаторы с параметрами разных типов
Ваш индексатор может принимать параметры не только типа int, но и любого другого подходящего типа. Главное — чтобы это было логично для вашей задачи.
using System.Collections.Generic;
public class Employee
{
public string Name { get; set; }
public int Age { get; set; }
public string Position { get; set; }
}
public class EmployeeCollection
{
private List<Employee> employees = new List<Employee>();
// Индексатор по имени сотрудника (string)
public Employee this[string name]
{
get
{
foreach (var employee in employees)
{
if (employee.Name == name)
return employee;
}
return null; // Или можно бросить исключение
}
set
{
for (int i = 0; i < employees.Count; i++)
{
if (employees[i].Name == name)
{
employees[i] = value;
return;
}
}
// Если сотрудника с таким именем нет — добавим нового
employees.Add(value);
}
}
// Для совместимости — индексатор по числовому индексу
public Employee this[int index]
{
get { return employees[index]; }
set { employees[index] = value; }
}
}
var company = new EmployeeCollection();
company[0] = new Employee { Name = "Иван", Age = 30, Position = "Программист" };
company[1] = new Employee { Name = "Мария", Age = 25, Position = "Дизайнер" };
Employee employee = company["Иван"];
company["Петр"] = new Employee { Name = "Петр", Age = 28, Position = "Тестировщик" };
Важно: если у вас несколько индексаторов — их сигнатуры должны отличаться по набору и типам параметров.
3. Разные модификаторы доступа для get и set
Бывает, надо разрешить только чтение по индексу, а запись запретить (или наоборот). В C# можно задавать разные модификаторы доступа для get- и set-аксессоров индексатора.
public class SecureEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
get { return employees[index]; }
internal set { employees[index] = value; }
}
}
Это часто используется для защиты коллекций от несанкционированных изменений, делая класс более контролируемым.
4. Read-only и Write-only индексаторы
Иногда хочется разрешить только чтение или только запись через индексатор.
public class ReadOnlyEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
get { return employees[index]; }
// set отсутствует — нельзя менять!
}
}
public class WriteOnlyEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
set
{
employees.Insert(index, value);
}
// get отсутствует — нельзя читать!
}
}
В реальных проектах "write-only" почти не встречается: обычно нужны "read-only", например, когда из класса наружу можно только смотреть, но не менять данные по индексатору.
5. Индексаторы с проверкой границ и логикой
В качественных примерах важно не только дать доступ по индексу, но и корректно обработать выход за границы, ошибки поиска и другие исключительные ситуации.
public class SafeEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
get
{
if (index < 0 || index >= employees.Count)
throw new IndexOutOfRangeException("Сотрудник с таким индексом не существует!");
return employees[index];
}
set
{
if (index < 0 || index >= employees.Count)
throw new IndexOutOfRangeException("Нельзя заменить несуществующего сотрудника!");
employees[index] = value;
}
}
}
Можно возвращать null или использовать современные паттерны обработки отсутствующих значений — выбор зависит от логики вашего приложения.
6. Индексаторы с необычными параметрами
Вы можете встречать коллекции, где индексатор принимает нестандартные типы: перечисления (enum), пользовательские структуры, даже несколько параметров разных типов.
public enum Department { IT, HR, Finance, Marketing }
public class DepartmentEmployeeCollection
{
private Dictionary<Department , Employee> departmentLeads = new Dictionary<Department , Employee>();
public Employee this[Department department]
{
get { return departmentLeads.TryGetValue(department, out var employee) ? employee : null; }
set { departmentLeads[department] = value; }
}
}
var company = new DepartmentEmployeeCollection();
company[Department.IT] = new Employee { Name = "Анна", Age = 35, Position = "Руководитель IT" };
Employee itLead = company[Department.IT];
Такой подход встречается, когда у вас есть уникальные идентификаторы или четкое сопоставление типа и значения — удобно, понятно и типобезопасно.
7. Современные возможности: Range и Index
С появлением типов Range и Index в C# 8 индексаторы получили новые возможности для работы с диапазонами и индексами от конца:
public class SmartArray
{
private int[] numbers = Enumerable.Range(0, 100).ToArray();
public int[] this[Range range] => numbers[range];
public int this[Index index] => numbers[index];
}
// Использование:
var smart = new SmartArray();
int[] middle = smart[20..30]; // с 20-го по 29-й элементы
int last = smart[^1]; // последний элемент
Если вы реализуете свою коллекцию, поддержка Range и Index делает её максимально "нативной" и удобной для разработчиков.
8. Свойства против индексаторов
Чтобы лучше понимать, когда использовать свойства, а когда индексаторы, полезно сравнить их особенности.
- Свойства удобны для доступа к индивидуальным характеристикам объекта по имени: person.Name, car.Speed.
- Индексаторы подходят для доступа к элементам коллекции или структуры по ключу: employees[0], phoneBook["Иван"].
- Свойство всегда имеет конкретное имя и в классе может быть только одно свойство с данным именем.
- Индексатор использует ключевое слово this и может иметь несколько вариантов, если их сигнатуры различаются.
- Свойства могут быть статическими, индексаторы — нет, потому что this всегда указывает на конкретный экземпляр объекта.
9. Типичные ошибки при работе с индексаторами
Ошибка №1: отсутствие проверки границ.
Если не проверить индекс на выход за пределы массива, можно получить IndexOutOfRangeException в неожиданный момент.
Ошибка №2: не обрабатывается возможный null из индексатора.
Если индексатор по строке возвращает null при отсутствии элемента, а вызывающий код бездумно обращается к результату, возникает NullReferenceException.
Ошибка №3: дублирование сигнатур индексаторов.
C# не позволяет создавать два индексатора с одинаковыми наборами параметров.
Ошибка №4: неочевидная логика в set-аксессоре.
Если в set используется логика «добавления при отсутствии», а не замены, это может сбивать с толку. Такие решения стоит делать явными и хорошо документировать.
10. Практическое применение и заключение
В реальных проектах индексаторы часто используются для создания специализированных коллекций, кэшей, словарей с дополнительной логикой, матриц и многомерных структур данных. Они делают код более читаемым и интуитивно понятным — вместо вызова методов типа GetElementByIndex(5) можно просто написать collection[5].
Помните: индексаторы должны логически соответствовать природе вашего класса. Если класс не является коллекцией или структурой данных, вероятно, индексатор ему не нужен. Но если ваш класс хранит и управляет набором элементов, индексатор может сделать его использование гораздо более удобным и естественным.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ