Реализация пользовательских приведений 


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



ЗНАЕТЕ ЛИ ВЫ?

Реализация пользовательских приведений



В настоящем разделе иллюстрируется применение явных и неявных пользовательских приведений на примере под названием SimpleCurrency. В этом примере определяется структура Currency, хранящая положительное денежное значение в американских долла­рах. Для этой цели в C# предусмотрен тип decimal, но все-таки может случиться, что вы захотите написать собственный класс, представляющий денежные значения, если возник­нет потребность в какой-то сложной финансовой обработке, а, следовательно - в реализа­ции специфических методов этого класса.

Синтаксис приведения одинаков для структур и классов. Здесь мы приводим пример со структурой, но он будет работать точно так же, если объявить Currency классом.

Изначальное определение структуры Currency выглядит следующим образом:

struct Currency

{

public uint Dollars;

public ushort Cents;

public Currency(uint dollars, ushort cents)

{

this.Dollars = dollars;

this.Cents = cents;

}

public override string ToString()

{

return string. Format (“${0}.{1,-2:00}”, Dollars, Cents);

}

}

Применение беззнаковых типов данных для полей Dollar и Cents гарантирует, что экземпляры Currency будут содержать только положительные значения. Это ограничение мы реализуем таким способом, чтобы ниже проиллюстрировать некоторые моменты, свя­занные с явными приведениями. Вы можете использовать подобный класс для того, что­бы, например, хранить информацию о зарплате сотрудников компании (понятно, что зар­платы не бывают отрицательными). Для простоты мы делаем поля общедоступными, хртя обычно вы будете объявлять их приватными и определять соответствующие свойства для доступа к долларам и центам.

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

Currency balance = new Currency(10,50);

float f = balance; // Нужно присвоить f значение 10.5

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

public static implicit operator float (Currency value)

{

return value.Dollars + (value.Cents/100.0f);

}

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

Здесь есть небольшая натяжка: фактически, при преобразовании и int в float происходит потеря точности, но Microsoft в любом случае рассматривает эту ошибку как не столь существенную и разрешает неявное приведение uint в float.

Однако если вы хотите преобразовывать float в Currency, то не гарантируется, что такое преобразование будет работать: float может содержать отрицательные значения, которые для Currency недопустимы. Кроме того, float может содержать числа намного большие, чем можно уместить в поле (uint)Dollar нашей структуры Currency. Поэтому если float содержит неподходящее значение, то преобразование его в Currency может дать непредсказуемый результат. Учитывая такую опасность, приведение float в Currency должно быть явным (explicit).

Предпримем первую попытку, которая, хотя и не даст правильного результата, но по­служит поучительным примером:

public static explicit operator Currency (float value)

{

uint dollars = (uint)value;

ushort cents = (ushort)((value-dollars)*100);

return new Currency(dollars, cents);

}

Следующий код теперь успешно скомпилируется:

float amount = 45.63f;

Currency amount2 = (Currency)amount;

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

float amount = 45.63f;

Currency amount2 = amount; // неверно

Объявив приведение явным, вы тем самым предупреждаете разработчика о том, что следует быть осторожным, поскольку может случиться потеря данных. Однако, как вско­ре будет показано, это не совсем желательное поведение структуры Currency. Попробуем написать тестовый пример и запустить его. У нас будет метод Main(), который создаст экземпляр Currency и попытается выполнить несколько преобразований. В начале кода напишем вывод на экран значения денежного баланса двумя разными способами (это по­надобится для того, чтобы кое-что проиллюстрировать позднее),

static void Main()

{

try

{

Currency balance = new Currency(50,35);

Console.WriteLine(balance);

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

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

balance.ToString());

float balance2 = balance;

Console.WriteLine("После преобразования в float = " +

balance2);

balance = (Currency) balance2;

Console.WriteLine("После преобразования обратно в Currency = "

+ balance);

Console.WriteLine("Теперь преобразуем значение вне допустимого диапазона " +

"-$50.50 в Currency:");

checked

{

balance = (Currency)(-50.50);

Console.WriteLine("Результат: " + balance.ToString());

}

}

catch(Exception e)

{

Console.WriteLine("Возникло исключение: " + e.Message);

}

}

Обратите внимание, что весь код помещен в блок try, чтобы перехватывать любые ис­ключения, которые могут случиться во время приведений. Кроме того, те строки, которые проверяют преобразование значений вне допустимого диапазона в Currency, помещены в блок checked, чтобы отловить отрицательные значения. Запуск этого кода даст следую­щий вывод:

50.35

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

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

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

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

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

 

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

Первая проблема вызвана ошибками округления. Если приведение применяется для преобразования float в uint, то компьютер отбрасывает дробную часть вместо того, что­бы округлять ее. Компьютер сохраняет значения в двоичном виде, а не в десятичном, и дробная часть 0.35 не может быть точно представлена в двоичном виде (как и значение 1/3 не может быть представлено в десятичном виде - оно превращается в периодическую десятичную дробь 0,3333...). Поэтому компьютер сохраняет значение, которое чуть мень­ше, чем 0.35, и которое может быть представлено в двоичном формате. Умножив его на 100, вы получите число, на некоторую дробную величину меньше, чем 35, которое усекается до 34 центов. Понятно, что в этой ситуации ошибки, вызванные усечением, достаточно серьезны, и чтобы избежать их, нужно применить некоторый интеллектуальный способ округления. К счастью, Microsoft поставляет класс, который может это делать: System.Convert. Этот класс содержит большое количество статических методов, выполняющих разнообразные числовые преобразования, включая подходящий в этой ситуации Convert.ToUInt16().

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

Теперь разберемся, почему не возникло исключение переполнения. Проблема вот в чем: место, где на самом деле случилось переполнение, на самом деле вовсе не находится в процедуре Main() - оно находится в коде операции приведения, который вызывается из метода Main(). И этот код не было помечен как checked.

Решение состоит в заключении в блок checked также и тела операции приведения. Внеся эти два изменения, получим следующий код преобразования типа:

public static explicit operator Currency (float value)

{

checked

{

uint dollars = (uint)value;

ushort cents = Convert.ToUInt16((value-dollars)*100);

return new Currency(dollars, cents);

}

}

Следует обратить внимание, что для вычисления центов применяется Convert.ToUInt16(), как упоминалось ранее, но этот метод не используется для вычисления долларовой час­ти суммы. Для работы с долларовой составляющей суммы нет необходимости в System. Convert, потому что усечение значения float дает то, что нужно.

 

 

Стоит упомянуть, что методы System.Convert также сами заботятся о контроле переполнения. Поэтому в данном конкретном случае, который мы здесь рассматриваем, нет необходимости помещать вызов Convert.ToUInt16() в checked-контекст. Однако этот контекст все еще нужен для явного приведе­ния значения в доллары.

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

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

Приведение между классами

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

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

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

Чтобы проиллюстрировать эти требования, предполо­жим, что есть иерархия классов, показанная на рис. 7.1.

Другими словами, классы С и D непрямо унаследованы от А. В этом случае единственными допустимыми пользователь­ским приведением между типами А, В, С и D будут приведения между классами С и D, потому что эти классы не наследуют друг друга. Код таких приведений может выглядеть следую­щим образом (предполагается, что они будут явными, как это обычно бывает при опреде­лении приведений между пользовательскими типами):

public static explicit operator D(C value)

{

// и т.д.

}

public static explicit operator C(D value)

{

// и т.д.

}

При определении каждой из этих операций приведения у вас есть выбор, куда их по­местить - внутрь определения класса С или же внутрь определения класса D, но никуда более. C# требует, чтобы определение приведения было помещено в определение либо исходного класса (или структуры), либо целевого. Побочным эффектом этого требования является невозможность определить приведение между двумя классами, не имея доступа на редактирование исходного кода хотя бы одного из них. И это разумно, поскольку подоб­ным образом предотвращается определение приведений к вашим классам от независимых разработчиков.

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



Поделиться:


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

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