Использование индексаторов. Руководство по программированию на C#

Применение индексаторов упрощает работу с синтаксисом, позволяя создавать классы, структуры и интерфейсы, к которым клиентские приложения могут обращаться так же, как к массиву. В этом случае компилятор создает свойство Item (или свойство с другим именем, если присутствует IndexerNameAttribute) и соответствующие методы доступа. Индексаторы чаще всего реализуются в типах, предназначенных преимущественно для инкапсуляции внутренней коллекции или массива. Допустим, у вас есть класс TempRecord, представляющий журнал с 10 измерениями температуры по шкале Фаренгейта за период в 24 часа. Этот класс содержит массив temps типа float[] для хранения значений температуры. Реализация индексатора в этом классе позволит клиентам получать доступ к значениям температуры в экземпляре TempRecord, используя float temp = tempRecord[4] вместо float temp = tempRecord.temps[4]. Это позволяет не только упростить синтаксис клиентских приложений, но и облегчить понимание кода класса и его предназначения другими разработчиками.

Чтобы объявить индексатор для класса или структуры, используйте ключевое слово this, как в следующем примере:

// Indexer declaration
public int this[int index]
{
    // get and set accessors
}

Важно!

При объявлении индексатора для объекта автоматически создается свойство с именем Item. Свойство Item не будет доступно непосредственно из выражения доступа к члену экземпляра. Кроме того, если вы добавите к объекту с индексатором собственное свойство Item, возникнет ошибка компилятора CS0102. Чтобы избежать этого, используйте IndexerNameAttribute, чтобы переименовать индексатор, как описывается ниже.

Примечания

Тип индексатора и типы его параметров должны иметь по крайней мере такой же уровень доступности, как и сам индексатор. Дополнительные сведения об уровнях доступа см. в разделе Модификаторы доступа.

Дополнительные сведения об использовании индексаторов с интерфейсом см. в разделе Индексаторы интерфейса.

Сигнатура индексатора определяет число и типы его формальных параметров. В ней не указываются тип индексатора или имена его формальных параметров. Если для одного класса объявляется несколько индексаторов, они должны иметь разные сигнатуры.

Значение индексатора не классифицируется как переменная и, соответственно, не может передаваться в качестве параметра ref или out.

Чтобы присвоить индексатору имя, которое можно использовать в других языках, используйте System.Runtime.CompilerServices.IndexerNameAttribute, как показано в этом примере:

// Indexer declaration
[System.Runtime.CompilerServices.IndexerName("TheItem")]
public int this[int index]
{
    // get and set accessors
}

Этот индексатор будет иметь имя TheItem, поскольку оно переопределено атрибутом имени индексатора. По умолчанию используется имя индексатора Item.

Пример 1

В следующем примере показано, как объявить частное поле массива temps и индексатор. Индексатор обеспечивает прямой доступ к экземпляру tempRecord[i]. Вместо использования индексатора можно объявить массив как элемент public и осуществлять доступ к его элементам напрямую (tempRecord.temps[i]).

public class TempRecord
{
    // Array of temperature values
    float[] temps = new float[10]
    {
        56.2F, 56.7F, 56.5F, 56.9F, 58.8F,
        61.3F, 65.9F, 62.1F, 59.2F, 57.5F
    };

    // To enable client code to validate input
    // when accessing your indexer.
    public int Length => temps.Length;
    
    // Indexer declaration.
    // If index is out of range, the temps array will throw the exception.
    public float this[int index]
    {
        get => temps[index];
        set => temps[index] = value;
    }
}

Обратите внимание, что при определении прав доступа индексатора, например в инструкции Console.Write, вызывается метод доступа get. Таким образом, если метод доступа get отсутствует, возникает ошибка времени компиляции.

class Program
{
    static void Main()
    {
        var tempRecord = new TempRecord();

        // Use the indexer's set accessor
        tempRecord[3] = 58.3F;
        tempRecord[5] = 60.1F;

        // Use the indexer's get accessor
        for (int i = 0; i < 10; i++)
        {
            Console.WriteLine($"Element #{i} = {tempRecord[i]}");
        }

        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
    /* Output:
        Element #0 = 56.2
        Element #1 = 56.7
        Element #2 = 56.5
        Element #3 = 58.3
        Element #4 = 58.8
        Element #5 = 60.1
        Element #6 = 65.9
        Element #7 = 62.1
        Element #8 = 59.2
        Element #9 = 57.5
    */
}

Индексирование с использованием других значений

В C# тип параметра индексатора не ограничивается целочисленными значениями. Например, в качестве индексатора могут использоваться строки. Такой индексатор можно реализовать путем поиска строки в коллекции с возвратом соответствующего значения. Поскольку методы доступа можно перегружать, строковые и целочисленные версии могут сосуществовать.

Пример 2

Этот пример объявляет класс, который хранит названия дней недели. Метод доступа get принимает название дня в виде строкового значения и возвращает соответствующее целое число. Например, для Sunday возвращается значение 0, для Monday — 1 и т. д.

// Using a string as an indexer value
class DayCollection
{
    string[] days = { "Sun", "Mon", "Tues", "Wed", "Thurs", "Fri", "Sat" };

    // Indexer with only a get accessor with the expression-bodied definition:
    public int this[string day] => FindDayIndex(day);

    private int FindDayIndex(string day)
    {
        for (int j = 0; j < days.Length; j++)
        {
            if (days[j] == day)
            {
                return j;
            }
        }

        throw new ArgumentOutOfRangeException(
            nameof(day),
            $"Day {day} is not supported.\nDay input must be in the form \"Sun\", \"Mon\", etc");
    }
}

Пример использования 2

class Program
{
    static void Main(string[] args)
    {
        var week = new DayCollection();
        Console.WriteLine(week["Fri"]);

        try
        {
            Console.WriteLine(week["Made-up day"]);
        }
        catch (ArgumentOutOfRangeException e)
        {
            Console.WriteLine($"Not supported input: {e.Message}");
        }
    }
    // Output:
    // 5
    // Not supported input: Day Made-up day is not supported.
    // Day input must be in the form "Sun", "Mon", etc (Parameter 'day')
}

Пример 3

В этом примере объявляется класс, в котором хранятся названия дней недели с использованием перечисления System.DayOfWeek. Метод доступа get принимает название дня (DayOfWeek) в виде строкового значения и возвращает соответствующее целое число. Например, для DayOfWeek.Sunday возвращается 0, для DayOfWeek.Monday — 1 и т. д.

using Day = System.DayOfWeek;

class DayOfWeekCollection
{
    Day[] days =
    {
        Day.Sunday, Day.Monday, Day.Tuesday, Day.Wednesday,
        Day.Thursday, Day.Friday, Day.Saturday
    };

    // Indexer with only a get accessor with the expression-bodied definition:
    public int this[Day day] => FindDayIndex(day);

    private int FindDayIndex(Day day)
    {
        for (int j = 0; j < days.Length; j++)
        {
            if (days[j] == day)
            {
                return j;
            }
        }
        throw new ArgumentOutOfRangeException(
            nameof(day),
            $"Day {day} is not supported.\nDay input must be a defined System.DayOfWeek value.");
    }
}

Пример использования 3

class Program
{
    static void Main()
    {
        var week = new DayOfWeekCollection();
        Console.WriteLine(week[DayOfWeek.Friday]);

        try
        {
            Console.WriteLine(week[(DayOfWeek)43]);
        }
        catch (ArgumentOutOfRangeException e)
        {
            Console.WriteLine($"Not supported input: {e.Message}");
        }
    }
    // Output:
    // 5
    // Not supported input: Day 43 is not supported.
    // Day input must be a defined System.DayOfWeek value. (Parameter 'day')
}

Отказоустойчивость

Повысить безопасность и надежность индексаторов можно двумя способами:

  • Реализуйте стратегию обработки ошибок, предусматривающую действия в ситуациях, когда из клиентского кода передается недопустимое значение индекса. В первом примере из этого раздела класс TempRecord содержит свойство Length, с помощью которого клиентский код проверяет введенное значение, прежде чем передать его в индексатор. Кроме того, код обработки ошибок можно поместить в сам индексатор. Не забудьте задокументировать исключения, которые будут вызываться в методе доступа индексатора, для других пользователей.

  • Настройте максимально ограничивающие уровни доступа для методов доступа get и set. Особенно важно сделать это для метода доступа set. Дополнительные сведения см. в разделе Доступность методов доступа.

См. также