6

Почему команда >? Math.Round(0.45f, 1, MidpointRounding.AwayFromZero) возвращает 0.4 , а команда >? Math.Round(0.45d, 1, MidpointRounding.AwayFromZero) 0.5 ?

Команды выполнены в окне команд Visual Studio 2008, версия .Net Framework 3.5

4per
  • 2,696

2 Answers2

10

Дело в том, что у 0.45 нет точного представления в двоичном виде. Это приводит к потерям точности даже там, где разработчик ожидает более точного результата.

В вашем случае происходит upcast float-значения 0.45 к double.

0.45f выглядит примерно так

0 01111101      1100 11001100 11001100 110 

Это примерно 0.449999988079071044921875.

.NET знает что точность float недостаточна, и при преобразовании в строку использует 7 значащих цифр. 0.4499999 88 отображается как 0.45. Поэтому и в отладке, и при выводе на консоль вы увидите 0.45.

0.45d - представляется примерно так:

0 0111111 1101  1100 11001100 11001100 11001100 11001100 11001100 11001101

Что примерно равно 0.450000000000000011102230246252. Для double при преобразовании в строку .NET использует 14 значащих цифр. Значение представляется как 0.45000000000000 00 -> 0.45

Ок, теперь вы даете процессору 0.45f и просите его сконвертировать его в double. Процессор берет и добивает нулями:

0 0111111 1101  1100 11001100 11001100 11000000 00000000 00000000 00000000

Что все еще равно 4.49999988079071044921875

С точки зрения процессора - точность не пострадала. Но теперь это double, и при операциях с ним предполагается что он достаточно точен, чтобы при работе с ним как с десятичным числом захватить побольше цифр.

Это больше не "примерно 0.45". Это ведь double. Теперь в нем примерно 14 значащих цифр - значение с точки зрения .NET и процессора превратилось в "примерно 0.449999988079071".

Т.е. в двоичном представлении точность не потерялась, в десятичном - тоже - но полученное число теперь отличается от 0.45d, что приводит к округлению в неожиданном направлении.


Почему работает аналогичный вызов с приведением к decimal:

Math.Round((Decimal)0.45f, 1, MidpointRounding.AwayFromZero) // 0.5 

Приведение в decimal происходит через системный вызов метода VarDecFromR4, который при конвертации округляет float до 7 значащих десятичных цифр, делая из 0.449999988079071044921875 красивое 0.4500000:

// Round the input to a 7-digit integer.  The R4 format has
// only 7 digits of precision, and we want to keep garbage digits
// out of the Decimal were making.

Собственно, сам код преобразования (пара экранов) в основном посвящен этому округлению с последующим отрезанием незначащих нулей.

  • Для чего же происходит этот upcast? Почему нельзя сразу округлять float? – 4per Jul 12 '16 at 06:45
  • 1
    Всё понял! просто нет перегрузки Math.Round для float – 4per Jul 12 '16 at 06:47
  • 1
    @3per: А даже если бы и была — в какую сторону ей округлять 0.449999988079071044921875? Это не midpoint, это строго меньше середины. – VladD Jul 13 '16 at 17:22
  • @VladD, посмотрите код в моём ответе. Если оно строго меньше середины, почему при конвертации в Decimal, округляется "правильно"? – 4per Jul 14 '16 at 00:03
  • @3per: Судя по всему, при конвертации в decimal произошло округление до в точности 3.45. – VladD Jul 14 '16 at 09:47
  • @VladD, ну т.е. вопрос решаем... – 4per Jul 14 '16 at 23:49
  • @3per: Или повезло в данном конкретном случае. – VladD Jul 15 '16 at 01:21
  • @Vlad, значит мне следует провести исследование... – 4per Jul 15 '16 at 01:26
  • Думаю, исследование не нужно. Если вы работаете с плавающей точкой, нет смысла надеяться на попадание в точное значение, вот и всё. А если вам нужны точные десятичные знаки (скорее всего, нет), о нужно отказаться от double и пользоваться исключительно decimal. – VladD Jul 15 '16 at 06:56
1

Из https://stackoverflow.com/a/8240013/5574962 Для того чтобы обойти проблему можно применять такой подход. Такие вычисления, конечно, медленнее.

Math.Round((Decimal)0.45f, 1, MidpointRounding.AwayFromZero)

Возвращает 0.5

4per
  • 2,696