1. Пару слов о коллекциях
До сих пор мы работали с массивами — это простейший способ хранить несколько элементов одного типа. Но в C# есть множество более удобных и мощных коллекций, которые решают различные задачи.
Коллекция — это объект, который может хранить группу других объектов. В отличие от массивов, коллекции часто могут изменять свой размер динамически, предоставляют удобные методы для работы с данными и оптимизированы для разных сценариев использования.
Основные типы коллекций (краткий обзор):
List<T> — динамический массив, который может расти и сжиматься:
List<string> names = new List<string>();
names.Add("Алекс");
names.Add("Мария");
Console.WriteLine(names[0]); // Алекс
Console.WriteLine(names.Count); // 2
Dictionary<TKey, TValue> — коллекция пар "ключ-значение", где каждому ключу соответствует одно значение:
Dictionary<string, int> ages = new Dictionary<string, int>();
ages["Алекс"] = 25;
ages["Мария"] = 30;
Console.WriteLine(ages["Алекс"]); // 25
Обратите внимание: в примерах выше мы используем квадратные скобки [] для доступа к элементам коллекций — точно так же, как с массивами! Это возможно благодаря индексаторам, о которых мы сегодня и поговорим.
Это только первое знакомство с коллекциями — подробно их возможности, отличия и применение мы изучим в следующих лекциях. Сейчас важно понять, что коллекции позволяют обращаться к своим элементам через квадратные скобки, и это не магия, а специальный механизм языка.
2. Индексаторы
Обычные объекты C# работают через свойства и методы. Но что делать, если ваш объект — это некая мини-коллекция? Например, представьте:
- Вы пишете класс Week, который должен возвращать название дня недели по номеру: week[0] → "Понедельник".
- Или класс Library, где можно обращаться к книге по её номеру: library[3] → "В сторону Свана".
Звучит удобно, верно? Было бы странно писать library.GetBookByIndex(3) каждый раз — хочется обращаться к своему объекту, как к массиву!
Вот тут-то и появляются индексаторы.
Индексатор — это особый член класса, который позволяет использовать объекты этого класса с синтаксисом квадратных скобок, как массивы: obj[0], obj["ключ"], и так далее.Такой индексатор выглядит внешне как свойство, но вместо имени — принимает параметры внутри квадратных скобок. Это похоже на то, как мы пишем .Name, только вместо этого — [i].
3. Простая коллекция с индексатором
Давайте соберем мини-класс, который будет хранить любимые цвета. Используем для хранения массив строк. Без индексатора пришлось бы делать метод вроде GetColor(int i). Но с индексатором:
using System;
public class FavoriteColors
{
// Приватное поле для хранения цветов
private string[] colors = new string[5];
// Индексатор:
public string this[int index]
{
get
{
// Контроль границ массива (инкапсуляция в действии!)
if (index < 0 || index >= colors.Length)
throw new IndexOutOfRangeException("Неверный индекс цвета!");
return colors[index];
}
set
{
if (index < 0 || index >= colors.Length)
throw new IndexOutOfRangeException("Неверный индекс цвета!");
colors[index] = value ?? throw new ArgumentNullException(nameof(value));
}
}
}
class Program
{
static void Main()
{
FavoriteColors favorites = new FavoriteColors();
favorites[0] = "Зелёный";
favorites[1] = "Синий";
favorites[2] = "Красный";
favorites[10] = "Фиолетовый"; // Бросает исключение!
Console.WriteLine(favorites[1]); // Синий
}
}
Что здесь происходит?
- Мы создаём приватный массив, чтобы никто снаружи не мог с ним баловаться напрямую.
- Индексатор определён как public string this[int index], где this — ключевое слово, указывающее, что индексатор относится к объекту.
- Внутри get и set мы контролируем диапазон, не даём выйти за границы/записать null.
- На выходе мы можем делать favorites[0], как с обычным массивом.
4. Синтаксис индексатора: подробности
Синтаксис похож на свойство, но вместо имени (например, Age) указывается ключевое слово this с параметрами:
// Сигнатура индексатора (общий шаблон)
[модификатор] ТипРезультата this[ТипИндекса имяИндекса]
{
get { ... }
set { ... }
}
Пример: Классический
public class MyCollection
{
private int[] data = new int[10];
// Индексатор для чтения и записи
public int this[int index]
{
get { return data[index]; }
set { data[index] = value; }
}
}
Индексаторы бывают не только по int
Основная фишка: индексатор не обязан принимать только int. Вы можете назначить любой тип (главное, чтобы обращение по ключу имело смысл):
public string this[string colorName]
{
get { /* ... */ }
set { /* ... */ }
}
Например, в классе телефонной книги логично искать по имени:
public class PhoneBook
{
private Dictionary<string, int> entries = new Dictionary<string, int>();
public int this[string name]
{
get
{
if (entries.ContainsKey(name))
return entries[name];
return null;
}
set
{
entries[name] = value;
}
}
}
О коллекциях и как работает Dictionary<string, string> я расскажу в будущих лекциях :P
5. Практический пример: Счетчик слов в тексте
Продолжим развивать наше приложение. Допустим, теперь у нас есть класс, который считает, сколько раз каждое слово встретилось в тексте. Удобно, если пользователь сможет обращаться к объекту через квадратные скобки, чтобы получить количество по слову:
using System.Collections.Generic;
public class WordCounter
{
private Dictionary<string, int> counter = new Dictionary<string, int>();
// Индексатор по строке (слово)
public int this[string word]
{
get
{
if (counter.ContainsKey(word))
return counter[word];
return 0; // Если такого слова нет, возвращаем 0.
}
set
{
counter[word] = value;
}
}
// Метод для подсчета слов из строки
public void AddWords(string text)
{
foreach (var word in text.Split(' ', System.StringSplitOptions.RemoveEmptyEntries))
{
if (counter.ContainsKey(word))
counter[word]++;
else
counter[word] = 1;
}
}
}
// В Main:
var wc = new WordCounter();
wc.AddWords("мама мыла раму мыла мама папа");
Console.WriteLine($"'мама' встречается {wc["мама"]} раз(а)");
Console.WriteLine($"'рама' встречается {wc["рама"]} раз(а)");
Console.WriteLine($"'кот' встречается {wc["кот"]} раз(а)"); // 0
Для чего это нужно на практике? Такой подход часто используется для реализации собственных коллекций, библиотек памяти, маппингов (словари и индексы) и даже DSL (специализированных языков внутри C#).
6. Ограничения и нюансы
Индексаторы — вещь мощная, но есть несколько правил и подводных камней (куда же без них).
Названия у индексатора нет
В отличие от свойства, у индексатора нет имени, только сигнатура вида this[тип параметра]. Если вы начнёте писать public int MyIndexer[int i] — компилятор удивится. Используем только this.
Не бывает статических индексаторов
Индексаторы всегда создаются для экземпляра класса, а не для статических членов. То есть нельзя объявить static int this[int i] — логика в том, что this всегда указывает на конкретный экземпляр объекта.
Могут быть перегружены по типу/числу параметров
Вы можете создать несколько индексаторов в одном классе, если их параметры различаются по типу или количеству. Например:
public string this[int i] { get { ... } set { ... } }
public string this[string key] { get { ... } set { ... } }
Это легально, компилятор не запутается — и напомнит вам, если параметры пересекаются.
Поддержка только через свойства
Нельзя объявить индексатор без аксессоров get или set. Если вы хотите только для чтения — уберите set, только для записи — get. Обычно используют оба.
7. Практическая польза и зачем это знать
- Индексаторы активно применяются в коллекциях данных. Многие классы .NET имеют их: например, List<T>, Dictionary<TKey,TValue>. Когда вы пишете list[2], вы используете именно индексатор!
- Индексаторы позволяют скрыть внутреннюю реализацию (инкапсуляция!), но предоставить удобный и привычный интерфейс. Пользователь вашего класса не задумывается, как вы храните данные, он просто использует привычные [index].
- Ваш код становится лаконичным и интуитивно понятным — что особенно ценят на собеседованиях (и ваши будущие коллеги).
Свойства и Индексаторы: сравнение
| Свойство | Индексатор | |
|---|---|---|
| Имя | Да (например, Name) | Нет (вместо имени — this[параметр]) |
| Доступ | По имени | По индексу (или другому ключу) |
| Статика | Может быть static | Только для экземпляра |
| Много в классе | Да, любое количество | Да, но с разной сигнатурой параметров |
| Применение | Хранение/доступ к данным | Мини-коллекции, ассоциативные данные |
8. Типичные ошибки и советы
Ошибка №1: попытка сделать индексатор static.
Так нельзя — this[...] работает только с объектом.
Ошибка №2: забыли проверку индекса.
Если не проверить границы массива в get или set, программа может завершиться аварийно.
Ошибка №3: перепутаны типы параметров.
Если создать два индексатора с одинаковыми параметрами, компилятор выдаст ошибку.
Ошибка №4: забыли реализовать get или set.
Если нужен доступ и на чтение, и на запись — оба должны быть реализованы.
Совет: если ваш класс оборачивает массив — просто передавайте обращения к массиву через индексатор. Это ускорит работу и сделает код интуитивным.
9. Зачем всё это знать
Индексаторы делают интерфейс объекта простым, понятным и удобным. Они позволяют скрыть внутреннюю реализацию, но оставить разработчику удобный способ доступа к данным.
Именно так реализованы встроенные коллекции: string, List<T>, Dictionary<TKey, TValue>, Span<T> и многие другие. Когда вы пишете array[2] или text[0], вы уже используете индексатор.
А самое главное — вы теперь можете писать свои классы, которые работают так же гибко и лаконично. А значит — вы шаг ближе к написанию профессионального и читаемого кода.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ