Справочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией
Скачать 5.05 Mb.
|
Глава 10 Индексаторы и свойства Глава 10. Индексаторы и свойства 257 этой главе рассматриваются два специальных типа членов класса, которые тесно связаны друг с другом: индексаторы и свойства. Каждый из этих типов расширяет возможности класса, усиливая его интеграцию в систему типов языка C# и гибкость. Индексаторы обеспечивают механизм, посредством которого к объектам можно получать доступ по индексу подобно тому, как это реализовано в массивах. Свойства предлагают эффективный способ управления доступом к данным экземпляра класса. Эти типы связаны друг с другом, поскольку оба опираются на еще одно средство C#: аксессор, или средство доступа к данным. Индексаторы Как вы знаете, индексация массивов реализуется с использованием оператора “ [] ”. В своих классах вы можете перегрузить его, но не прибегая к “услугам” метода operator() , а посредством создания индексатора (indexer). Индексатор позволяет обеспечить индексированный доступ к объекту. Главное назначение индексаторов — поддержать создание специализированных массивов, на которые налагается одно или несколько ограничений. При этом индексаторы можно использовать в синтаксисе, подобном реализованному в массивах. Индексаторы могут характеризоваться одной или несколькими размерностями, но мы начнем с одномерных индексаторов. Создание одномерных индексаторов Одномерный индексатор имеет следующий формат. тип_элемента this[int индекс ] { // Аксессор считывания данных. get { // Возврат значения, заданного элементом индекс. } // Аксессор установки данных. set { // Установка значения, заданного // элементом индекс. } } Здесь тип_элемента — базовый тип индексатора. Таким образом, тип_элемента — это тип каждого элемента, к которому предоставляется доступ посредством индексатора. Он соответствует базовому типу массива. Параметр индекс получает индекс опрашиваемого (или устанавливаемого) элемента. Строго говоря, этот параметр не обязательно должен иметь тип int , но поскольку индексаторы обычно используются для обеспечения индексации массивов, целочисленный тип — наиболее подходящий. В теле индексатора определяются два аксессора (средства доступа) с именами get и set . Аксессор подобен методу за исключением того, что в нем отсутствует объявление типа возвращаемого значения и параметров. При использовании индексатора аксессоры вызываются автоматически, и в качестве параметра оба аксессора принимают индекс . Если индексатор стоит слева от оператора присваивания, вызывается аксессор set и устанавливается элемент, заданный параметром индекс . В противном случае вызывается аксессор get и возвращается значение, соответствующее параметру индекc . В 258 Часть I. Язык C# Метод set также получает значение (именуемое value ), которое присваивается элементу массива, найденному по заданному индексу. Одно из достоинств индексатора состоит в том, что он позволяет точно управлять характером доступа к массиву, “отбраковывая” попытки некорректного доступа. Рассмотрим пример. В следующей программе класс FailSoftArray реализует массив, который “вылавливает” ошибки нарушения границ, предотвращая возникновение исключительных ситуаций. Это достигается за счет инкапсуляции массива как закрытого члена класса и осуществления доступа к этому массиву только через индексатор. При таком подходе можно предотвратить любую попытку получить доступ к массиву за пределами его границ, причем последствия попытки нарушить границы в этом случае можно сравнить с “мягкой посадкой”, а не с “катастрофическим падением”. Поскольку в классе FailSoftArray используется индексатор, к массиву можно обращаться с помощью обычной формы записи ( [] ). // Использование индексатора для создания // отказоустойчивого массива. using System; class FailSoftArray { int[] a; // Ссылка на массив. public int Length; // Length - открытый член. public bool errflag; // Индикатор результата // последней операции. // Создаем массив заданного размера. public FailSoftArray(int size) { a = new int[size]; Length = size; } // Это - индексатор для класса FailSoftArray. public int this[int index] { // Это - get-аксессор. get { if(ok(index)) { errflag = false; return a[index]; } else { errflag = true; return 0; } } // Это - set-аксессор. set { if(ok(index)) { a[index] = value; errflag = false; } else errflag = true; } } // Метод возвращает значение true, если Глава 10. Индексаторы и свойства 259 // индекс - в пределах границ. private bool ok(int index) { if(index >= 0 & index < Length) return true; return false; } } // Демонстрируем отказоустойчивый массив. class FSDemo { public static void Main() { FailSoftArray fs = new FailSoftArray(5); int x; // Вот как выглядит "мягкая посадка" при ошибках. Console.WriteLine("Мягкое приземление."); for(int i=0; i < (fs.Length * 2); i++) fs[i] = i*10; for(int i=0; i < (fs.Length * 2); i++) { x = fs[i]; if(x != -1) Console.Write(x + " "); } Console.WriteLine(); // Теперь генерируем некорректный доступ. Console.WriteLine( "\nРабота с уведомлением об ошибках."); for(int i=0; i < (fs.Length * 2); i++) { fs[i] = i*10; if(fs.errflag) Console.WriteLine("fs[" + i + "] вне границ"); } for(int i=0; i < (fs.Length * 2); i++) { x = fs[i]; if(!fs.errflag) Console.Write(x + " "); else Console.WriteLine("fs[" + i + "] вне границ"); } } } При выполнении этой программы получаем такие результаты: "Мягкое приземление". 0 10 20 30 40 0 0 0 0 0 Работа с уведомлением об ошибках. fs[5] вне границ fs[6] вне границ fs[7] вне границ fs[8] вне границ fs[9] вне границ 0 10 20 30 40 fs[5] вне границ fs[6] вне границ fs[7] вне границ fs[8] вне границ fs[9] вне границ 260 Часть I. Язык C# Созданный здесь индексатор предотвращает нарушение границ массива. Рассмотрим подробно каждую часть индексатора. Индексатор начинается с такой строки: public int this[int index] { Здесь объявляется индексатор, который оперирует элементами типа int . Индекс передается в параметре index . Сам индексатор — открытый для использования любым кодом вне его класса. Теперь приведем код аксессора get get { if(ok(index)) { errflag = false; return a[index]; } else { errflag = true; return 0; } } Аксессор get предотвращает ошибки нарушения границ. Если заданный индекс находится в пределах границ, аксессор get возвращает элемент, соответствующий этому индексу. А если переданный индекс выходит за пределы границ, операции с массивом не выполняются, но и ничего страшного при этом не происходит. В данной версии класса FailSoftArray переменная errflag содержит результат выполнения каждой операции. Чтобы узнать, как завершилась очередная операция, достаточно проанализировать это поле. Ниже приведен код аксессора set() . Он также предотвращает ошибки нарушения границ. set { if(ok(index)) { a[index] = value; errflag = false; } else errflag = true; } Если заданный индекс находится в пределах границ, значение, переданное через переменную value , присваивается соответствующему элементу массива. В противном случае признак ошибки errflag устанавливается равным значению true . Вспомните, что в любом аксессорном методе value — это автоматически устанавливаемый параметр, который содержит значение, подлежащее записи. Вам не нужно (да вы и не сможете) объявлять этот параметр самим. Индексаторы необязательно создавать с поддержкой как get -, так и set -аксессоров. Можно создать индексатор, предназначенный только для чтения, реализовав лишь get - аксессор. И точно также можно создать индексатор, предназначенный только для записи, реализовав лишь set -аксессор. Перегрузка индексаторов Индексаторы можно перегружать. Здесь приведен пример определения класса FailSoftArray , в котором перегружается индексатор для индексов типа double . В действительности double -индексатор округляет индекс до ближайшего целого числа. Таким образом, из двух определенных в классе индексаторов будет выполняться тот, для которого окажется наилучшим соответствие типов параметра индексатора и его аргумента, используемого в качестве индекса. Глава 10. Индексаторы и свойства 261 // Перегрузка индексатора для класса FailSoftArray. using System; class FailSoftArray { int[] a; // Ссылка на базовый массив. public int Length; // Length (длина) - открытый член. public bool errflag; // Индикатор результата // последней операции. // Создаем массив заданной длины. public FailSoftArray(int size) { a = new int[size]; Length = size; } // Это int-индексатор для класса FailSoftArray. public int this[int index] { // Это — get-аксессор. get { if(ok(index)) { errflag = false; return a[index]; } else { errflag = true; return 0; } } // Это — set-аксессор. set { if(ok(index)) { a[index] = value; errflag = false; } else errflag = true; } } /* Это еще один индексатор для класса FailSoftArray. Здесь в качестве индекса принимается double-аргумент. Затем аргумент округляется до ближайшего целого индекса. */ public int this[double idx] { // Это — get-аксессор. get { int index; // Округление до ближайшего целого int-значения. if( (idx - (int) idx) < 0.5) index = (int) idx; else index = (int) idx + 1; if(ok(index)) { errflag = false; return a[index]; } else { 262 Часть I. Язык C# errflag = true; return 0; } } // Это — set-аксессор. set { int index; // Округление до ближайшего целого int-значения. if( (idx - (int) idx) < 0.5) index = (int) idx; else index = (int) idx + 1; if(ok(index)) { a[index] = value; errflag = false; } else errflag = true; } } // Метод возвращает true, если индекс внутри границ. private bool ok(int index) { if(index >= 0 & index < Length) return true; return false; } } // Демонстрируем отказоустойчивый массив. class FSDemo { public static void Main() { FailSoftArray fs = new FailSoftArray(5); // Помещаем в массив fs несколько значений. for(int i=0; i < fs.Length; i++) fs[i] = i; // Теперь используем в качестве индекса // int- и double-значения. Console.WriteLine("fs[1]: " + fs[1]); Console.WriteLine("fs[2]: " + fs[2]); Console.WriteLine("fs[1.1]: " + fs[1.1]); Console.WriteLine("fs[1.6]: " + fs[1.6]); } } Эта программа генерирует такие результаты: fs[1]: 1 fs[2]: 2 fs[1.1]: 1 fs[1.6]: 2 Как подтверждают результаты выполнения этой программы, double -индексы округляются до ближайших целых значений. В частности, число 1.1 округляется до 1 , а число 1.6 — до 2 Глава 10. Индексаторы и свойства 263 Несмотря на то что перегрузка индексатора, показанная в этой программе, вполне допустима, этот пример — нетипичен. Чаще всего индексатор перегружается, чтобы иметь возможность использовать объект класса в качестве индекса, значение которого вычисляется специальным образом. Индексаторам не требуется базовый массив Индексатор может не использовать базовый массив. Вполне достаточно, чтобы индексатор обеспечивал функционирование, которое для пользователя выглядело бы, как то, что обеспечивают массивы. Например, следующая программа включает индексатор, который действует подобно массиву, предназначенному только для чтения. Этот “массив” содержит степени числа 2 для чисел от 0 до 15. Однако в действительности никакого массива не существует. Вместо этого индексатор просто (и быстро) вычисляет соответствующее значение для заданного индекса. // Индексаторы не обязательно должны использовать // реальные массивы. using System; class PwrOfTwo { /* Доступ к логическому массиву, который содержит степени числа 2 для чисел от 0 до 15. */ public int this[int index] { // Вычисляем и возвращаем степень числа 2. get { if((index >= 0) && (index < 16)) return pwr(index); else return -1; } // Здесь нет set-аксессора. } int pwr(int p) { int result = 1; for(int i=0; i result *= 2; return result; } } class UsePwrOfTwo { public static void Main() { PwrOfTwo pwr = new PwrOfTwo(); Console.Write("Первые 8 степеней числа 2: "); for(int i=0; i < 8; i++) Console.Write(pwr[i] + " "); Console.WriteLine(); Console.Write("А вот несколько ошибок: "); Console.Write(pwr[-1] + " " + pwr[17]); Console.WriteLine(); } } 264 Часть I. Язык C# Вот результаты выполнения этой программы: Первые 8 степеней числа 2: 12 4 8 16 32 64 128 А вот несколько ошибок: -1 -1 Обратите внимание на то, что индексатор класса UsePwrOfTwo включает get - аксессор, но обходится без set -аксессора. Это означает, что индексатор предназначен только для чтения. Таким образом, объект класса UsePwrOfTwo можно использовать в правой части инструкции присвоения, но ни в коем случае не в левой. Например, попытка добавить эту инструкцию в предыдущую программу, обречена на неудачу: pwr[0] = 11; // не скомпилируется Эта инструкция вызовет ошибку компиляции, поскольку в индексаторе не определен set -аксессор. На использование индексаторов накладывается два ограничения. Во-первых, поскольку в индексаторе не определяется область памяти, получаемое индексатором значение нельзя передавать методу в качестве ref - или out -параметра. Во-вторых, индексатор должен быть членом экземпляра своего класса, поэтому его нельзя объявлять с использованием ключевого слова static Многомерные индексаторы Можно создавать индексаторы и для многомерных массивов. Например, вот как работает двумерный отказоустойчивый массив. Обратите особое внимание на способ объявления индексатора в этом классе. // Двумерный отказоустойчивый массив. using System; class FailSoftArray2D { int[,] a; // Ссылка на базовый двумерный массив. int rows, cols; // размерности public int Length; // Length - открытый член. public bool errflag; // Индикатор результата // последней операции. // Создаем массив заданного размера. public FailSoftArray2D(int r, int с) { rows = r; cols = с; a = new int[rows, cols]; Length = rows * cols; } // Это индексатор для класса FailSoftArray2D. public int this[int index1, int index2] { // Это — get-аксессор. get { if(ok(index1, index2)) { errflag = false; return a[index1, index2]; } else { errflag = true; return 0; } } Глава 10. Индексаторы и свойства 265 // Это — set-аксессор. set { if(ok(index1, index2)) { a[index1, index2] = value; errflag = false; } else errflag = true; } } // Метод возвращает значение true, если индексы // находятся внутри границ. private bool ok(int index1, int index2) { if( index1 >= 0 & index1 < rows & index2 >= 0 & index2 < cols) return true; return false; } } // Демонстрируем использование двумерного индексатора. class TwoDIndexerDemo { public static void Main() { FailSoftArray2D fs = new FailSoftArray2D(3, 5); int x; // Демонстрируем "мягкую посадку" при ошибках. Console.WriteLine("Мягкое приземление."); for(int i=0; i < 6; i++) fs[i, i] = i*10; for(int i=0; i < 6; i++) { x = fs[i, i ]; if(x != -1) Console.Write(x + " "); } Console.WriteLine(); //А теперь генерируем ошибки. Console.WriteLine( "\nРабота с уведомлением об ошибках."); for(int i=0; i < 6; i++) { fs[i,i] = i*10; if(fs.errflag) Console.WriteLine( "fs[" + i + ", " + i + "] вне границ"); } for(int i=0; i < 6; i++) { x = fs[i,i]; if(!fs.errflag) Console.Write(x + " "); else Console.WriteLine( "fs[" + i + ", " + i + "] вне границ"); } } } 266 Часть I. Язык C# Результаты, генерируемые этой программой: Мягкое приземление. 0 10 20 0 0 0 Работа с уведомлением об ошибках. fs[3, 3] вне границ fs[4, 4 ] вне границ fs[5, 5] вне границ 0 10 20 fs[3, 3] вне границ fs[4, 4] вне границ fs[5, 5] вне границ Свойства Свойство — это второй специальный тип членов класса, о котором мы собирались поговорить в этой главе. Свойство включает поле и методы доступа к этому полю. Часто требуется создать поле, которое должно быть доступно для пользователей объекта, но программист при этом хочет осуществлять управление операциями, разрешенными к выполнению над этим полем. Например, по некоторым обстоятельствам вы желаете ограничить диапазон значений, которые можно присваивать этому полю. Несмотря на то что этого можно достичь с помощью закрытой переменной и методов доступа к ней, свойство предлагает более удобный и простой способ решения этой задачи. Свойства во многом напоминают индексаторы. Свойство состоит из имени и пары аксессоров ( get и set ). Аксессоры используются для чтения содержимого переменной и записи в нее нового значения. Основное достоинство свойства состоит в том, что его имя можно использовать в выражениях и инструкциях присваивания подобно обычной переменной, хотя в действительности здесь будут автоматически вызываться get - и set - аксессоры. Автоматический вызов аксессоров и роднит свойства с индексаторами. Формат записи свойства таков: тип имя { get{ // код аксессора чтения поля } set{ // код аксессора записи поля } Здесь тип — это тип свойства (например, int ), а имя — его имя. После определения свойства любое использование его имени означает вызов соответствующего аксессора. Аксессор set автоматически принимает параметр с именем value , который содержит значение, присваиваемое свойству. Важно понимать, что свойства не определяют область памяти. Следовательно, свойство управляет доступом к полю, но самого поля не обеспечивает. Это поле должно быть задано независимо от свойства. Рассмотрим простой пример, в котором определяется свойство myprop , используемое для доступа к полю prop . Это свойство позволяет присваивать полю только положительные числа. // Пример использования свойства. using System; Глава 10. Индексаторы и свойства 267 class SimpProp { int prop; // Это поле управляется свойством myprop. public SimpProp() { prop =0; } /* Это свойство поддерживает доступ к закрытой переменной экземпляра prop. Оно позволяет присваивать ей только положительные числа. */ public int myprop { get { return prop; } set { if(value >= 0) prop = value; } } } // Демонстрируем использование свойства. class PropertyDemo { public static void Main() { SimpProp ob = new SimpProp(); Console.WriteLine("Исходное значение ob.myprop: " + ob.myprop); ob.myprop = 100; // Присваиваем значение. Console.WriteLine("Значение ob.myprop: " + ob.myprop); // Переменной prop невозможно присвоить // отрицательное значение. Console.WriteLine( "Попытка присвоить -10 свойству ob.myprop"); ob.myprop = -10; Console.WriteLine("Значение ob.myprop: " + ob.myprop); } } Результаты выполнения этой программы выглядят так: Исходное значение ob.myprop: 0 Значение ob.myprop: 100 Попытка присвоить -10 свойству ob.myprop Значение ob.myprop: 100 На этой программе стоит остановиться подробнее. В классе SimpProp определяется закрытое поле prop и свойство myprop , которое управляет доступом к полю prop . Как упоминалось выше, свойство само не определяет область хранения поля, а лишь управляет доступом к нему. Поэтому без определения базового поля определение свойства теряет всякий смысл. Более того, поскольку поле prop закрытое, к нему можно получить доступ только посредством свойства myprop. Свойство myprop определяется как public -член класса, чтобы к нему можно было обратиться с помощью кода вне класса, включающего это свойство. В этом есть своя логика, поскольку свойство предоставляет доступ к закрытому полю prop с помощью аксессоров: get -аксессор просто возвращает значение prop , a set -аксессор устанавливает новое значение prop , если оно положительно. Таким образом, свойство 268 Часть I. Язык C# myprop управляет тем, какие значения может содержать поле prop . В этом и состоит важность свойств. Свойство myprop предназначено для чтения и записи, поскольку позволяет как прочитать содержимое своего базового поля, так и записать в него новое значение. Но можно создавать свойства, предназначенные только для чтения (определив лишь get - аксессор) либо только для записи (определив лишь set -аксессор). Мы можем использовать свойство для дальнейшего усовершенствования класса, определяющего отказоустойчивый массив. Как вы уже знаете, с каждым массивом связано свойство Length . До сих пор в классе FailSoftArray для этой цели просто использовалось открытое целочисленное поле Length . Такое решение — не самое лучшее, поскольку в этом случае можно записать в поле Length значение, отличное от реальной длины этого массива. (Например, какой-нибудь программист с дурными наклонностями мог умышленно ввести это значение.) Потенциально опасную ситуацию можно исправить, заменив открытую переменную Length свойством, предназначенным только для чтения, как показано в следующей версии класса FailSoftArray // Добавляем в класс FailSoftArray свойство Length. using System; class FailSoftArray { int[] a; // Ссылка на базовый массив. int len; // Длина массива, основа для свойства Length. public bool errflag; // Индикатор результата // последней операции. // Создаем массив заданного размера. public FailSoftArray(int size) { a = new int[size]; len = size; } // Свойство Length предназначено только для чтения. public int Length { get { return len; } } // Это — индексатор класса FailSoftArray. public int this[int index] { // Это — get-аксессор. get { if(ok(index)) { errflag = false; return a[index]; } else { errflag = true; return 0; } } // Это — set-аксессор. set { if(ok(index)) { a[index] = value; Глава 10. Индексаторы и свойства 269 errflag = false; } else errflag = true; } } // Метод возвращает true, если индекс внутри границ. private bool ok(int index) { if(index >= 0 & index < Length) return true; return false; } } // Демонстрируем улучшенный отказоустойчивый массив. class ImprovedFSDemo { public static void Main() { FailSoftArray fs = new FailSoftArray(5); int x; // Свойство Length можно считывать. for(int i=0; i < fs.Length; i++) fs[i] = i*10; for(int i=0; i < fs.Length; i++) { x = fs[i]; if(x != -1) Console.Write(x + " "); } Console.WriteLine(); // fs.Length = 10; // Ошибка, запись запрещена! } } Теперь Length — это свойство, которое в качестве области памяти использует закрытую переменную len . В этом свойстве определен только get -аксессор, и потому свойство Length можно только читать, но не изменять. Чтобы убедиться в этом, попробуйте убрать символы комментариев в начале следующей строки программы: // fs.Length = 10; // Ошибка, запись запрещена! При попытке скомпилировать программу с этой строкой кода вы получите сообщение об ошибке, уведомляющее о том, что свойство Length предназначено только для чтения. Внесение в класс FailSoftArray свойства Length значительно улучшило его, но на этом рано ставить точку. Член errflag — еще один кандидат на “превращение” из обычной переменной экземпляра в свойство со всеми преимуществами, поскольку доступ к нему следует ограничить до определения “только для чтения”. Приводим окончательную версию класса FailSoftArray , в которой создано свойство Error , использующее в качестве области хранения индикатора ошибки исходную переменную errflag // Превращаем переменную errflag в свойство. using System; class FailSoftArray { int[] a; // Ссылка на базовый массив. int len; // Длина массива. 270 Часть I. Язык C# bool errflag; // Теперь этот член закрыт. // Создаем массив заданного размера. public FailSoftArray(int size) { a = new int[size]; len = size; } // Свойство Length предназначено только для чтения. public int Length { get { return len; } } // Свойство Error предназначено только для чтения. public bool Error { get { return errflag; } } // Это - индексатор класса FailSoftArray. public int this[int index] { // Это — get-аксессор. get { if(ok(index)) { errflag = false; return a[index]; } else { errflag = true; return 0; } } // Это — set-аксессор. set { if(ok(index)) { a[index] = value; errflag = false; } else errflag = true; } } // Метод возвращает true, если индекс внутри границ. private bool ok(int index) { if(index >= 0 & index < Length) return true; return false; } } // Демонстрируем улучшенный отказоустойчивый массив. class FinalFSDemo { public static void Main() { FailSoftArray fs = new FailSoftArray(5); // Используем свойство Error. Глава 10. Индексаторы и свойства 271 for(int i=0; i < fs.Length + 1; i++) { fs[i] = i*10; if(fs.Error) Console.WriteLine("Ошибка в индексе " + i); } } } Создание свойства Error заставило нас внести в класс FailSoftArray два изменения. Во-первых, переменную errflag пришлось сделать закрытой, поскольку она теперь используется в качестве базовой области памяти для свойства Error . В результате к члену errflag теперь нельзя обращаться напрямую. Во-вторых, добавлено свойство Error , предназначенное только для чтения. Отныне программы для выявления ошибок будут опрашивать не поле errflag , а свойство Error . Это продемонстрировано в методе Main() , в котором умышленно генерируется ошибка нарушения границ массива, а для ее обнаружения используется свойство Error Правила использования свойств На использование свойств налагаются довольно серьезные ограничения. Во-первых, поскольку в свойстве не определяется область памяти, его нельзя передавать методу в качестве ref - или out -параметра. Во-вторых, свойство нельзя перегружать. (Но при необходимости вы можете иметь два различных свойства, которые используют одну и ту же базовую переменную, но к такой организации свойств прибегают нечасто.) Наконец, свойство не должно изменять состояние базовой переменной при вызове get -аксессора, хотя несоблюдение этого правила компилятор обнаружить не в состоянии. Другими словами, get -операция должна быть максимально простой. Использование индексаторов и свойств Несмотря на то что в предыдущих примерах продемонстрирован механизм работы индексаторов и свойств, в них не отражена в полной мере вся мощь этих программных средств. В заключение этой главы мы рассмотрим класс RangeArray , использующий индексаторы и свойства для создания массива такого типа, в котором индексный диапазон массива определяется программистом. Как вы знаете, в C# индексация массивов начинается с нуля. Однако в некоторых приложениях было бы удобно начинать индексацию массивов с произвольного числа, например с единицы или даже с отрицательного числа, чтобы индексы изменялись в диапазоне от -5 до 5. Приведенный здесь класс RangeArray как раз и позволяет подобные способы индексации массивов. При использовании класса RangeArray можно написать такие строки кода: RangeArray rа = new RangeArray(-5, 10); // Массив с // индексами от -5 до 10. for(int i = -5; i <= 10; i++) ra[i] = i; // Индекс // изменяется от -5 до 10. Нетрудно догадаться, что первая строка создает объект класса RangeArray (массив ra ), в котором индексы изменяются от -5 до 10 включительно. Первый аргумент задает начальный индекс, а второй — конечный. 272 Часть I. Язык C# Ниже представлены классы RangeArray и RangeArrayDemo , демонстрирующие использование этого массива. Класс RangeArray поддерживает массив int -элементов, но при необходимости можно заменить этот тип данных другим. /* Создание класса для поддержки массива с заданным диапазоном индексации. Класс RangeArray позволяет начинать индексацию с числа, отличного от нуля. При создании объекта класса RangeArray необходимо задать индексы начала и конца диапазона. Отрицательные индексы также допустимы. Например, можно создать массивы с диапазоном изменения индексов от -5 до 5, от 1 до 10 или от 50 до 56. */ using System; class RangeArray { // Закрытые данные. int[] a; // Ссылка на базовый массив. int lowerBound; // Наименьший индекс. int upperBound; // Наибольший индекс. // Данные для свойств. int len; // Базовая переменная для свойства Length. bool errflag; // Базовая переменная для свойства Error. // Создаем массив с заданным размером. public RangeArray(int low, int high) { high++; if(high <= low) { Console.WriteLine("Неверные индексы."); high = 1; // Создаем минимальный массив для // безопасности. low = 0; } a = new int[high - low]; len = high - low; lowerBound = low; upperBound = --high; } // Свойство Length, предназначенное только для чтения. public int Length { get { return len; } } // Свойство Error, предназначенное только для чтения. public bool Error { get { return errflag; } } // Это — индексатор для класса RangeArray. public int this[int index] { Глава 10. Индексаторы и свойства 273 // Это — get-аксессор. get { if(ok(index)) { errflag = false; return a[index - lowerBound]; } else { errflag = true; return 0; } } // Это — set-аксессор. set { if(ok(index)) { a[index - lowerBound] = value; errflag = false; } else errflag = true; } } // Метод возвращает true, если индекс находится // внутри границ. private bool ok(int index) { if(index >= lowerBound & index <= upperBound) return true; return false; } } // Демонстрируем использование массива с произвольно // заданным диапазоном индексов. class RangeArrayDemo { public static void Main() { RangeArray ra = new RangeArray(-5, 5); RangeArray ra2 = new RangeArray(1, 10); RangeArray ra3 = new RangeArray(-20, -12); // Используем массив ra. Console.WriteLine("Длина массива ra: " + ra.Length); for(int i = -5; i <= 5; i++) ra[i] = i; Console.Write("Содержимое массива ra: "); for(int i = -5; i <= 5; i++) Console.Write(ra[i] + " "); Console.WriteLine("\n"); // Используем массив ra2, Console.WriteLine("Длина массива ra2: " + ra2.Length); for(int i = 1; i <= 10; i++) ra2[i] = i; Console.Write("Содержимое массива ra2: "); for(int i = 1; i <= 10; i++) 274 Часть I. Язык C# Console.Write(ra2[i] + " "); Console.WriteLine("\n"); // Используем массив ra3 Console.WriteLine("Длина массива ra3: " + ra3.Length); for(int i = -20; i <= -12; i++) ra3[i] = i; Console.Write("Содержимое массива ra3: "); for(int i = -20; i <= -12; i++) Console.Write(ra3[i] + " "); Console.WriteLine("\n"); } } При выполнении эта программа генерирует такие результаты: Длина массива ra: 11 Содержимое массива ra: -5 -4 -3 -2 -1 0 1 2 3 4 5 Длина массива ra2: 10 Содержимое массива rа2: 1 2 3 4 56 7 8 9 10 Длина массива ra3: 9 Содержимое массива ra3: -20 -19 -18 -17 -16 -15 -14 -13 -12 Как подтверждают результаты выполнения этой программы, объекты типа RangeArray могут быть индексированы не обязательно начиная с нуля. Рассмотрим, как же реализован класс RangeArray Определение этого класса начинается с определения закрытых переменных экземпляра: // Закрытые данные. int[] а; // Ссылка на базовый массив. int lowerBound; // Наименьший индекс. int upperBound; // Наибольший индекс. // Данные для свойств. int len; // Базовая переменная для свойства Length. bool errflag; // Базовая переменная для свойства Error. Базовый массив имеет имя а . Он размешается в памяти с помощью конструктора класса RangeArray . Индекс нижней границы массива сохраняется в закрытой переменной lowerBound , а индекс верхней границы — в закрытой переменной upperBound . Затем объявляются переменные элемента, которые поддерживают свойства Length и Error Конструктор класса RangeArray имеет такой вид: // Создаем массив с заданным размером. public RangeArray(int low, int high) { high++; if(high <= low) { Console.WriteLine("Неверные индексы."); high = 1; // Создаем минимальный массив для // безопасности. low = 0; Глава 10. Индексаторы и свойства 275 } а = new int[high - low]; len = high - low; lowerBound = low; upperBound = --high; } Объект типа RangeArray создается в результате передачи нижнего граничного индекса в параметр low и верхнего граничного индекса — в параметр high . Значение high затем инкрементируется, чтобы вычислить размер массива, поскольку задаваемые индексы изменяются от low до high включительно. После этого проверяем, действительно ли верхний индекс больше нижнего. Если это не так, выводится сообщение об ошибке в индексах и создается одноэлементный массив. Затем выделяется область памяти для массива (либо корректно заданного, либо ошибочно), и ссылка на эту область присваивается переменной а . А переменная len (на которой основано свойство Length ) устанавливается равной количеству элементов в массиве. Наконец, устанавливаются закрытые переменные lowerBound и upperBound В классе RangeArray затем реализуются свойства Length и Error . Ниже приведены их определения. // Свойство Length, предназначенное только для чтения. public int Length { get { return len; } } // Свойство Error, предназначенное только для чтения. public bool Error { get { return errflag; } } Эти свойства аналогичны свойствам, используемым классом FailSoftArray , и их работа организована подобным образом. Далее в классе RangeArray реализуется индексатор. Вот его определение: // Это — индексатор для класса RangeArray. public int this[int index] { // Это — get-аксессор. get { if(ok(index)) { errflag = false; return a[index - lowerBound]; } else { errflag = true; return 0; } } // Это — set-аксессор. set { if(ok(index)) { a[index - lowerBound] = value; errflag = false; } else errflag = true; 276 Часть I. Язык C# } } Этот индексатор очень похож на индексатор класса FailSoftArray , но с одним важным отличием. Обратите внимание на выражение, которое служит в качестве значения индекса массива а index - lowerBound Это выражение преобразует реальный индекс, переданный через параметр index , в “нормальный”, т.е. в значение, которое имел бы индекс текущего элемента, если бы индексирование массива начиналась с нуля. Ведь только такое индексирование подходит для базового массива а . Это выражение работает при любом значении переменной lowerBound : положительном, отрицательном или нулевом. Теперь осталось рассмотреть определение метода ok() // Метод возвращает значение true, если индекс // находится внутри границ. private bool ok(int index) { if(index >= lowerBound & index <= upperBound) return true; return false; } Это определение аналогично тому, которое используется в классе FailSoftArray за исключением того, что попадание индекса в нужный диапазон проверяется с помощью значений переменных lowerBound и upperBound Класс RangeArray иллюстрирует только один вид массива, создаваемого “под заказ” с помощью индексаторов и свойств. Можно также создавать динамические массивы (которые расширяются и сокращаются по необходимости), ассоциативные и разреженные массивы. Попробуйте создать один из таких массивов в качестве упражнения. |