Индексатор представляет собой разновидность свойства. Если у класса есть скрытое поле, представляющее собой массив, то с помощью индексатора можно обратиться к элементу этого массива, используя имя объекта и номер элемента массива в квадратных скобках. Иными словами, индексатор — это такой «умный» индекс для объектов.
Синтаксис индексатора аналогичен синтаксису свойства:
[атрибуты] [спецификаторы] тип this [ список_параметров ]
{
get код_доступа
set код_доступа
}
ВНИМАНИЕ
В данном случае квадратные скобки являются элементом синтаксиса, а не указанием на необязательность конструкции.
Атрибуты мы рассмотрим позже, в главе 12, а спецификаторы аналогичны спецификаторам свойств и методов. Индексаторы чаще всего объявляются со спецификатором public, поскольку они входят в интерфейс объекта (в версии С# 2.0 допускается раздельное указание спецификаторов доступа для блоков получения и установки индексатора, аналогично свойствам). Атрибуты и спецификаторы могут отсутствовать.
|
|
Код доступа представляет собой блоки операторов, которые выполняются при получении (get) или установке значения (set) элемента массива. Может отсутствовать либо часть get, либо set, но не обе одновременно. Если отсутствует часть set, индексатор доступен только для чтения (read-only), если отсутствует часть get, индексатор доступен только для записи (write-only).
Список параметров содержит одно или несколько описаний индексов, по которым выполняется доступ к элементу. Чаще всего используется один индекс целого типа.
Индексаторы в основном применяются для создания специализированных массивов, на работу с которыми накладываются какие-либо ограничения. В листинге 7.3 создан класс-массив, элементы которого должны находиться в диапазоне [О, 100]. Кроме того, при доступе к элементу проверяется, не вышел ли индекс за допустимые границы.
Листинг 7.3. Использование индексаторов
using System;
namespace ConsoleApplication1
{
class SafeArray
{
public SafeArray(int size) // конструктор класса
{
a = new int[size];
length = size;
}
public int Length // свойство - размерность
{
get { return length; }
}
public int this[int i] // индексатор
{
get
{
if (i >= 0 && i < length) return a[i];
else { error = true; return 0; }
}
set
{
if (i >= 0 && i < length && value >= 0 && value <= 100)
a[i] = value;
else error = true;
}
}
public bool error = false; // открытый признак ошибки
int[] a; // закрытый массив
int length; // закрытая размерность
}
class Class1
{
static void Main()
{
int n = 100;
SafeArray sa = new SafeArray(n); // создание объекта
for (int i = 0; i < n; ++i)
{
sa[i] = i * 2; // 1 использование индексатора
Console.Write(sa[i]); // 2 использование индексатора
}
if (sa.error)
Console.Write("Были ошибки!");
}
}
}
Из листинга видно, что индексаторы описываются аналогично свойствам. Благодаря применению индексаторов с объектом, заключающим в себе массив, можно работать так же, как с обычным массивом. Если обращение к объекту встречается в левой части оператора присваивания (оператор 1), автоматически вызывается метод get. Если обращение выполняется в составе выражения (оператор 2), вызывается метод set.
|
|
В классе SafeArray принята следующая стратегия обработки ошибок: если при попытке записи элемента массива его индекс или значение заданы неверно, значение элементу не присваивается; если при попытке чтения элемента индекс не входит в допустимый диапазон, возвращается 0; в обоих случаях формируется значение открытого поля error, равное true.
ПРИМЕЧАНИЕ
Проверка значения поля error может быть выполнена после какого-либо блока, предназначенного для работы с массивом, как в приведенном примере, или после каждого подозрительного действия с массивом (это может замедлить выполнение программы). Такой способ обработки ошибок не является единственно возможным. Правильнее обрабатывать подобные ошибки с помощью механизма исключений, соответствующий пример будет приведен далее в этой главе.
Вообще говоря, индексатор не обязательно должен быть связан с каким-либо внутренним полем данных. В листинге 7.4 приведен пример класса Pow2, единственное назначение которого — формировать степень числа 2.
Листинг 7.4. Индексатор без массива
using System;
namespace ConsoleApplication1
{
class Pow2
{
public ulong this[int i]
{
get
{
if (i >= 0)
{
ulong res = 1;
for (int k = 0; k < i; k++) // цикл получения степени
unchecked { res *= 2; } // 1
return res;
}
else return 0;
}
}
}
class Class1
{
static void Main()
{
int n = 13;
Pow2 pow2 = new Pow2();
for (int i = 0; i < n; ++i)
Console.WriteLine("{0}\t{l}", i, pow2[i]);
}
}
}
Оператор 1 выполняется в непроверяемом контексте1, для того чтобы исключение, связанное с переполнением, не генерировалось. В принципе, данная программа работает и без этого, но если поместить класс Pow2 в проверяемый контекст, при значении, превышающем допустимый диапазон для типа ulong, возникнет исключение. Результат работы программы:
0 1
1 2
2 4
3 8
4 16
5 32
6 64
7 128
8 256
9 512
10 1024
11 2048
12 4096
Язык С# допускает использование многомерных индексаторов. Они описываются аналогично обычным и применяются в основном для контроля за занесением данных в многомерные массивы и выборке данных из многомерных массивов, оформленных в виде классов. Например:
int[,] a;
Если внутри класса объявлен такой двумерный массив, то заголовок индексатора должен иметь вид
public int this[int i, int j]
Операции класса
C# позволяет переопределить действие большинства операций так, чтобы при использовании с объектами конкретного класса они выполняли заданные функции. Это дает возможность применять экземпляры собственных типов данных в составе выражений таким же образом, как стандартных, например:
MyObject a, b, с;
с = а + b; // используется операция сложения для класса MyObject
Определение собственных операций класса часто называют перегрузкой операций. Перегрузка обычно применяется для классов, описывающих математические или физические понятия, то есть таких классов, для которых семантика операций делает программу более понятной. Если назначение операции интуитивно не понятно с первого взгляда, перегружать такую операцию не рекомендуется. Операции класса описываются с помощью методов специального вида {функций-операций). Перегрузка операций похожа на перегрузку обычных методов. Синтаксис операции:
[ атрибуты ] спецификаторы объявитель_операции тело
Атрибуты рассматриваются в главе 12, в качестве спецификаторов одновременно используются ключевые слова public и static. Кроме того, операцию можно объявить как внешнюю (extern).
Объявитель операции содержит ключевое слово operator, по которому и опозна-ется описание операции в классе. Тело операции определяет действия, которые выполняются при использовании операции в выражении. Тело представляет собой блок, аналогичный телу других методов.
|
|
ВНИМАНИЕ
Новые обозначения для собственных операций вводить нельзя. Для операций класса сохраняются количество аргументов, приоритеты операций и правила ассоциации (справа налево или слева направо), используемые в стандартных типах данных.
При описании операций необходимо соблюдать следующие правила:
□ операция должна быть описана как открытый статический метод класса (спецификаторы public static);
□ параметры в операцию должны передаваться по значению (то есть не должны
предваряться ключевыми словами ref или out);
□ сигнатуры всех операций класса должны различаться;
□ типы, используемые в операции, должны иметь не меньшие права доступа, чем
сама операция (то есть должны быть доступны при использовании операции).
В С# существуют три вида операций класса: унарные, бинарные и операции преобразования типа.
Унарные операции
Можно определять в классе следующие унарные операции: + -! ~ ++ -- true false
Синтаксис объявителя унарной операции:
тип operator унарная_операция (параметр)
Примеры заголовков унарных операций:
public static int operator +(MyObject m)
public static MyObject operator --(MyObject m)
public static bool operator true(MyObject m)
Параметр, передаваемый в операцию, должен иметь тип класса, для которого она определяется. Операция должна возвращать:
□ для операций +, -,! и ~ величину любого типа;
□ для операций ++ и - - величину типа класса, для которого она определяется;
□ для операций true и false величину типа bool.
Операции не должны изменять значение передаваемого им операнда. Операция, возвращающая величину типа класса, для которого она определяется, должна создать новый объект этого класса, выполнить с ним необходимые действия и передать его в качестве результата.
ПРИМЕЧАНИЕ
Префиксный и постфиксный инкременты не различаются (для них может существовать только одна реализация, которая вызывается в обоих случаях).
ПРИМЕЧАНИЕ
|
|
Операции true и false обычно определяются для логических типов SQL, обладающих неопределенным состоянием, и не входят в число тем, рассматриваемых в этой книге.
В качестве примера усовершенствуем приведенный в листинге 7.3 класс SafeArray для удобной и безопасной работы с массивом. В класс внесены следующие изменения:
□ добавлен конструктор, позволяющий инициализировать массив обычным мас
сивом или серией целочисленных значений произвольного размера;
□ добавлена операция инкремента;
□ добавлен вспомогательный метод Print вывода массива;
□ изменена стратегия обработки ошибок выхода за границы массива;
□ снято требование, чтобы элементы массива принимали значения в заданном
диапазоне.
Текст программы приведен в листинге 7.5.
Листинг 7.5. Определение операции инкремента для класса SafeArray
using System;
namespace ConsoleApplication1
{
class SafeArray
{
public SafeArray(int size) // конструктор
{
a = new int[size]; length = size;
}
public SafeArray(params int[] arr) // новый конструктор
{
length = аrr.Length;
a = new int[length];
for (int i = 0; i < length; ++i) a[i] = arr[i];
}
public static SafeArray operator ++(SafeArray x) // ++
{
SafeArray temp = new SafeArray(x.length);
for (int i = 0; i < x.length; ++i)
temp[i] = ++x.a[i];
return temp;
}
public int this[int i] // индексатор
{
get
{
if (i >= 0 && i < length)
return a[i];
else
throw new IndexOutOfRangeException(); // исключение
}
set
{
if (i >= 0 && i < length)
a[i] = value;
else
throw new IndexOutOfRangeException(); // исключение
}
}
public void Print(string name) // вывод на экран
{
Console.WriteLine(name + ":");
for (int i = 0; i < length; ++i)
Console.Write("\t" + a[i]); Console.WriteLine();
}
int[] a; // закрытый массив
int length; // закрытая размерность
}
class Class1
{
static void Main()
{
try
{
SafeArray a1 = new SafeArray(5, 2, -1, 1, -2);
al.Print("Массив 1");
a1++;
a1.Print("Инкремент массива 1");
}
catch (Exception e) // обработка исключения
{
Console.WriteLine(e.Message);
}
}
}
}
Бинарные операции
Можно определять в классе следующие бинарные операции:
+ - * / % & | ^ << >> ==!= > < >= <=
ВНИМАНИЕ
Операций присваивания в этом списке нет.
Синтаксис объявителя бинарной операции:
тип operator бинарная_операция (параметр1, параметр2)
Примеры заголовков бинарных операций:
public static MyObject operator + (MyObject ml, MyObject m2)
public static bool operator == (MyObject m1, MyObject m2)
Хотя бы один параметр, передаваемый в операцию, должен иметь тип класса, для которого она определяется. Операция может возвращать величину любого типа.
Операции == и! =, > и <, >= и <= определяются только парами и обычно возвращают логическое значение. Чаще всего в классе определяют операции сравнения на равенство и неравенство для того, чтобы обеспечить сравнение объектов, а не их ссылок, как определено по умолчанию для ссылочных типов. Перегрузка операций отношения требует знания интерфейсов, поэтому она рассматривается позже, в главе 9 (см. с. 203).
Пример определения операции сложения для класса SafeArray, описанного в предыдущем разделе, приведен в листинге 7.6. В зависимости от операндов операция либо выполняет поэлементное сложение двух массивов, либо прибавляет значение операнда к каждому элементу массива.
Листинг 7.6. Определение операции сложения для класса SafeArray
using System;
namespace ConsoleApplication1
{
class SafeArray
{
public SafeArray(int size)
{
a = new int[size]; length = size;
}
public SafeArray(params int[] arr)
{
length = arr.Length; a = new int[length];
for (int i = 0; i < length; ++i) a[i] = arr[i];
}
public static SafeArray operator +(SafeArray x, SafeArray у) // +
{
int len = x.length < y.length? x.length: y.length;
SafeArray temp = new SafeArray(len);
for (int i = 0; i < len; ++i) temp[i] = x[i] + y[i]; return temp;
}
public static SafeArray operator +(SafeArray x, int у) // +
{
SafeArray temp = new SafeArray(x.length);
for (int i = 0; i < x.length; ++i)
temp[i] = x[i] + y;
return temp;
}
public static SafeArray operator +(int x, SafeArray у) // +
{
SafeArray temp = new SafeArray(y.length);
for (int i = 0; i < y.length; ++i) temp[i] = x + y[i];
return temp;
}
public static SafeArray operator ++(SafeArray x) // ++
{
SafeArray temp = new SafeArray(x.length);
for (int i = 0; i < x.length; ++i) temp[i] = ++x.a[i];
return temp;
}
public int this[int i] // []
{
get
{
if (i >= 0 && i < length) return a[i];
else throw new IndexOutOfRangeException();
}
set
{
if (i >= 0 && i < length)
a[i] = value;
else throw new IndexOutOfRangeException();
}
}
public void Print(string name)
{
Console.WriteLine(name + ":");
for (int i = 0; i < length; ++i) Console.Write("\t" + a[i]);
Console.WriteLine();
}
int[] a; // закрытый массив
int length; // закрытая размерность
}
class Class1
{
static void Main()
{
try
{
SafeArray al = new SafeArray(5, 2, -1, 1, -2);
al.Print("Массив 1");
SafeArray a2 = new SafeArray(1, 0, 3);
a2.Print("Массив 2");
SafeArray a3 = al + a2;
a3.Print("Сумма массивов 1 и 2");
al = al + 100; // 1
al.Print("Массив 1 + 100");
a1 = 100 + a1; // 2
a1.Print("100 + массив 1");
a2 += ++a2 + 1; // 3 оторвать руки!
a2.Print("++a2, a2 + a2 + 1");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
}
Результат работы программы:
Массив 1:
5 2 -1 1 -2
Массив 2:
1 0 3
Сумма массивов 1 и 2:
7 3 3
Массив 1+100:»
106 103 100 102 99
100 + массив1:
206 203 200 202 199
++a2, а2 + а2 + 1:
5 3 9
Обратите внимание: чтобы обеспечить возможность сложения с константой, операция сложения перегружена два раза для случаев, когда константа является первым и вторым операндом (операторы 2 и 1).
Сложную операцию присваивания += (оператор 3) определять не требуется, да это и невозможно. При ее выполнении автоматически вызываются сначала операция сложения, а потом присваивания. В целом же оператор 3 демонстрирует недопустимую манеру программирования, поскольку результат его выполнения неочевиден.
ПРИМЕЧАНИЕ
В перегруженных методах для объектов применяется индексатор. Для повышения эффективности можно обратиться к закрытому полю-массиву и непосредственно, например: temp.a[i] = x + y.a[i].
Операции преобразования типа
Операции преобразования типа обеспечивают возможность явного и неявного преобразования между пользовательскими типами данных. Синтаксис объявителя операции преобразования типа:
implicit operator тип (параметр) // неявное преобразование
explicit operator тип (параметр) // явное преобразование
Эти операции выполняют преобразование из типа параметра в тип, указанный в заголовке операции. Одним из этих типов должен быть класс, для которого определяется операция. Таким образом, операции выполняют преобразование либо типа класса к другому типу, либо наоборот. Преобразуемые типы не должны быть связаны отношениями наследования (следовательно, нельзя определять преобразования к типу object и наоборот, впрочем, они уже определены без нашего участия). Примеры операций преобразования типа для класса Monster, описанного в главе 5:
public static implicit operator int(Monster m)
{
return m.health:
}
public static explicit operator Monster(int h)
{
return new Monster(h, 100, "Fromlnt"):
}
Ниже приведены примеры использования этих преобразований в программе. Не
надо искать в них смысл, они просто иллюстрируют синтаксис:
Monster Masha = new Monster(200, 200. "Masha");
int i = Masha; // неявное преобразование
Masha = (Monster) 500; // явное преобразование
Неявное преобразование выполняется автоматически:
□ при присваивании объекта переменной целевого типа, как в примере;
□ при использовании объекта в выражении, содержащем переменные целевого типа;
□ при передаче объекта в метод на место параметра целевого типа;
□ при явном приведении типа.
Явное преобразование выполняется при использовании операции приведения типа. Все операции класса должны иметь разные сигнатуры. В отличие от других видов методов, для операций преобразования тип возвращаемого значения включается в сигнатуру, иначе нельзя было бы определять варианты преобразования данного типа в несколько других. Ключевые слова implicit и explicit в сигнатуру не включаются, следовательно, для одного и того же преобразования нельзя определить одновременно явную и неявную версии.
Неявное преобразование следует определять так, чтобы при его выполнении не возникала потеря точности и не генерировались исключения. Если эти ситуации возможны, преобразование следует описать как явное.
Деструкторы
В С# существует специальный вид метода, называемый деструктором. Он вызывается сборщиком мусора непосредственно перед удалением объекта из памяти. В деструкторе описываются действия, гарантирующие корректность последующего удаления объекта, например, проверяется, все ли ресурсы, используемые объектом, освобождены (файлы закрыты, удаленное соединение разорвано и т. п.). Синтаксис деструктора:
[ атрибуты ] [ extern ] ~имя_класса() тело
Как видно из определения, деструктор не имеет параметров, не возвращает значения и не требует указания спецификаторов доступа. Его имя совпадает с именем класса и предваряется тильдой (~), символизирующей обратные по отношению к конструктору действия. Тело деструктора представляет собой блок или просто точку с запятой, если деструктор определен как внешний (extern). Сборщик мусора удаляет объекты, на которые нет ссылок. Он работает в соответствии со своей внутренней стратегией в неизвестные для программиста моменты времени. Поскольку деструктор вызывается сборщиком мусора, невозможно гарантировать, что деструктор будет обязательно вызван в процессе работы программы. Следовательно, его лучше использовать только для гарантии освобождения ресурсов, а «штатное» освобождение выполнять в другом месте программы.
Применение деструкторов замедляет процесс сборки мусора.