Операции checked и unchecked 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Операции checked и unchecked



Операции и приведения

Операции в C#

Идея эквивалентности при работе со ссылками и типами значений

Преобразование данных между примитивными типами

Преобразование типов значений в ссылочные типы посредством

Упаковки

Преобразования ссылочных типов посредством приведения

Перегрузка стандартных операций для пользовательских типов

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

 

В предыдущих главах была представлена большая часть материала, необходимого для того, чтобы приступить к написанию полезных программ на С#. В настоящей главе дискус­сия о важнейших элементах языка завершается и иллюстрируется ряд наиболее мощных аспектов С#, которые позволяют расширять возможности языка.

Операции

Хотя большинство операций C# должны быть знакомы разработчикам на С и C++, в на­стоящем разделе мы представим наиболее важные из них - большей частью для программистов-новичков и тех, кто переходит на C# из Visual Basic. Кроме того, мы прольем свет, на некоторые отличия в синтаксисе и семантике операций, присущие С#.

В языке C# поддерживаются операции, перечисленные в табл. 7.1.

Таблица 7.1. Операции в С#

Категория Операции
Арифметические + - * / %
Логические & | ^ ~ && ||!
Конкатенация строк +
Инкремент и декремент ++ --
Битовый сдвиг << >>
Сравнение ==!= < > <= >=
Присваивание = += -= *= /= %= &= |= ^= <<= >>=
Доступ к члену (для объектов и структур) .
Индексация (для массивов и индексов) []
Приведение ()
Условная (тернарная операция) ?:
Делегатная конкатенация и удаление (см. раздел 6.8) + -

Таблица 7.1. Операции в С#

Категория Операции
Создание объектов new
Информация о типе sizeof is typeof as
Контроль исключения, связанного с переполнением checked unchecked
Косвенное обращение и адрес []
Квалификатор псевдонима пространства имен ::
Операция поглощения null ??

 

Обратите внимание, что четыре специфических операции (sizeof, *, -> и &, описан­ные в табл. 7.2) доступны только в небезопасном коде (коде, минующем контроль безопас­ности типов С#), который мы рассмотрим в главе 13. Важно также отметить, что ключе­вые слова операции sizeof, используемые в ранних версиях.NET Framework 1.0 и 1.1, требовали небезопасного режима. Начиная с версии.NET Framework 2.0, это требование отменено.

Таблица 7.2. Операции, доступные только в небезопасном режиме

Категория Операции
Ключевые слова операции sizeof (только для версий.NET Framework 1.0 и 1.1)
Логические & | ^ ~ && ||!

 

Одной из основных ловушек, которых нужно остерегаться при использовании опера­ций С#, является то, что, как и во всех С-подобных языках, в C# применяются разные опе­рации для присваивания (=) и сравнения (==). Например, следующий оператор означает пусть х будет, равно 3:

х = 3;

Теперь, если вы хотите сравнить х со значением, то должны использовать двойной знак равенства ==:

if (х ==3)

{

}

Ксчастью, строгие правила безопасности типов C# предотвращают очень частую ошиб­ку в С, когда вместо сравнения в логическом операторе используется присваивание. Это значит, что следующий оператор вызовет ошибку компиляции:

if (х = 3)

{

}

Программисты на Visual Basic, привыкшие использовать амперсанд (&) для конкатена­ции строк, должны быть внимательны. В C# для этого служит знак плюс (+), в то время как & — битовая операция “И”, применяемая к двум целым числам. | позволяет выполнить битовое “ИЛИ” между двумя целыми. Программистам на Visual Basic также не знакома арифметическая операция взятия модуля (%). Она возвращает остаток от целочисленного деления. То есть, например, если х равно 7, то х%5 вернет 2.

Вы также будете использовать немного указателей в С#, а вместе с ними - немного опе­раций косвенного обращения. Точнее говоря, единственное место, где они используются - это блоки небезопасного кода, поскольку только в них разрешены указатели С#. Указатели и небезопасный код обсуждаются в разделе 6.13.

Сокращения операций

В табл. 7.3 представлен полный список сокращенных операций присваивания, доступ­ных в С#.

Таблица 7.3. Сокращенные операции присваивания

Сокращенная операция Эквивалент
x++, ++x x = x + 1
x--, --x x = x – 1
x += y x = x + y
x -= y x = x - y
x *= y x = x * y
x /= y x = x / y
x %= y x = x % y
x >>= y x = x >> y
x <<= y x = x << y
x &= y x = x & y
x |= y x = x | y

 

Возможно, вас удивит, почему здесь показано по два примера операций инкремента ++ и декремента --. Дело в том, что размещение операции перед выражением известно как пре­фиксная форма, а после выражения - как постфиксная форма, и их поведение различается.

Операции инкремента и декремента могут служить полными выражениями и ис­пользоваться внутри других выражений. Когда они применяются сами по себе, эф­фект от постфиксной и префиксной версий одинаков и соответствует конструкции х = х + 1 (для ++). Когда же они участвуют в более крупных выражениях, то префикс­ная операция увеличит значение х перед вычислением выражения; другими словами, значение х увеличится, и в выражении будет использовано это новое значение. В про­тивоположность этому постфиксная операция увеличивает значение х после вычис­ления выражения - т.е. в выражении будет участвовать первоначальное значение х. В следующем примере используется инкрементная операция для демонстрации разницы между поведением префиксного и постфиксного ее вариантов:

int х = 5;

if (++х == 6) //true - х увеличивается до 6 перед проверкой условия {

Console.WriteLine("Это выполнится");

}

if (х++ == 7) //false - х увеличивается до 7 после проверки условия

{

Console.WriteLine("А это - нет");

}

Условие в первом операторе if вычисляется как true, потому что х увеличивается от 5 до 6 перед тем, как будет вычислено все выражение. Условие во втором операторе if, од­нако, дает false, поскольку х увеличивается до 7 только после того, как будет вычислено все выражение (пока х == 6).

Поведение префиксной и постфиксной форм --х и х-- аналогично, но при этом опе­ранд уменьшается, а не увеличивается.

Другие сокращенные операции, такие как += и -=, требуют двух операндов и применяют­ся для модификации значения первого из них, выполняя арифметическое, логическое или битовое действие над ним. Например, приведенные ниже две строки кода эквивалентны:

х += 5; х = х + 5;

В последующих разделах рассматриваются некоторые первичные операции и операции приведения, которые будут часто использоваться в коде С#.

Условная операция

Условная операция (?:), также известная, как тернарная операция - это сокращенная форма конструкции if...else. Название отражает тот факт, что она работает с тремя операндами. Эта операция вычисляет выражение и возвращает одно из значений, если выражение истинно, и другое - если выражение ложно.

Синтаксис выглядит следующим образом:

условие? true_значение: false_значение

Здесь условие - булевское выражение, которое должно быть вычислено, true_знaче- ние - значение, которое возвращается, если условие истинно, а fаlsе_значение - значе­ние, которое возвращается, если условие ложно.

При аккуратном использовании условная операция может придать удивительную крат­кость разрабатываемым программам. Особенно удобно это бывает для передачи одного из нескольких аргументов при вызове функции. Вы можете использовать его для быстрого преобразования булевского выражения в строковое значение true или false. Также он удобен для отображения единственной или множественной формы существительного, на­пример:

int х = l;

string s = х + " ";

s += (x == 1? "man": "men");

Console.WriteLine(s);

Этот код отображает 1 man, если x равно 1, но отобразит корректную множественную форму английского языка для любого другого числа. Однако обратите внимание, что если вывод программы должен быть локализован для разных языков, то придется написать более сложные процедуры, принимающие во внимание грамматические правила этих языков.

Операция is

Операция is позволяет проверить, совместим ли объект с определенным типом. В дан­ном случае “совместимость” означает, что объект либо имеет этот тип, либо наследуется от этого типа. Например, для проверки, совместима ли переменная с типом object, исполь­зуется приведенный ниже код:

int i = 10;

if (i is object)

{

Console.WriteLine(”i является object");

}

Как и все типы С#, int унаследован от object, поэтому выражение i is object вычис­ляется в данном случае как true, и сообщение будет отображено.

Операция as

Операция as применяется для выполнения явного преобразования типа ссылочных пе­ременных. Если преобразуемый тип совместим с указанным, преобразование проходит ус­пешно. Однако если типы несовместимы, то операция as возвращает значение null. Как показано в следующем коде, попытка преобразовать ссылку object в string возвращает null, если только ссылка object на самом деле не ссылается на экземпляр string:

object o1 = "Некоторая строка";

object о2 = 5;

string s1 = o1 as string; // s1 = "Некоторая строка"

string s2 = o2 as string; // s2 = null

Операция as позволяет выполнить безопасное преобразование типа за один шаг, без необходимости первоначальной проверки его на совместимость с помощью операции is до собственно преобразования.

Операция sizeof

Чтобы определить место (в байтах), требуемое для размещения в стеке типа значения, необходимо воспользоваться операцией sizeof:

Console.WriteLine(sizeof(int));

Этот код отобразит число 4, потому что int занимает в памяти 4 байта.

Если операция sizeof применяется со сложными (а не с примитивными) типами, код понадобится заключить в блок unsafe, как показано ниже:

unsafe

{

Console.WriteLine(sizeof(int));

}

Небезопасный код подробно рассматривается в разделе 6.13.

Операция typeof

Операция typeof возвращает объект System.Туре, представляющий указанный тип. Например, typeof (string) вернет объект Туре, представляющий тип System.String. Это удобно, если необходимо использовать рефлексию, чтобы динамически получить ин­формацию об объекте. Рефлексия рассматривается в разделе 6.14.

Операция поглощения null

Операция поглощения null (??) представляет собой сокращенный механизм, обеспе­чивающий возможность работы с null-значениями в выражениях, включающих ссылоч­ные типы и типы, которые допускают null. Операция размещается между двумя операн­дами — первый должен иметь тип, допускающий null-значения, или ссылочный, а второй должен быть того же типа, что и первый, либо типа, неявно преобразуемого к типу перво­го операнда.

Операция поглощения null работает следующим образом:

- если первый операнд не равен null, то все выражение принимает значение перво­го операнда;

- если первый операнд равен null, все выражение принимает значение второго опе­ранда.

Например:

int? а = null;

int b;

b = a?? 10; // b имеет значение 10

a = 3;

b = a?? 10; //b имеет значение 3

Если второй операнд не может быть неявно преобразован к типу первого операнда, генерируется ошибка компиляции.

Приоритеты операций

В табл. 7.4 представлены приоритеты операций С#. Операции, указанные в верхней части таблицы, имеют наивысший приоритет (т.е. вычисляются первыми в выражении, содержащем множество операций).

Таблица 7.4. Приоритеты операций C#

Группа Операции
Первичные ().[] x++ x-- new typeof sizeof checked unchecked
Унарные + -! ~ ++x -x приведения
Умножения/деления / * %
Сложения/вычитания + -
Побитового сдвига << >>
Отношения <= >= <> is as
Сравнения ==!=
Битовое «И» &
Битовое исключающее «ИЛИ» ^
Битовое «ИЛИ» |
Логическое «И» &&
Логическое «ИЛИ» ||
Тернарная операция ?:
Присваивания = += -= *=.= %= &= |= ^= <<= >>= >>>=

 

В сложных выражениях для получения корректного результата не следует слишком полагаться на приоритеты операций. Применение скобок для указания порядка вычисления операций проясняет код и позволяет избежать возможной путаницы.

Безопасность типов

В разделе 6.1 отмечалось, что промежуточный язык (IL) поддерживает строгую безопас­ность типов в коде. Строгая типизация обеспечивает работу многих служб, представляе­мых.NET, включая безопасность и межъязыковое взаимодействие. Как и следовало ожи­дать от языка, компилируемого в IL, C# также является строго типизированным. Помимо прочего, это означает, что типы данных не всегда являются гладко взаимозаменяемыми. В этом разделе речь пойдет о преобразованиях между примитивными типами.

Язык C# также поддерживает преобразование между разными ссылочными ти­пами и всегда позволяет определить, как создаваемые вами типы данных ведут себя при преобразовании в другие типы и обратно. Обе эти темы обсуждаются далее в настоящей главе. С другой стороны, обобщения (generics) позволяют избе­жать некоторых наиболее часто встречающихся ситуаций, когда может пона­добиться преобразование типов. Подробности ищите.в главах 5 и 10.

Преобразования типов

Часто возникает необходимость в преобразовании данных из одного типа в другой. Рассмотрим следующий код:

byte value1 = 10;

byte value2 = 23;

byte total;

total = valuel + value2;

Console.WriteLine(total);

Если вы попытаетесь скомпилировать эти строки, то получите сообщение об ошибке:

Cannot implicitly convert type ’int' to 'byte'

Неявное преобразование типа 'int' в ’byte' невозможно

Проблема здесь в том, что если складывать два байта, результат будет возвращен как int, а не другой byte. Это потому, что byte может содержать только восемь бит данных, поэтому сложение двух байт очень легко может дать результат, который не уместится в один тип byte. Если результат необходимо сохранить в переменной типа byte, его потребуется преобразовать в byte. Последующие разделы посвящены двум механизмам такого преобра­зования, поддерживаемым в C# - явному (explicit) и неявному (implicit) преобразованию.

Неявные преобразования

Преобразования типов обычно могут быть выполнены автоматически (неявно) толь­ко в том случае, когда при этом можно гарантировать, что значение не будет изменено никоим образом. Именно по этой причине не работает предыдущий пример; пытаясь пре­образовать int в byte, потенциально можно потерять 3 байта данных. Компилятор не по­зволит это сделать, если только явно не сообщить ему о том, что именно это и требуется. Однако если поместить результат в переменную long вместо byte, то проблем не будет:

byte valuel = 10;

byte value2 = 23;

long total; // это скомпилируется нормально

total = valuel + value2;

 

Console.WriteLine(total);

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

В табл. 7.5 показаны неявные преобразования типов, поддерживаемые С#.

Таблица 7.5. Неявные пребразования типов, поддерживаемые в C#

Из В
sbyte short, int, long, float, double, decimal, BigInteger
byte short, ushort, int, uint, long, ulong, float, double, decimal, BigInteger
short int, long, float, double, decimal, BigInteger
ushort int, uint, long, ulong, double, decimal, BigInteger
int long, float, double, decimal, BigInteger
uint long, ulong, float, double, decimal, BigInteger
long, ulong double, decimal, BigInteger
float double, BigInteger
char ushort, int, uint, long, ulong, floar, double, decimal, BigInteger

 

Как и следовало ожидать, неявные преобразования допускаются только от меньших це­лых типов к большим, но не наоборот. Можно также преобразовывать данные между целы­ми и действительными значениями, однако здесь правила слегка отличаются. Допускается преобразовывать данные между типами одинакового размера, например, из int/uint в float и long/ulong - в double, однако можно также выполнять преобразование long/ulong в float. При этом может быть потеряно 4 байта, но это означает только, что полу­ченное значение float будет менее точным, чем double; компилятор не трактует это как ошибку, потому что модуль значения не изменяется. Можно также присваивать значение переменной типа без знака переменной типа со знаком, если значение первой окажется в пределах допустимых значений второй.

- Относительно преобразования типов, допускающих null-значения, приняты описан­ные ниже дополнительные соглашения.

- Допускающие null типы неявно преобразуются в другие типы, допускающие null, подчиняясь правилам, описанным в табл. 7.5; т.е. int? неявно преобразуется в long?, float?, double? и decimal?.

- Не допускающие null типы неявно преобразуются в типы, допускающие null, со­гласно тем же правилам. То есть int неявно преобразуется в long, float, double и decimal.

- Допускающие null типы не могут быть неявно преобразованы в типы, не допускаю­щие null; для этого должно выполняться явное преобразование, как описано в сле­дующем разделе. Дело в том, что переменные этих типов могут содержать значение null, которое невозможно представить типом, не допускающим null.

Явные преобразования

Многие преобразования типов не могут быть выполнены явно, и компилятор выдаст ошибку при попытке сделать это. Ниже перечислены некоторые из них:

- int в short - возможна потеря данных;

- int в uint - возможна потеря данных;

- uint в int - возможна потеря данных;

- float в int - теряется все, что находится после десятичной запятой;

- любой числовой тип в char - возможна потеря данных;

- decimal в любой числовой тип - потому что тип decimal внутренне устроен иначе, чем целые числа и числа с плавающей точкой;

- int? в int - тип, допускающий null, может иметь значение null.

Однако такие преобразования можно выполнить принудительно, используя приведения (cast). Когда вы приводите один тип к другому, то сознательно заставляете компилятор вы­полнить преобразование. Приведение типов выглядит следующим образом:

long val = 30000;

int i = (int)val; // Корректное приведение.

// Максимальное значение int равно 2147483647.

Тип, к которому выполняется приведение, помещается в круглые скобки непосредствен­но перед преобразуемым значением. Если вы ранее имели дело с языком С, то подобное приведение должно быть знакомо. Если вы работали со специальными ключевыми словами приведения типов C++, такими как static_cast, то имейте в виду, что в C# ничего подоб­ного нет, и надо использовать старый синтаксис приведения С.

Приведение может оказаться опасной операцией. Даже простейшее приведение longк intможет вызвать проблемы, когда исходное значение longбольше, чем максимально допустимое значение int:

long val = 3000000000;

int i = (int)val; // Неверное приведение.

// Максимальное значение int равно 2147483647.

В этом случае вы не получите ошибки, но также не получите и ожидаемого результата. Если запустить этот код и вывести на экран значение i, то получится вот что:

-1294967296

Всегда имеет смысл предполагать, что явное приведение не даст ожидаемого резуль­тата. Как вы уже видели, C# предлагает операцию checked, которую можно использовать для проверки того, вызывает ли действие арифметическое переполнение. С помощью этой операции можно проверить, безопасно ли приведение, и если нет — заставить исполняю­щую среду сгенерировать исключение:

long val = 3000000000;

int i = checked ((int)val);

Памятуя о том, что явные приведения типов потенциально опасны, вы должны поза­ботиться о включении в свое приложение кода, обрабатывающего возможные сбои таких приведений. Раздел 6.15 посвящена описанию обработки структурированных исключений с применением операторов try и catch. Используя приведения, можно преобразовывать большинство примитивных типов друг в друга; например, в следующем коде 0.5 добавля­ется к price, а результат приводится к int:

double price = 25.30;

int approximatePrice = (int)(price + 0.5);

Это даст цену, округленную до ближайшего доллара. Однако при таком преобразовании теряются данные - именно те, что следуют после десятичной запятой. Поэтому подобное преобразование никогда не должно применяться, если нужно выполнять дальнейшие вы­числения с модифицированным значением цены. Тем не менее, это удобно, когда необхо­димо вывести примерное значение завершенных либо частично завершенных вычислений, не утомляя пользователя множеством цифр после запятой.

В следующем примере показано, что произойдет при преобразовании беззнакового це­лого в char:

ushort с = 43;

char symbol = (char) с;

Console.WriteLine(symbol);

На экран будет выведен символ с ASCII-кодом 43 - т.е. знак +. Таким образом, вы мо­жете попробовать выполнить любое, какое захотите, преобразование, между числовыми типами (включая char), и оно будет работать (например, decimal в char или наоборот). Преобразование между типами значений не ограничено отдельными переменными, как мы видели до сих пор. Можно также преобразовывать элемент массива типа double в переменную-член структуры типа int:

struct ItemDetails

{

public string Description;

public int ApproxPrice;

}

// ...

double [] Prices = { 25.30, 26.20, 27.40, 30.00 };

ItemDetails id;

id.Description = "Что-то";

id.ApproxPrice = (int) (Prices [0] +0.5);

Чтобы преобразовать тип, допускающий null, к типу, null не допускающему, или же в другой тип, допускающий null, когда при этом может происходить потеря данных, следует применять явное приведение. Важно отметить, что это истинно даже при преобразовании данных между элементами с одинаковым типом, лежащим в основе, например, из int? в int или из float? в float. Это потому, что допускающий null тип может Содержать зна­чение null, которое невозможно представить типом, такого значения не допускающим. Там, где возможно явное приведение между двумя эквивалентными, не допускающими null типами, возможно также явное приведение между двумя аналогичными типами, до­пускающими null. Однако если выполняется приведение допускающего null типа в тип, не допускающий null, и при этом первая переменная имеет значение null, то генериру­ется исключение InvalidOperationException, например:

int? а = null;

int b = (int)а; // Сгенерирует исключение

Используя явное приведение и проявив немного осторожности и внимания, мож­но преобразовать любой экземпляр простого типа значений в почти любой другой тип. Однако существуют и ограничения - поскольку предполагается применение типов значе­ний, между собой можно преобразовывать числовые типы, тип char и типы перечисле­ний. Приводить булевский тип в любой другой или наоборот нельзя.

Для выполнения преобразований между числами и строками в библиотеке классов.NET предусмотрены специальные методы. Класс Object реализует возвращающий строку метод ToString(), который переопределен во всех базовых типах.NET:

int i = 10;

string s = i.ToString();

Аналогично, если необходимо разобрать строку и извлечь из нее числовое или булев­ское значение, можно воспользоваться методом Parse(), который поддерживают все пре­допределенные типы значений:

string s = "100";

int i = int.Parse (s);

Console.WriteLine (i + 50); // Добавляем 50, чтобы доказать,

// что это действительно int.

Следует отметить, что Parse() регистрирует ошибку, генерируя исключение, если ока­зывается не в состоянии преобразовать строку (например, если вы попытаетесь преобра­зовать строку Hello в целое число). Исключения описаны в разделе 6.15.

Упаковка и распаковка

В разделе 6.2 вы узнали, что все типы - простые предопределенные вроде int и char, а также сложные наподобие классов и структур - наследуются от типа object. Это значит, что даже с литералами можно обращаться как с объектами:

string s = 10.ToString();

Однако вы также видели, что типы данных C# подразделяются на типы значений, кото­рые размещаются в стеке, и ссылочные типы, размещаемые в куче. Как объяснить возмож­ность вызова методов для int, если int - не более чем четырехбайтовое значение в стеке?

Способ, который использует C# для достижения такого эффекта, называется упаковкой (boxing); Упаковка и ее противоположность - распаковка (unboxing) - позволяют преоб­разовывать типы значений в ссылочные типы и затем обратно в типы значений. Эта тема была включена в раздел, посвященный приведению, потому, что, по сути, при этом выпол­няется приведение значения к типу object. Упаковка - термин, применяемый для описа­ния трансформации типа значения в ссылочный тип. В основном, при этом исполняющая среда создает временный “ящик” типа ссылки на объект в куче.

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

int myIntNumber = 20;

object myObject = myIntNumber;

Распаковка - это термин, описывающий обратный процесс, когда ранее упакованная величина приводится обратно к типу значений. Здесь используется термин приведение, по­скольку это должно быть сделано явно. Используемый синтаксис подобен тому, что приме­няется при явном приведении типов, описанном выше:

int myIntNumber = 20;

object myObject = mylntNumber; // Упаковать int

int mySecondNumber = (int)myObject; // Распаковать обратно в int

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

Одно важное предостережение: при распаковке следует соблюдать осторожность, что­бы принимающая переменная имела в себе достаточно места для сохранения всех байтов распаковываемого значения. Например, int в C# имеет длину только 32 бита, а потому распаковка значения long (длиной 64 бита) в int, как показано ниже, приведет к генера­ции исключения InvalidCastException:

long myLongNumber = 333333423;

object myObject = (object)myLongNumber;

int mylntNumber = (int)myObject;

Проверка равенства объектов

После того, как мы обсудили операции в целом и кратко коснулись операции равенства в частности, стоит задуматься - что может означать равенство, когда речь идет о классах и структурах? Понимание механики сравнения объектов существенно для программиро­вания логических выражений, а также важно для реализации перегрузок и приведений. Это - тема, на которой будет сосредоточено внимание в оставшейся части главы.

Механизмы сравнения объектов различаются в зависимости от того, сравниваются ли ссылочные типы (экземпляры классов) или же типы значений (примитивные типы дан­ных, экземпляры структур или перечислений). В последующих разделах мы независимо рассмотрим сравнение ссылочных типов и типов значений.

Метод ReferenceEquals()

ReferenceEquals() - статический метод, проверяющий, указывают ли две ссылки на один и тот же экземпляр класса; точнее - содержат ли две ссылки один и тот адрес памяти. Поскольку это статический метод, который невозможно переопределить, его реализация из System.Object - это то, что доступно всегда. ReferenceEquals() всегда будет возвра­щать true, когда ему передаются две ссылки, указывающие на один и тот же экземпляр объ­екта, и false - в противном случае. Однако две ссылки на null трактуются как равные:

SomeClass х, у;

х = new SomeClass();

у = new SomeClass();

bool В1 = ReferenceEquals(null,null); //возвращает true

bool B2 = ReferenceEquals(null,x); //возвращает false

bool B3 = ReferenceEquals (x, у); //возвращает false, потому что

// x и у указывают на разные объекты

Виртуальный метод Equals()

Реализация System.Object виртуальной версии Equals() также сравнивает ссылки. Однако, поскольку этот метод виртуальный, его можно переопределять в собственных классах, чтобы сравнивать объекты по значению. В частности, если вы намерены исполь­зовать экземпляры своего класса в качестве ключей словаря, то должны переопределить этот метод для сравнения значений. В противном случае, в зависимости от того, как вы пе­реопределите

Object.HashCode(), класс словаря, содержащий объекты, может либо вооб­ще не работать, либо работать крайне неэффективно. Об одном моменте следует помнить, переопределяя Equals(): ваша версия этого метода не должна генерировать исключения. В противном случае это может стать причиной проблем для классов словарей, а также, воз­можно, и для других базовых классов.NET, которые внутренне вызывают этот метод.

Статический метод Equals()

Статическая версия Equals() в действительности делает то же самое, что и виртуаль­ная версия уровня экземпляра. Разница в том, что статическая версия принимает два пара­метра и проверяет их на предмет равенства. Этот метод справляется с ситуацией, когда обе ссылки равны null, а потому обеспечивает дополнительную защиту от генерации исключе­ний, когда существует риск, что оба объекта окажутся равными null. Статическая версия первым делом проверяет переданные ей ссылки на предмет равенства их null. Если они обе равны null, возвращается true (поскольку ссылка null трактуется как равная другой ссылке null). Если только одна из ссылок равна null, возвращается false. Если обе ссыл­ки на самом деле указывают на что-либо, вызывается версия Equals() уровня экземпляра. Это значит, что если вы переопределите виртуальный метод Equals() уровня экземпляра, то эффект будет такой, как если бы вы переопределили и статическую версию также.

Операция сравнения (==)

Операцию сравнения можно рассматривать как промежуточный выбор между строгим сравнением значений и строгим сравнением ссылок. В большинстве случаев конструкция

bool b = (х == у); // х, у - объектные ссылки

означает, что сравниваются ссылки. Однако возможно существование классов, для кото­рых более естественным и интуитивно ожидаемым будет трактовка сравнения значений. В таких случаях лучше переопределить эту операцию, чтобы она сравнивала значения. Переопределение операций рассматривается ниже, но очевидным примером может слу­жить класс System.String, для которого Microsoft переопределила операцию с тем, что­бы она сравнивала содержимое строк, а не их ссылки.

Перегрузка операций

В этом разделе рассматривается другой тип членов, которые можно определить в клас­се и структуре: перегруженные операции.

Перегрузка операций - понятие, знакомое разработчикам на C++. Однако, поскольку эта концепция является новой для разработчиков на Java и Visual Basic, мы объясним ее здесь. Разработчики на C++, возможно, предпочтут сразу перейти к главному примеру.

Идея перегрузки операций заключается в том, что вы не всегда ограничиваетесь вы­зовом методов и свойств объектов. Часто возникает необходимость выполнять сложение величин, их умножение, или же осуществить логические операции, такие как сравнение объектов. Предположим, что имеется определенный класс, представляющий математиче­скую матрицу. В мире математики матрицы можно складывать или перемножать, подобно обычным числам. Поэтому, скорее всего, вы захотите написать код вроде такого:

Matrix а, Ь, с;

// Предположим, что а, b и с уже инициализированы

Matrix d = с * (а + b);

С помощью перегрузки операций компилятору можно сообщить, что делают + и * при, использовании с Matrix, что и позволит записать приведенный выше код. Если вы до сих пор кодировали на языке, который не поддерживал перегрузку операций, то, возможно, ре­шите определить методы, выполняющие такие операции. Результат получится не таким ин­туитивно понятным и, скорее всего, будет выглядеть примерно так:

Matrix d = с.Multiply(a.Add(b));

Как вы знаете, операции вроде + и * изначально предназначены исключительно для работы с предопределенными типами данных и на то имеется важная причина: компиля­тор точно знает, что означают операции общего назначения для этих типов данных. Так, например, ему известно, как сложить два значения long или как разделить одно double на другое double, и он может сгенерировать соответствующий код на промежуточном языке. Однако когда вы определяете собственные классы или структуры, то должны предоставить компилятору необходимые сведения: какие методы доступны для вызова, какие поля со­храняются в каждом экземпляре, и т.п. Аналогично, если вы хотите использовать опера­ции с собственными типами, то должны'сообщить компилятору, что эти операции будут означать в контексте данного класса. Сделать это позволяет перегрузка операций.

Еще одно обстоятельство, которое следует подчеркнуть - это то, что, говоря об опе­рациях, мы имеем в виду не только арифметические операции. Существуют также опера­ции сравнения: ==, <, >,!=, >= и <=. Рассмотрим, к примеру, конструкцию if (a==b). Для классов эта конструкция по умолчанию сравнит две ссылки на предмет того, указывают ли они на одно и то же место в памяти, вместо того, чтобы проверить, содержат ли экзем­пляры объектов одинаковые данные. Для класса string это поведение переопределено таким образом, что сравнение строк на самом деле сравнивает их содержимое. Вы можете сделать нечто подобное с собственными классами. По умолчанию для структур операция == вообще ничего не делает. Попытка сравнить две структуры, чтобы узнать, содержат ли они одинаковые значения, приведет к ошибке компиляции, если только вы не перегрузите операцию ==, чтобы сообщить компилятору, как он должен выполнять такое сравнение.

- Существует множество ситуаций, в которых возможность перегрузки операций позво­ляет генерировать лучше читаемый и интуитивно понятный код.

- Практически любые математические объекты, такие как координаты, векторы, матрицы, тензоры, функции и т.д. Если вы пишете программу, которая выполняет некоторое математическое или физическое моделирование, то почти наверняка ис­пользуете классы, представляющие эти объекты.

- Графические программы, которые используют математические или связанные с ко­ординатами объекты при вычислении позиций на экране.

- Класс, представляющий денежные суммы (например, в финансовой программе).

- Текстовый процессор или программа анализа текста, использующая классы, кото­рые представляют предложения, высказывания и т.п.; операции могут понадобиться для комбинирования предложений (более сложная версия конкатенации строк).

Однако существует также много типов, для которых перегрузка операций нежелатель­на. Неосмотрительное применение перегрузки операций порождает код, который гораздо труднее понять. Например, умножение двух объектов DateTime даже концептуально не имеет смысла.

Как работают операции

Чтобы понять, как перегружать операции, очень полезно задуматься о том, что проис­ходит, когда компилятор встречает операцию в коде. Используя операцию сложения (+) в качестве примера, предположим, что компилятор обрабатывает следующие строки кода:

int myInteger = 3;

uint myUnsignedInt = 2;

double myDouble = 4.0;

long myLong = myInteger + myUnsignedInt;

double myOtherDouble = myDouble + myInteger;

Что происходит, когда компилятор встречает показанную ниже строку?

long myLong = myInteger + myUnsignedInt;



Поделиться:


Последнее изменение этой страницы: 2016-12-30; просмотров: 355; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 18.216.186.164 (0.163 с.)