Как .NET Math.Round не имеет ничего общего с математикой. И это нормально!
Прежде чем мы начнем, представьте, какие значения будут иметь "a" и "b" после выполнения следующего:
a = Math.Round(1.5);
b = Math.Round(2.5);
Если ваш ответ «a = 2 и b = 3», вы ошибаетесь. Правильный ответ таков: и a, и b становятся 2. Смущены? Возможно, вы захотите продолжить чтение и узнать почему.
Бэкграунд
Около недели назад один разработчик работал над кодом, который перемещал два объекта вдоль оси x. Внутри библиотеки их значение x было двойным, но общедоступный интерфейс допускал только метры в виде целого числа. Разработчик использовал Math.Round для перевода двойного значения в это целое число.
Одним из требований к коду было то, что объекты никогда не могли быть ближе, чем на один метр друг к другу. Разработчик использовал округленное значение, чтобы проверить это.
Как вы уже догадались, код продолжал давать сбои. Несмотря на то, что оба объекта начинали ровно на расстоянии одного метра, имели одинаковую начальную скорость и ускорялись с одинаковой скоростью, используя один и тот же алгоритм, целочисленное значение время от времени возвращало одно и то же значение x, и, следовательно, код продолжал давать сбой.
Сначала казалось, что это какая-то ошибка точности в результате использования двойных значений. Но это не имеет большого смысла, так как алгоритм приведет к одинаковой потере точности в обоих значениях. После некоторой отладки в конце концов было обнаружено, что это был не двойник, а Math.Round, который вел себя не так, как ожидалось.
Банковское округление
После того, как выяснилось, что проблема была с использованием Math.Round, я быстро понял проблему: реализация по умолчанию для округления в .NET - это «округление от половины до четного» или «Банковское округление». Это означает, что значения средней точки округляются до ближайшего четного числа. В примере, который мы привели во введении, это означает, что оба значения округляются до 2, ближайшего четного числа.
Так почему же это было реализовано в .NET? Это ошибка?
Что ж, при первом внедрении Microsoft следовала стандарту IEEE 754. Это является причиной реализации по умолчанию Math.Round. (Примечание: текущий стандарт IEEE 754 содержит пять правил округления)
Другая веская причина заключается в том, что банковское округление не страдает от отрицательного или положительного смещения так же, как округление наполовину от нулевого метода по большинству разумных распределений.
Округление до 0,5
Но еще не все потеряно.
Если вы ожидаете, что 1,5 будет округлено до 2, а 2,5 округлено до 3 (как ожидалось), метод Math.Round имеет переопределение, которое позволяет вам вместо этого использовать метод «Округление до 0,5».
a = Math.Round(1.5, MidpointRounding.AwayFromZero);
b = Math.Round(2.5, MidpointRounding.AwayFromZero);
В приведенном выше фрагменте кода 1,5 округляется до 2, а 2,5 округляется до 3.
Округление вверх
Интересно, однако, что после проведения некоторых исследований, похоже, что «округление до 0,5» также не является методом, используемым в математике. Вместо этого в математике обычно используется метод «Округление вверх» . Для положительных чисел нет никакой разницы, но для отрицательных чисел средние значения округляются в сторону + ∞, а не от 0.
Таким образом, есть одинаковое количество дробей, округляемых до нуля, в то время как в методе «Округление до 0,5» и 0,5, и -0,5 не округляются до нуля, что делает ноль исключением из всех других чисел.
Math ≠ математика
Таким образом, оказывается, что метод Math.Round даже не поддерживает фактический метод округления, который обычно используется в математике.
Когда наш разработчик впервые обнаружил это, он был очень разочарован. В конце концов, даже при наличии веских причин, библиотека по-прежнему называется Math. Похоже, он сообщает определенное намерение.
Но написание этой статьи заставило задуматься: прежде чем заглянуть в нее: оказывается существует так много методов округления. У каждого свои плюсы и минусы. У каждого свои последствия. Если бы я он не знал обо всех этих методах, он бы точно не знал и об этих последствиях. Так что, может быть, это не так уж плохо, что кто-то другой сделал это и принял соответствующее решение по умолчанию.
В конце концов, если последствия вашего округления настолько важны для вашего кода, надеемся, что вы не сделаете те же предположения, или, по крайней мере, обнаружите, что они были неверны в некоторых старомодных тестах.
Завершение
Несмотря на то, что этот предмет на самом деле не является слишком сложным для понимания, надеемся, что вы узнали что-то новое или, по крайней мере, получили удовольствие от чтения.
В конце концов, мы думаем, что это хорошее напоминание о том, насколько сложными могут быть даже кажущиеся простыми вещи, которые мы используем каждый день. И здесь, вероятно, есть урок о предположениях.