Упаковывающие и распаковывающие приведения 


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



ЗНАЕТЕ ЛИ ВЫ?

Упаковывающие и распаковывающие приведения



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

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

Приведение любой структуры (или примитивного типа) к типу object всегда выпол­няется неявно, потому что это, по сути - приведение производного типа к базовому, что представляет собой уже известный процесс упаковки. Для примера возьмем структуру Currency:

Currency balance = new Currency(40,0);

object baseCopy = balance;

Когда выполняется это неявное приведение, содержимое balance копируется в кучу, в упакованный объект, и на этот объект устанавливается объектная ссылка baseCopy. Что же на самом деле происходит за кулисами этого процесса? Когда мы изначально определяем структуру Currency, среда.NET Framework неявно создает другой (скрытый) класс - класс-упаковку Currency, который содержит все те же поля, что исходная структура Currency, но является ссылочным типом, размещаемым в куче. Это случается всякий раз, когда опре­деляется новый тип значения - будь то перечисление или структура; подобные упакован­ные тссылочные типы существуют для всех примитивных типов значений, таких как int, double, uint и т.д. Получать прямой доступ к какому-либо классу-упаковке в исходном коде невозможно, да и не нужно, однако они являются объектами, которые работают “за кулиса­ми” при каждом приведении типа значения к object. Когда Currency неявно приводится к object, при этом создается упакованный экземпляр Currency, который инициализиру­ется данными из структуры Currency. В последнем примере это будет упакованный экзем­пляр Currency, на который ссылается baseCopy. Благодаря такому механизму, существует возможность приведения от производного к базовому типу, которое синтаксически работа- ет одинаково и для ссылочных типов, и для типов значений.

Обратное приведение известно как распаковка. Так же, как в случае приведения между базовым ссылочным типом и производным ссылочным типом, такое приведение является явным, потому что генерируется исключение, если преобразуемый объект относится к не­допустимому типу:

object derivedObject = new Currency(40,0);

object baseObject = new object();

Currency derivedCopy1 = (Currency)derivedObject; // нормально

Currency derivedCopy2=(Currency)baseObject; // генерирует исключение

Этот код работает точно так же, как приведенный ранее пример со ссылочными типами. Приведение derivedObject к Currency работает правильно, потому что derivedObject на самом деле ссылается на упакованный экземпляр Currency - приведение осуществля­ется копированием полей упакованного объекта Currency в новую структуру Currency. Второе приведение завершается неудачей, поскольку baseObject не ссылается на упако­ванный объект Currency.

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

Множественные приведения

При определении приведений следует обращать особое внимание на одно обстоятель­ство. Если компилятор C# оказывается в ситуации, когда не определено прямое приведе­ние для выполнения необходимого преобразования, то он пытается найти способ комби­нации существующих приведений, чтобы все-таки выполнить задачу. Рассмотрим пример со структурой Currency; предположим, что компилятор встретил следующий фрагмент исходного кода:

Currency balance = new Currency (10,50);

long amount = (long)balance;

double amountD = balance;;

Здесь сначала мы инициализируем экземпляр Currency, затем пытаемся преобразовать его в long. Проблема в том, что мы не определили в нашей структуре такого приведения. Однако, несмотря на это, код будет успешно скомпилирован. Дело в том, что компилятор видит, что у нас определено неявное преобразование из Currency в float, кроме того, он знает, как явно приводить float к long. Поэтому он скомпилирует эту строку в IL-код, преобразующий сначала Currency во float, а потом полученный результат - в long. То же самое происходит в последней строке, где balance преобразуется в double. Однако поскольку и приведение Currency к float, и встроенное приведение float к double яв­ляются неявными, мы можем записать такое приведение как неявное. Если вам больше нравится, можете специфицировать маршрут приведения явным образом:

Currency balance = new Currency(10,50);

long amount = (long)(float)balance;

double amountD = (double)(float)balance;

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

Currency balance = new Currency(10,50);

long amount = balance;

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

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

Например, предположим, так случится, что кто-то другой в группе напишет структуру Currency, которая будет уметь преобразовывать значение uint, содержащее количество центов денежной суммы, в структуру Currency (центы, а не доллары, потому что идея была связана с исключением потери дробных частей доллара).

Можно попробовать написать такое приведение следующим образом:

public static implicit operator Currency(uint value)

{

return new Currency(value/100u, (ushort)(value%100));

} // He делайте так!

Обратите внимание в этом коде на символ и после числа 100, который необходим, что­бы value/100u интерпретировалось как uint. Если написать просто value/100, то ком­пилятор интерпретирует это как int, а не uint.

Комментарий ясно говорит: “Не делайте так!”, и вот почему. Взглянем на следующий фрагмент кода. Все, что он делает — это преобразует значение 350 типа uint в Currency и обратно. Как вы думаете, что будет содержать Ьа12 после его выполнения?

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, а затем явно - float в ulong.

Преобразование float в ulong требует явного приведения, но с этим все в порядке, поскольку оно как раз и указано.

Предположим, однако, что затем вы добавили другое приведение, которое преобразует неявно Currency в uint. Это делается добавлением двух операций приведения в структуру Currency. Их код есть в примере 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. Какой из двух пу­тей будет выбран? Язык C# следует некоторым правилам приоритетов (в этой книге они подробно не рассматриваются; если интересно, детали можно найти в документации MSDN), согласно которым компилятор принимает решение о выборе наилучшего пути из нескольких возможных. Наилучший ответ на заданный вопрос состоит в том, что вы долж­ны проектировать приведения так, чтобы все маршруты давали один и тот же результат (исключая возможную потерю точности), чтобы не имело значения, какой из возможных путей выберет компилятор. (В данном случае компилятор отдаст предпочтение маршруту Currency-uint-ulong вместо Currency-float-ulong.)

Чтобы протестировать пример SimpleCurrency2, добавьте следующий код в пример SimpleCurrency:

try

{

Currency balance = new Currency(50,35);

Console.WriteLine(balance);

Console.WriteLine("Баланс равен " + balance);

Console.WriteLine("Баланс равен (используя ToString ()) " +

balance.ToString());

uint balance3 = (uint) balance;

Console.WriteLine("Преобразование к uint дает " + balance3);

}

Выполнение примера даст такой результат:

Баланс равен $50.35

Баланс равен (используя ToString ()) $50.35

Преобразование к uint дает 50

После преобразования в float = 50.35

После преобразования обратно в Currency = $50.34

Теперь преобразуем значение вне допустимого диапазона -$50.50 в Currency:

Результат равен $4294967246.00

Вывод показывает, что преобразование в uint выполнилось успешно, хотя, как и ожи­далось, с потерей центовой части значения Currency, подвергшегося преобразованию. Приведение отрицательного значения float к Currency также генерирует ожидаемое исключение переполнения, потому что приведение float к Currency определяет внутри себя checked-контекст. Однако вывод программы также демонстрирует одну потенциаль­ную проблему, на которую следует обратить внимание при работе с приведениями. Самая первая строка вывода отображает баланс некорректно, указывая 50 вместо $50.35.

Рассмотрим следующие строки:

Console.WriteLine(balance);

Console.WriteLine("Баланс равен " + balance);

Console.WriteLine("Баланс равен (используя ToString()) " +

balance.ToString());

Только вторая и третья строки корректно отображают значение Currency в виде стро­ки. Что же происходит? Проблема в том, что когда вы комбинируете приведения с пере­грузкой методов, то получаете другой источник неопределенности. Посмотрим на эти строки в обратном порядке.

Третий оператор Console.WriteLine() явно вызывает метод ToString() для обес­печения отображения Currency как строки. Второй этого не делает. Однако строковый литерал "Баланс равен", переданный Console.WriteLine(), дает понять компилятору, что параметр должен быть интерпретирован как строка. А потому здесь неявно вызывает­ся метод Currency.ToString().

Тем не менее, первый же вызов метода Console.WriteLine() просто передает не­форматированную структуру Currency методу Console.WriteLine(). Этот метод имеет множество перегрузок, но ни одна из них не предусматривает получения в качестве пара­метра структуры Currency. Поэтому компилятор начинает искать, к чему можно, привести Currency, чтобы ее значение подошло к одной из перегрузок Console.WriteLine(). Так случилось, что одна из этих перегрузок предназначена для быстрого и эффективного ото­бражения значений типа uint, она принимает значение uint в качестве параметра, а у нас определено неявное приведение Currency к uint.

На самом деле Console.WriteLine() имеет и другую перегрузку, которая прини­мает параметр типа double и отображает его значение. Если вы посмотрите внима­тельно на вывод первого примера SimpleCurrency, то увидите там, что самая первая строка отображает значение Currency как double. В том примере еще не было опре­делено прямое приведение Currency к uint, поэтому компилятор предпочел маршрут Currency-float-double, чтобы создать значение, наиболее подходящее к одной из пере­грузок Console.WriteLine(). Однако теперь в примере SimpleCurrency2 имеется пря­мое приведение к uint, и компилятор предпочитает использовать его.

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

Итоги

В настоящем разделе были рассмотрены стандартные операции С#, описаны механизмы сравнения объектов, а также способы преобразования стандартных типов данных между собой. Было продемонстрировано, как осуществляется поддержка реализации операций для пользовательских типов данных посредством перегрузки. И, наконец, была описана перегрузка операций специального рода - операций приведения, позволяющих специфи­цировать механизмы преобразований пользовательских типов (классов и структур) в дру­гие типы данных.

 

 



Поделиться:


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

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