- •Тема 6. Основы программирования на языке c#
- •Условная операция
- •Операции checked и unchecked
- •Операция поглощения null
- •Безопасность типов
- •Преобразования типов
- •Неявные преобразования
- •Явные преобразования
- •Проверка равенства объектов
- •Упаковка и распаковка
- •Виртуальный метод Equals()
- •Статический метод Equals()
- •Проверка типов значений на равенство
- •Перегрузка операций
- •Как работают операции
- •Пример перегрузки операции: структура Vector
- •Добавление дополнительных перегрузок
- •Перегрузка операций сравнения
- •Пользовательские приведения
- •Реализация пользовательских приведений
- •Приведение между классами
- •Приведение между базовым и производным классами
- •Упаковывающие и распаковывающие приведения
- •Множественные приведения
Реализация пользовательских приведений
В настоящем разделе иллюстрируется применение явных и неявных пользовательских приведений на примере под названием 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.
Если вы определяете приведение, которое будет использоваться очень часто и для которого производительность стоит на первом месте, то можете предпочесть не выполнять никакой проверки ошибок. Это вполне легитимная ситуация, если поведение такого преобразования и отсутствие контроля ошибок четко документированы.
