Home » Создание сайтов и программирование » Почему Round "раундит" "не правильно" или все про округление в .NET

Почему Round "раундит" "не правильно" или все про округление в .NET

Как Вы думаете, какой результат получится в результате выполнения этого кода:

Если Вы думаете, что получится 4 и 5, то ошибаетесь. В обоих случаях результатом будет 4. Тот же самый результат Вы получите и в Visual Basic 6.0. В .NET статический метод Round() класса Math округляет половину к ближайшему четному. В школе же нас учили, что половина всегда округляется в большую сторону. Поэтому многие (и я в том числе) очень удивляются, узнав о таком “неправильном” округлении. Часто незнание этого факта может привести к неправильным расчетам, если алгоритмы преполагают “обычное” округление.

Что же это такое? Очередной баг Microsoft? Вовсе нет! Просто существует несколько способов округления.

Округление в меньшую сторону

Простейший случай – когда цифры после заданной точности просто отбрасываются (округляем до целого):

3.9 округляется до 3
-3.9 округляется до -3

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

С учетом знака -3.9 округляется до -4. Метод Floor() класса Math производит несимметричное округление:

Округление в большую сторону

Несимметричное округление до ближайшего большего или равного целого выполняет метод Ceiling() класса Math:

Заметьте, что Floor() и Ceiling() округляют всегда до целого.

Но чаще всего приходится округлять не в меньшую или большую сторону, а к ближайшему числу (с заданной точностью). В этом случае погрешность будет меньшей. Главный вопрос, который возникает в этом случае – как округлять половину?

Арифметическое округление

Это привычное нам округление, когда половина округляется в большую сторону:

3.5 округляется до 4
4.5 округляется до 5

Как и в предыдущих случаях, можно рассматривать симметричное и несимметричное арифметическое округление.

Банковское округление

Если складывать много чисел, округляя .5 всегда в большую сторону, то возникнет перекос, который будет тем больше, чем больше чисел мы складываем. Банковское округление позволяет минимизировать этот перекос. В этом случае половина округляется к ближайшему четному. Метод Round() класса Math реализует именно банковское округление. В качестве параметра он принимает округляемое значение и, возможно, точность, до которой необходимо выполнить округление. Если точность не указана, то округление выполняется до целого.

Случайное округление

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

Попеременное (alternate) округление

В этом случае .5 округляется попеременно то в большую сторону, то в меньшую.

Что же делать, если надо произвести арифметическое округление? К сожалению, Microsoft не реализовала соответствующий метод в классе Math (так же, как и для случайного и попеременного округлений). Но выход, конечно, есть.

Во первых, это класс SqlDecimal из пространства имен System.Data.SqlTypes со статическим методом Round:

Это отличие связано с тем, что Round из Sql Server выполняет математическое округление.

Во вторых, можно самостоятельно написать метод для математического (и не только) округления. Например, вот реализация на C# для симметричного арифметического округления.

Я предпочитаю поместить несколько перегрузок приведенной реализации (для float, double, decimal) в некоторый служебный класс и использовать их вместо Math.Round() (мне пока не приходилось реализовывать задачи, требующие банковского округления).

Реализацию описанных алгоритмов округления на Visual Basic 6.0 можно найти здесь: http://support.microsoft.com/default.aspx?scid=kb;en-us;196652

Добавлено 25.10.2005

В .NET 2.0 метод Round() класса Math имеет перегрузки с параметром mode типа MidpointRounding, определяющим, как будет округляться половина. MidpointRounding может принимать два значения:

  • AwayFromZero – половина округляется к ближайшему числу, которое дальше от нуля (т.е обыкновенное математическое симметричное округление).
  • ToEven – округление половины к ближайшему четному – единственная текущая реализация Round(). Естественно, если параметр mode не задан, по умолчанию используется ToEven (для совместимости).
  • Rometsss

    Пора мелкософту гнать своих индусов с их “восхитительной” логикой и миропредставлением :)
    А потом у них винда и прочие приложения глючат почемуто…
    Обычная арифметика: 30-20=10, как у десяти может быть середина? там с одной стороны 0-4 (5 цифр) и с другой 5-9 (5 цифр)!

    • А при чем здесь индусы? Просто есть разные способы округления, о которых я рассказал в статье, и в первой версии .NET использовался не тот, к котрому привыкли мы. В этом нет ничего страшного, если знать.

      Ноль после запятой не учитывается, так что цифр, которые надо округлять, остается 9. И цифра 5 – середина.

  • Dasistgut

    А если ещё немножко подумать, то округлять надо результат, а не слагаемые.

    • А если еще подумать, то можно понять, что слагаемые, например, могут уже храниться в базе. В любом случае, результат можно округлять по разному (о чем и идет речь в статье).

  • Venom

    > простым привидением 

  • tarenych

    есть еще волшебные округления чисел в/за пределами 32768
    пример
    32767.075 32767.08
    32768.075 32768.07

    так округляет и Math.Round(32768.075, 2, MidpointRounding.AwayFromZero) и public static double Round(32768.075, 2)

    • tarenych

      это связано с хранением чисел с плавающей точкой, поэтому лучше использовать Decimal.Round(32768.075, MidpointRounding.AwayFromZero) = 32767.08