Заглавная страница Избранные статьи Случайная статья Познавательные статьи Новые добавления Обратная связь КАТЕГОРИИ: АрхеологияБиология Генетика География Информатика История Логика Маркетинг Математика Менеджмент Механика Педагогика Религия Социология Технологии Физика Философия Финансы Химия Экология ТОП 10 на сайте Приготовление дезинфицирующих растворов различной концентрацииТехника нижней прямой подачи мяча. Франко-прусская война (причины и последствия) Организация работы процедурного кабинета Смысловое и механическое запоминание, их место и роль в усвоении знаний Коммуникативные барьеры и пути их преодоления Обработка изделий медицинского назначения многократного применения Образцы текста публицистического стиля Четыре типа изменения баланса Задачи с ответами для Всероссийской олимпиады по праву Мы поможем в написании ваших работ! ЗНАЕТЕ ЛИ ВЫ?
Влияние общества на человека
Приготовление дезинфицирующих растворов различной концентрации Практические работы по географии для 6 класса Организация работы процедурного кабинета Изменения в неживой природе осенью Уборка процедурного кабинета Сольфеджио. Все правила по сольфеджио Балочные системы. Определение реакций опор и моментов защемления |
Операции checked и uncheckedСтр 1 из 9Следующая ⇒
Операции и приведения • Операции в C# • Идея эквивалентности при работе со ссылками и типами значений • Преобразование данных между примитивными типами • Преобразование типов значений в ссылочные типы посредством Упаковки • Преобразования ссылочных типов посредством приведения • Перегрузка стандартных операций для пользовательских типов • Добавление операций приведения к пользовательским типам
В предыдущих главах была представлена большая часть материала, необходимого для того, чтобы приступить к написанию полезных программ на С#. В настоящей главе дискуссия о важнейших элементах языка завершается и иллюстрируется ряд наиболее мощных аспектов С#, которые позволяют расширять возможности языка. Операции Хотя большинство операций C# должны быть знакомы разработчикам на С и C++, в настоящем разделе мы представим наиболее важные из них - большей частью для программистов-новичков и тех, кто переходит на C# из Visual Basic. Кроме того, мы прольем свет, на некоторые отличия в синтаксисе и семантике операций, присущие С#. В языке C# поддерживаются операции, перечисленные в табл. 7.1. Таблица 7.1. Операции в С#
Таблица 7.1. Операции в С#
Обратите внимание, что четыре специфических операции (sizeof, *, -> и &, описанные в табл. 7.2) доступны только в небезопасном коде (коде, минующем контроль безопасности типов С#), который мы рассмотрим в главе 13. Важно также отметить, что ключевые слова операции sizeof, используемые в ранних версиях.NET Framework 1.0 и 1.1, требовали небезопасного режима. Начиная с версии.NET Framework 2.0, это требование отменено.
Таблица 7.2. Операции, доступные только в небезопасном режиме
Одной из основных ловушек, которых нужно остерегаться при использовании операций С#, является то, что, как и во всех С-подобных языках, в C# применяются разные операции для присваивания (=) и сравнения (==). Например, следующий оператор означает пусть х будет, равно 3: х = 3; Теперь, если вы хотите сравнить х со значением, то должны использовать двойной знак равенства ==: if (х ==3) { } Ксчастью, строгие правила безопасности типов C# предотвращают очень частую ошибку в С, когда вместо сравнения в логическом операторе используется присваивание. Это значит, что следующий оператор вызовет ошибку компиляции: if (х = 3) { } Программисты на Visual Basic, привыкшие использовать амперсанд (&) для конкатенации строк, должны быть внимательны. В C# для этого служит знак плюс (+), в то время как & — битовая операция “И”, применяемая к двум целым числам. | позволяет выполнить битовое “ИЛИ” между двумя целыми. Программистам на Visual Basic также не знакома арифметическая операция взятия модуля (%). Она возвращает остаток от целочисленного деления. То есть, например, если х равно 7, то х%5 вернет 2. Вы также будете использовать немного указателей в С#, а вместе с ними - немного операций косвенного обращения. Точнее говоря, единственное место, где они используются - это блоки небезопасного кода, поскольку только в них разрешены указатели С#. Указатели и небезопасный код обсуждаются в разделе 6.13. Сокращения операций В табл. 7.3 представлен полный список сокращенных операций присваивания, доступных в С#. Таблица 7.3. Сокращенные операции присваивания
Возможно, вас удивит, почему здесь показано по два примера операций инкремента ++ и декремента --. Дело в том, что размещение операции перед выражением известно как префиксная форма, а после выражения - как постфиксная форма, и их поведение различается.
Операции инкремента и декремента могут служить полными выражениями и использоваться внутри других выражений. Когда они применяются сами по себе, эффект от постфиксной и префиксной версий одинаков и соответствует конструкции х = х + 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#
В сложных выражениях для получения корректного результата не следует слишком полагаться на приоритеты операций. Применение скобок для указания порядка вычисления операций проясняет код и позволяет избежать возможной путаницы. Безопасность типов В разделе 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#
Как и следовало ожидать, неявные преобразования допускаются только от меньших целых типов к большим, но не наоборот. Можно также преобразовывать данные между целыми и действительными значениями, однако здесь правила слегка отличаются. Допускается преобразовывать данные между типами одинакового размера, например, из 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 с.) |