Шаг 135.
Язык программирования C#. Начала
Свойства и индексаторы. Перегрузка индексаторов

    На этом шаге мы рассмотрим пример такой перегрузки.

    Индексаторы можно перегружать. Перегрузка индексаторов означает, что в классе может быть описано несколько индексаторов, которые должны отличаться количеством и/или типом индексов. Пример класса с перегрузкой индексатора можно найти в программе ниже.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace pr135_1
{
    // Класс с несколькими индексаторами: 
    class MyClass{
        // Закрытое целочисленное поле:
        private int[] nums;
        // Конструктор класса с одним аргументом: 
        public MyClass(int size) {
            // Создание массива: 
            nums = new int[size];
            // Заполнение массива:
            for (int k = 0; k < nums.Length; k++) {
                // Использование индексатора: 
                this[k] = k + 1;
            }
        }
        // Переопределение метода ToString(): 
        public override string ToString(){
            // Текстовая переменная:
            string txt = "Содержимое объекта:\n";
            // Формирование текстовой строки: 
            for (int k = 0; k < nums.Length; k++){
                // Используется индексатор с целочисленным 
                // индексом:
                txt += this[k] + (k == nums.Length - 1? "\n": " ");
            }
            // Результат метода: 
            return txt;
        }
        // Индексатор с целочисленным индексом: 
        public int this[int k]{
            // Аксессор для считывания значения: 
            get {
                return nums[k % nums.Length];
            }
            // Аксессор для присваивания значения: 
            set {
                nums[k % nums.Length] = value;
            }
        }
        // Индексатор с символьным индексом: 
        public int this[char s]{
            // Аксессор для считывания значения: 
            get {
                // Используется индексатор с целочисленным 
                // индексом: 
                return this[s - 'a'];
            }
            // Аксессор для присваивания значения: 
            set {
                // Используется индексатор с целочисленным
                // индексом:
                this[s - 'a'] = value;
            }
        }
        // Индексатор с целочисленным и текстовым индексом: 
        public int this[int k, string t] {
            // Аксессор для считывания значения: 
            get {
                // Используется индексатор с символьным индексом: 
                return this[t[k]];
            }
            // Аксессор для присваивания значения: 
            set {
                // Используется индексатор с символьным индексом: 
                this[t[k]] = value;
            }
        }
        // Индексатор с текстовым и целочисленным индексом:
        public int this[string t, int k] {
            // Аксессор для считывания значения: 
            get {
                // Использование индексатора с целочисленным и 
                // текстовым индексом: 
                return this[k, t];
            }
            // Аксессор для присваивания значения: 
            set {
                // Использование индексатора с целочисленным и 
                // текстовым индексом: 
                this[k, t] = value;
            }
        }
    }
    
    // Класс с главным методом: 
    class Program
    {
        // Главный метод:
        static void Main()
        {
            // Целочисленная переменная: 
            int n = 6;
            // Создание объекта:
            MyClass obj = new MyClass(n);
            // Проверка содержимого объекта:
            Console.WriteLine(obj);
            // Поэлементное отображение содержимого массива: 
            for (int k = 0; k < n + 3; k++) {
                // Объект с целочисленным индексом:
                Console.Write(obj[k] + " ");
            }
            Console.WriteLine("\n");
            // Объект с целочисленным индексом:
            obj[1] = 7; 
            obj[n + 3] = 8;
            Console.WriteLine(obj);
            // Объект с символьным индексом:
            obj['a'] = 9;
            obj['k'] = 0;
            // Проверка содержимого объекта:
            Console.WriteLine(obj);
            Console.WriteLine("Проверка:");
            // Поэлементное отображение содержимого массива: 
            for(char s = 'a'; s < 'a' + n + 3; s++) {
                // Объект с символьным индексом:
                Console.Write(obj[s] + " ");
            }
            Console.WriteLine("\n");
            // Объект с целочисленным и текстовым индексом: 
            obj[4, "alpha"] = 0; 
            obj["bravo", 0] = 6;
            // Проверка содержимого массива:
            Console.WriteLine(obj);
            // Текстовая переменная: 
            string txt = "abc";
            Console.WriteLine("Проверка:");
            // Отображение значений элементов массива: 
            for (int k = 0; k < txt.Length; k++)
            {
                // Объект с двумя индексами:
                Console.WriteLine(obj[k, txt] + ": " + obj[txt, k]);
            }
            // Задержка:
            Console.ReadLine();
        }
    }
}
Архив проекта можно взять здесь.

    Результат выполнения программы такой.


Рис.1. Результат выполнения программы

    В классе MyClass есть закрытый целочисленный массив nums. Массив создается и заполняется при вызове конструктора. Для заполнения массива запускается цикл. Там (при заданном индексе k) выполняется команда

                this[k] = k + 1;    , 
в которой использован индексатор. В этой команде ключевое слово this обозначает объект, из которого вызывается метод (в данном случае речь идет об объекте, при создании которого вызывается конструктор).


Напомним, что ключевое слово this является зарезервированным и обозначает объект, из которого вызывается метод.

    Инструкция this[k] означает, что индексируется объект, для которого вызван конструктор. Этому выражению присваивается значение k+1. Данная команда выполняется в соответствии с тем, как описан set-аксессор индексатора с целочисленным индексом (в классе описано несколько индексаторов). В соответствии с кодом set-аксессора выполняется команда

                nums[k % nums.Length] = value;   , 
которой значение присваивается элементу массива nums. Индекс элемента вычисляется выражением k % nums.Length. Результатом является остаток отделения фактического значения индекса k на размер массива nums.Length. Общий эффект такой, что если положительный индекс k не превышает верхнюю допустимую границу (значение nums.Length-1), то значение выражения k % nums.Length совпадает со значением k, а при выходе индекса k за верхнюю допустимую границу выполняется циклическая перестановка индекса. Такой же подход использован и в get-аксессоре индексатора: результатом возвращается значение элемента nums[k % nums.Length], в котором индекс определяется выражением k % nums.Length.

    Индексатор с целочисленным индексом мы используем и при переопределении метода ToString(): в теле метода при формировании текстовой строки (возвращаемой результатом метода) использовано выражение this[k], смысл которого такой же, как описывалось выше.


Значение выражения
  k == nums.Length - 1? "\n": " "
на основе тернарного оператора ?: вычисляется так: если значение переменной k равно значению выражения nums.Length-1 (значение индекса последнего элемента массива nums), результатом выражения является текст "\n". В противном случае результатом выражения является текст " ".

    Но в классе MyClass описаны и другие индексаторы. Там есть индексатор с символьным индексом. В get-аксессоре результатом возвращается выражение this[s-'а']. Поскольку значением выражения s-'а' является число (разность кодов символов), то выражение this[s-'а'] вычисляется вызовом get-акссссора для индексатора с целочисленным индексом. Аналогично, в set-аксессоре для индексатора с символьным индексом при выполнении команды

                this[s - 'a'] = value;
вызывается set-аксессор для индексатора с целочисленным индексом.

    Индексатор с двумя индексами (целое число k и текст t) в get-аксессоре содержит команду

                return this[t[k]];    .
В выражении this[t[k]] индекс t[k] является символом (символ из текста t с индексом k), поэтому данное выражение вычисляется вызовом get-аксессора для индексатора с символьным индексом. То же справедливо и для set-аксессора, в котором выполняется команда
                this[t[k]] = value;    .

    Еще одна версия индексатора с двумя индексами отличается от описанной выше порядком индексов: теперь первый индекс t текстовый и второй индекс k целочисленный. В get-аксессоре выполняется команда

                return this[k, t];    .
В выражении this[k, t] первый индекс целочисленный, а второй индекс текстовый. Аналогично в set-аксессоре выполняется команда
                this[k, t] = value;   ,
содержащая такое же выражение this[k, t]. В обоих случаях речь идет об использовании индексатора с первым целочисленным индексом и вторым текстовым индексом. В итоге мы создали ситуацию, когда порядок, в котором мы указываем целочисленный и текстовый индекс, не имеет значения.

    В главном методе программы создается объект obj класса MyClass. Массив в этом объекте содержит 6 (значение переменной n) элементов, заполненных числами от 1 до 6 включительно. Далее мы запускаем конструкцию цикла, в котором индексная переменная k "выскакивает" за допустимую верхнюю границу. Но проблемы в этом нет, поскольку при индексировании объекта obj выполняется циклическая перестановка индексов.

    При выполнении команд

            obj[1] = 7; 
            obj[n + 3] = 8;
элементы с индексами 1 и 3 массива nums в объекте obj получают новые значения 7 и 8 соответственно. При выполнении команд
            obj['a'] = 9;
            obj['k'] = 0;
новые значения (9 и 0) получают элементы с индексами 0 и 4.

    Если мы используем символ в качестве индекса, то запрос выполняется для элемента с индексом, равным разности кодов этого символа и символа 'а'. Поэтому символ 'а' эквивалентен числовому индексу 0, а символ 'k' эквивалентен числовому индексу 10, что с учетом циклической перестановки для массива из шести элементов дает значение 4.

    Когда мы перебираем элементы массива, индексируя символьными значениями объект, то при выходе за допустимую верхнюю границу индексы переставляются циклически - после значения последнего элемента массива отображается значение первого (начального) элемента, затем второго и так далее. Причина в том, что при обработке выражения с символьным индексом на самом деле вычисляется выражение с целочисленным индексом, а для таких случаев предусмотрена циклическая перестановка индексов.

    При выполнении команды

            obj[4, "alpha"] = 0; 
нулевое значение присваивается элементу массива nums с индексом, который является символом с индексом 4 в тексте "alpha". Это символ 'а', который соответствует числовому индексу 0 в массиве. Выполнение команды
            obj["bravo", 0] = 6;
означает, что значение 6 присваивается элементу массива nums с индексом, который является символом с индексом 0 в тексте "bravo". Это символ 'b', который соответствует числовому индексу 1 в массиве.

    Также в главном методе есть конструкция цикла, в котором с помощью индексной переменной k перебираются символы из текстовой переменной txt (со значением "abc"). В консольном окне отображаются значения выражений вида obj[k, txt] и obj[txt, k], которые должны совпадать (и они совпадают), поскольку порядок следования индексов не имеет значения.


В рассмотренном примере при определении одних индексаторов мы использовали другие индексаторы. Это не является обязательным условием, но очень часто упрощает жизнь и делает код более универсальным.

    На следующем шаге мы резюмируем все ранее сказанное по данной теме на предыдущих шагах.




Предыдущий шаг Содержание Следующий шаг