| Клуб программистов Лучшая подборка книг и информации по программированию. |
-
Множественное приведение типов.
Написано 10.05.2010 21:47 Нет комментариевОдна из ситуаций, которую необходимо отслеживать при определении приведений типов, заключается в том, что если не будет существовать прямого приведения типов для выполнения требуемого преобразования, компилятор С# попытается найти способ совмещения нескольких приведений. Например, обращаясь к структуре Currency, предположим, что компилятор встречает такие строки кода:
Curency Balance = new Currency(10, 50);
long Amount = (long)Balance;
double AmountD = Balance;
Сначала инициализируется экземпляр Currency, затем мы пытаемся привести его к long. Проблема заключается в том что такой тип преобразования не определен. Однако код будет успешно компилироваться. Произойдет следующее: компилятор поймет, что вы определили неявное приведение Currency в float, а он знает, как явно привести float к long. Он откомпилирует эту строку в код на промежуточном языке, который сначала преобразует balance в float, а затем преобразует результат в long. То же самое происходит и в последней строке кода при преобразовании Balance в double. Однако так как приведения Currency в float и float в double являются неявными, то и в коде это преобразование можно записать неявно. При желании последовательность приведений можно указать явно:Currency Balance = new Currency(1O, 50);
long Amount = (long)(float)Balance;
double AmountD = (double)(float)Balance;Однако в большинстве случаев это лишь усложнит ваш код. Следующий код приведет к ошибке компиляции:
Currency Balance = new Currency(1O, 50);
long Amount = Balance;Дело в том, что наилучшим путем преобразования, который удается найти компилятору, по-прежнему является преобразование сначала в float, а затем в long. Но преобразование float в long должно быть указано явно. Даже такой код вызовет ошибку:
Currency Balance = new, Currency(1O, 50);
long Amount = (float)Balance;В данном случае мы (бесполезно) явно осуществили первую часть преобразования, но не сделали этого для преобразования float-long.
Само по себе это не должно создавать особых проблем. Эти правила понятны и предназначены для того, чтобы предотвратить потери данных, о которых разработчик может и не знать. Однако если вы не будете осторожны при определении своих собственных приведений типов, компилятор может осуществить такое приведение, что вы получите неожиданные результаты. Допустим, что кому-то еще в группе, занимающейся разработкой класса Currency, приходит в голову мысль, что было бы полезно иметь возможность преобразования uint, содержащего общее количество центов, в Currency (центов, а не долларов, так как нельзя терять доли доллара). Поэтому может быть написано преобразование:// не делайте этого!
public static implicit operator Currency (uint value) {
return new Currency (value/1O0u, (ushort) (value%100) );
}Отметим, что u, идущее за первым числом 100, гарантирует, что value/100u будет интерпретироваться как uint. Если бы мы написали value/100, компилятор интерпретировал бы это выражение как int.
В коде мы явно указали “Не делайте этого”, и вот почему. Посмотрите на следующий фрагмент кода: все, что мы делаем,— преобразуем uint, содержащее 350. в Currency и обратно. Что, по-вашему, будет находиться в Ваl2 после выполнения кода?uint Bal = 350;
Currency Balance = Bal;
uint Bal2 = (uint)Balance;Ответом будет не 350, a 3! И здесь все логично. Мы явно преобразуем 350 в Currency, получая в результате Balance.Dollars=3, Balance.Cents=50. Затем компилятор как обычно определяет наилучший путь для обратного преобразования. В результате Balance неявно преобразуется в float (значение 3.5), и уже это значение явно преобразуется в uint — получаем 3.
Имеются и другие типы данных, для которых преобразование из одного типа данных в другой, а затем обратно вызывает потерю данных. Например, преобразование float, содержащего 5.8, в int, а затем обратно в float приведет к потере дробной части, результатом будет 5. Однако существует принципиальная разница между потерей дробной части и “случайным” делением числа более чем на 100! Currency неожиданно стал опасным классом, который вытворяет странные вещи с целыми числами!
Проблема состоит в том, что имеется конфликт в интерпретации целых чисел нашими приведениями. Приведения между Currency и float интерпретируют целое число 1 как один доллар, а последнее приведение uint-Currency интерпретирует это значение как один цент. Это пример плохой разработки. Если вы хотите, чтобы ваши классы были пригодны для использования, вам придется убедиться в том, что все ваши приведения типов взаимозаменяемы в гом смысле, что они приводят к одинаковым результатам. В данном случае совершенно очевидно, что необходимо переписать приведение uint-Currency так. чтобы оно интерпретировало целое число 1 как один доллар:// Верно
public static implicit operator Currency (uint value) {
return new Currency(value, 0) ;
}В связи с этим возникает вопрос: зачем вообще нужно такое приведение? Дело в том, что без этого приведения единственным способом для компилятора осуществить приведение uint-Currency было бы приведение через float. В данном случае прямое преобразование является более эффективным, поэтому дополнительное приведение увеличивает производительность. Однако необходимо убедиться в том, что оно дает те же результаты, которые могут быть получены при использовании приведения через float. Вы можете встретить ситуации, в которых отдельное объявление преобразований для различных предопределенных типов данных позволяет больше преобразований делать неявно.
Для проверки совместимости приведения типов следует выяснить, приводят ли преобразования к одним и тем же результатам (за исключением, возможно, потери точности, как в преобразовании float-int), независимо от того, каким способом они осуществляются. Хорошим примером может быть класс Currency. Рассмотрим код:Currency Balance = new Currency(50, 35);
ulong Bal = (ulong) Balance;В настоящий момент существует только один способ, которым компилятор может выполнить это действие: преобразовав Currency неявно в float, а затем явно в ulong. Преобразование float-ulong требует явного приведения, в данном случае мы его указали.
Однако допустим, что мы добавляем другое приведение для неявного преобразования Currency в uint. Мы сделаем это, изменив структуру Currency и добавив преобразования как в, так и из uint. Этот код содержится в примере SimpleCurrency2:// Верно!
public static implicit operator Currency (uint value) {
return new Currency(value, 0);
}
public static implicit operator uint (Currency value) {
return value.Dollars;
}Теперь у компилятора имеется другая возможность преобразования из Currency в ulong: преобразовать неявно Currency в uint, а затем неявно в ulong. Какой из этих двух путей он выберет? Разумеется, в С# существуют четкие правила (которые мы не будем описывать в этой книге, их можно найти в документации MSDN), определяющие, как компилятор выбирает наилучший путь из нескольких возможных. Вы должны разрабатывать свои приведения типов так, чтобы все маршруты приведения давали один и тот же результат (за исключением возможной потери точности), тогда будет не важно, какой из них выберет компилятор. (В данном случае компилятор выбирает маршрут Currency->uint->ulong, а не Currency->float->ulong.)
Для проверки примера SimpleCurrency2 добавим следующий код в тестовую программу:try{
Currency balance = new Currency(50, 35);
Console.WriteLine(balance) ;
Console.WriteLine(”balance is ” * balance);
Console.WriteLine(”balance is (using ToString()) ” + balance.ToString()) ;
uint balance3 = (uint) balance;
Console.WriteLine(”Converting to uint gives ” + balance3);Преобразование в uint было успешным, хотя, как и ожидалось, мы потеряли при этом центы. Приведение отрицательного значения float к Currency вызвало ожидаемое исключение переполнения, так как теперь само преобразование float-Currency осуществляется в checked-контексте.
Однако результат показывает одну потенциальную проблему, которую следует учитывать при приведении типов. Самая первая строка некорректно вывела значение balance, показав 50 вместо $50.35. Из этих строк:Console.WriteLine(balance);
Console.WriteLine(”balance is ” + balance);
Console.WriteLine(”balance is (using ToString() ” + balance.ToString ());только две последние строки правильно отобразили Currency как строку. Что же произошло?
Проблема заключается в том, что совмещение приведений типов с перегрузками методов может быть непредсказуемым.
Рассмотрим эти три строки в обратном порядке. Третий оператор Console.WriteLine() явно вызывает метод Currency.ToString(), гарантируя, что Currency отобразится как строка. Второй оператор не делает этого. Однако строковый литерал “balance is “, который передается в Console.WriteLine(), ясно показывает компилятору, что параметр должен интерпретироваться как строка. Поэтому метод Currency.ToString() будет вызван неявно.
Самый первый метод Console.WriteLine() передает структуру Currency в Console.WriteLine(). Console.WriteLine() имеет большое число перегруженных версий, но ни одна из них не принимает структуру Currency. Поэтому компилятор начнет выяснять, как можно привести Currency, чтобы она соответствовапа одному из перегруженных методов Console.WriteLine(). Оказывается, что один из перегруженных методов Console.WriteLine() разработан специально для быстрого и эффективного отображения uint и принимает uint в качестве параметра. А мы только что определили приведение, которое неявно преобразует Currency в uint. Об остальном вы можете догадаться сами.
Кстати говоря, Console.WriteLine() имеет еще один перегруженный метод, который принимает в качестве параметра double и отображает его значение. Если вы внимательно посмотрите на результаты первого примера SimpleCurrency, то обнаружите, что первая строка отобразила Currency как double, используя этот перегруженный метод. В том примере отсутствовало прямое приведение Currency в uint, поэтому компилятор выбрал путь Currency->Eloat->double как предпочтительный при поиске подходящего перегруженного метода Console.WriteLine(). Но сейчас, имея возможность прямого преобразования в uint, компилятор предпочел использовать его.
Результатом является то. что если у вас имеется метод, для которого существует несколько перегруженных вариантов, и вы пытаетесь передать в него параметр, чей тип данных не соответствует точно типу данных ни в одном из этих перегруженных методов, то вы заставляете компилятор не только выбирать приведения типов для преобразования данных, но и какой перегруженный метод — а, следовательно, и какие преобразования — использовать. Разумеется, компилятор всегда поступает логично и в соответствии со строгими правилами. Однако результат может оказаться не тем, который ожидается. Если существуют какие-то сомнения, лучше явно указывать приведения типов.



Новые комментарии