Нэш Трей - C# 2010. Ускоренный курс для профессионалов (2010) (1160865), страница 49
Текст из файла (страница 49)
рттуэте боцЬ1е теа1; ртбтате боцЫе тщэдтпэтуз ) рцЫтс с1эээ Бпттуротпт ( зтаттс тот б Мз1п () ( Сощр1ех срх1 = пеы Сощр1ех ( 1. О, 3. 0 ); Сощр1ех срх2 = 2.0з // Использовать неявную операцию. боцЬ1е б = (боцЫе) срх1; // Использовать явную операцию. Сопэо1е.кттте11пе( "срх1 = (0)", срх1 ); Сопэо1е.иттте11ое( "срх2 = (0)", срх2 ); Сопэо1е.ит1те1 1ое ( "б = (О ) ", б ) з ) В коде метода матп используются операции преобразования. При реализации операций преобразования будьте осторожны, чтобы не создать для пользователей никаких сюрпризов или путаницы с неявными преобразованиями.
Внести путаницу с явными операциями, когда пользователи типа вынуждены применять синтаксис приведения для того, чтобы заставить их работать, довольно-таки сложно. В конце концов, чем можно удивить пользователя, если он должен указать в круглых скобках тип для преобразования7 Но с другой стороны, невнимательное или неправильное использование неявного преобразования может стать причиной большой путаницы. Если написать множество операций неявного преобразования, не имеющих семантического смысла, пользователи в один прекрасный день будут весьма удивлены, когда компилятор решит выполнить преобразование, когда они вовсе этого не ожидали.
Например, компилятор может выполнять неявное преобразование, пытаясь подогнать аргумент при вызове метода. Даже если операции преобразования имеют семантический смысл, они могут таить в себе ряд сюрпризов, поскольку у компилятора есть возможность молча преобразовывать экземпляры одного типа в другой, когда он сочтет это необходимым. В отличие от С++, где конструкторы с одним параметром по умолчанию ведут себя как операции преобразования, Сз требует явного написания операций неявного преобразования для определяемых вами типов з.
Таким образом, вы не сможете нечаянно создать операцию неявного преобразования, не осознавая того, что делаете (как это возможно в С++). Однако для того, чтобы предоставить такие преобразования, необходимо всегда подчиняться правилам перегрузки методов. Да, я понимаю последствия своих высказываний и возможную путаницу. вызванную при- менением слов явный и неявный. Я явно надеюсь не запутать вас неявно... 184 Глава б Рассмотрим случай, когда Сопр1ех предоставляет другую операцию явного преобразования в экземпляр Егаст1оп, а также в экземпляр с(опЬ1е.
Это потребует добавления в Согкр1ех методов со следующими сигнатурами: рпЬ11с эсастс ехр11стс орегзсог бопЬ1е( Совр1ех б ) риЬ11с зсэстс ехр11стс орегэгог Ггзсстоп( Сотр1ех Г ) Эти два метода принимают тип Соир1ех и возвращают другой тип. Однако правила перегрузки четко указывают, что тип возврата в сигнатуре метода не учитывается. Если следовать этому правилу, такие два метода вносят неоднозначность, приводяшую к ошибке компиляции. Но на самом деле они не вносят неоднозначности, поскольку существует специальное правило, разрешающее рассматривать тип возврата операции преобразования как часть сигнатуры. Между прочим, ключевые слова пэр11сбс и ехр11стг не входят в сигнатуру методов операций преобразования, поэтому иметь одновременно и явную,и неявную версии одной и той же операции преобразования невозможно.
Естественно, по крайней мере, один из типов в сигнатуре операции преобразования должен быть включающим типом. Реализация операции преобразования из типа Арр1ез в тип Окапав для типа Сотр1ех не допускается. Загадка плавающей точки Джон Скит (доп Знее() привел блестящий пример, который доказывает, что в некоторых случаях, имея дело с числами с плавающей точкой, неявные преобразования могут привести к потере данных. Джон иллюстрирует зто утверждение следующим примером: пзтпс зузсев; рпЬ11с с1азз Тезс ( зсзгтс тотб Мзгп() ( 1опд 11 = 1опс.нзхуз1пе — 5; 1опд 12 = 1опс.иэхуз1се — 4; босЬ1е б1 = 11; бопЬ1е б2 = 12; Сопзо1е.нг1сеьвпе(б1); Сопзо1е.нг1се11пе(б2); Сопэо1е.нг1сеЬгпе (11 == 12) ) Сопэо1е.иг1сеЬ1пе 051 == д2) ) ) ) После запуска этого кода на выполнение будет получен несколько неожиданный результат: 9.22337203685478Е+18 9.223372036854788+18 Еа1зе Тгое Прежде чем поднимать шум по этому поводу, следует вспомнить, что числа с плавающей точкой являются одной из самых коварных и часто недооцениваемых областей программирования.
Более того, имея дело с числами с плавающей точкой, операции сравнения часто учитывают только значащие разряды, а остальная часть рассматривается как допустимая погрешность. Тематике правильного представления чисел с плавающей точкой на машинах, рассчитанных, по сути, только на целочисленные вычисления, посвящено множество диссертаций. Заинтересованные мокнут ознакомиться с приложением О к монографии /тип) епсэ/ сол) ршзвьл Ои(бе (Руководство по числовым расчетам), которую можно найти по адресу Ьгер: //боса. зпп.
сов/зоигсе/806-3568/пс9 со1бЬегд. Ьси1. Кстати, если в предыдущем примере заменить тип боцЬ1е на бества1, то получится менее неожиданный результат, ввиду того, что бес1ва1 может представлять больше десятичных разрядов, чем бопЬ1е. Перегрузка операций к 85 Булевокие операции Некоторые типы могут принимать участие в булевских выражениях проверни, таких как встречающиеся внутри скобок блока 11 или внутри тернарной операции Р:.
Чтобы это работало, имеются две альтернативы. Первая заключается в том, что можно реализовать две операции преобразования, известные как орегасот стие и орегасот Ра1яе. Они должны быть реализованы в паре, чтобы позволить комплексному числу (типу Совр1ех)участвовать в булевскнхвыраженияхпроверки. Рассмотрим следующую модификацию типа Совр1ех,которая позволяет использовать его в выражениях; здесь значение(0, 0) означает Ра1зе,а все остальное — Стае: ияспд Буятеги риЬ11с ятгист Совр1ех ( риЫтс Совр1ех( боиЬ1е геа1, боиЫе 1вадспату ) ( СЬ1я.геа1 = теа1; СЫя.твадтпату = 1вачспату," ) // Переопределение Буясев.ОЪ)ест риЫтс очеггтбе яттспд Тозтгспд() ( гетитп Бтгспд.гогвас( "((О), (1))", теа1, гвад1пагу ) риЬ1тс боиЬ1е Мадп1гибе ( Оет ( гегигп Маги.Битт( Маги.Рон(СЬтя.геа1, 2) Матп.гон(СЫя.свадспату, 2) )~ ) ) РиЫ1с ятаттс Ьоо1 орегасот стие( Совр1ех с ) ( тесигп (с.геа1 != 0) (( (с.свадспату != 0); ) РиЬ1тс ясас1с Ьоо1 сретасат Ра1яе( Совр1ех с ) ( гегитп (с.геа1 == О) ая (с.свад1пату == 0)к ) // Остальные методы опущены для ясности.
ртсчате боиЬ1е теа1; ргтчате боиЫе 1вадспату; ) риЫтс с1аяя ЕпггуРо1пг ( ятастс чотб Матп() Совр1ех срх1 = пен Совр1ех( 1.0, 3.0 ); 1Е( срх1 Сопяо1е.нгсгепбпе( "срх1 равно Стие" ); ) е1яе ( Сопяо1е.нт1геьапе( "срх1 равно Ра1яе" )) ) Совр1ех срх2 = пен Совр1ех( О, 0 )к Сопяо1е.нтсгеьспе( "срх2 равно (0)", срх2 2 "итие": "та1яе" ); 186 Глава 6 Эдесь реализованы две операции для применения к типу Совр1ех проверок на предмет равенства т дне и Ра1яе. Обратите внимание на несколько причудливый синтаксис. Он выглядит почти так же, как в обычных операциях, за исключением того, что типом возврата является Ьоо1. Зачем это нужно — не совсем понятно, поскольку указать какой-то другой тип возврата для этих операций все равно не удастся. Если все-таки попробовать это сделать, то компилятор немедленно сообщит, что единственным допустимым типом возврата для оретатот тгое и оретатот Рз1яе является Ьоо1.
Тем не менее, тип возврата должен быть указан. Также обратите внимание, что эти операции нельзя пометить как ехр11с1С или 1вр11с1т, потому что они не являются операциями преобразования. После определения этих двух операций в типе можно использовать экземпляры Совр1ех в булевских выражениях проверки, как показано в методе Мауп. В качестве альтернативы можно реализовать преобразование к типу Ьоо1 и получить тот же результат.
Обычно для простоты использования эта операция реализуется неявно. Рассмотрим модифицированную форму предыдущего примера с использованием неявной операции преобразования к Ьоо1 вместо оретатот ттпе и оретатот Ра1яе: оятпд Яуятевв рсЬ1тс яттост Совр1ех роЬ1тс Совр1ех( ооиЬ1е геа1, с)ооЬ1е твад1пату ) ( тЫя.теэ1 = тез1; гЫя.ыяадтпату = твадтпэгу; О переопределение Яуятев.оь1ест роЫ1с отегттбе ятттпд тоятттпд() ( гетогп ятттпд.гогвас( "((О), (1))", геа1, твздтпагу ) рсЫтс ооиЫе Мядп1тос(е ( дет ( тетотп Метл.ядтт( Матп.рон(тп1я.теа1, 2) Мвтп.рон(тЫя.твадтпагу, 2) ) рпЫтс ясзстс твратстт орегатот Ьоо1( Совр1ех с ) ( тетотп (с.теа1 != О) (( (с.1вадтпагу != О); ) // Остальные методы опущены длн ясности.
рт1тзге СооЬ1е теа1в ртттате оооЫе твад1пату; ) рпЬ1тс с1еяя Япттуротпт ( ятзттс тотб Матп() ( Совр1ех срх1 = пен Совр1ех( 1.0, 3.0 ); 11( срх1 ) ( Сопяо1е.ит1текьпе( "срх1 равно тгое" )! ) е1яе ( Сопяо1е.ыт1теьтпе( "срх1 равно те1яе" ); ) Совр1ех срх2 = пен Совр1ех( О, 0 ); Сопзо1е.ытттеьтпе( "срх2 равно (О)", срх2 ? "ттое": "Га1яе" ); Перегрузка операций 187 Конечный результат выглядит так же, как в предыдущем примере. Теперь может возникнуть вопрос. а зачем вообще реализовывать орегасог сгце и орегасог бз1зе вместо простой неявной операции преобразования к Ьоо1? Ответ зависит от того, допустимо ли для заданного типа преобразование в тип Ьоо1. В последней форме, где реализована неявная операция преобразования, следующий оператор будет корректным: Ьоо1 Г = срх1; Такое присваивание должно работать, потому что компилятор найдет операцию неявного преобразования во время компиляции и применит ее.
Однако если вы смертельно устали после ночи кодирования этой строки и действительно намерены присваивать Г значение совершенно другой переменной, может пройти много времени прежде. чем удастся обнаружить ошибку. Это лишь один пример того. как необдуманное применение операций неявного преобразования может привести к возникновению проблем. Существует следующее эмпирическое правило: предусматривайте только те операции, которые действительно необходимы для выполнения работы, и не более. Если все.












