Индексаторы

Индексатор представляет собой разновидность свойства. Если у класса есть скрытое поле, представляющее собой массив, то с помощью индексатора можно обратиться к элементу этого массива, используя имя объекта и номер элемента массива в квадратных скобках. Иными словами, индексатор — это такой «ум­ный» индекс для объектов.

Синтаксис индексатора аналогичен синтаксису свойства:

[атрибуты] [спецификаторы] тип 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). Сборщик мусора удаляет объекты, на которые нет ссылок. Он работает в соот­ветствии со своей внутренней стратегией в неизвестные для программиста моменты времени. Поскольку деструктор вызывается сборщиком мусора, не­возможно гарантировать, что деструктор будет обязательно вызван в процессе работы программы. Следовательно, его лучше использовать только для гарантии освобождения ресурсов, а «штатное» освобождение выполнять в другом месте программы.

Применение деструкторов замедляет процесс сборки мусора.


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: