Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Операции и приведения.docx
Скачиваний:
3
Добавлен:
01.07.2025
Размер:
200 Кб
Скачать
☆

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

В настоящем разделе иллюстрируется применение явных и неявных пользовательских приведений на примере под названием 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.

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