
Неточности и погрешности при работе с Number. Чем это грозит и как решить?
Данная публикация является адаптированным переводом статьи о десятичных числах с моего блога.
Наверное, большинство камрадов сталкивались с непонятными «артефактами» и неточностями при работе с переменными типа Number. Попытайтесь умножить 0.1 на 3 и вы поймете, о чем идет речь. Хотя здравый смысл подсказывает, что результат должен быть 0.3, все же, как оказывается, алгоритмы AS3 не всегда поддаются логике и выдают что-то наподобие 0.30000000000000004.

Оказывается, такая погрешность возникает вследствие ограничения размера памяти, которую AS3 выделяет для хранения переменной Number. Если бы такого ограничения не существовало, банальная попытка сохранить значение числа «Пи» привела бы к полному истощению ресурсов.
К сожалению результатом этого ограничения становятся неприятные погрешности в тех местах, где мы их совсем не ожидали увидеть.
Рассмотрим следующий код:
Результат выполнения:
В первом цикле мы делили числа от 0 до 9 на 10, во втором – умножали на 0.1. Поскольку с точки зрения математики эти действия являются идентичными, результат должен был быть одинаковым. Но, как мы видим, при умножении целого числа на дробное, появились погрешности. Коих нет при использовании целых чисел (даже если результат, как в первом цикле – число с плавающей точкой).
Но бывают случаи, при которых возникновение подобных неточностей очень нежелательно. Именно с этим я столкнулся, занимаясь разработкой своей новой игры. Мне нужно было встроить в пользовательский интерфейс характеристики юнитов, таких как атака, защита и т. п. При этом предполагалось, что значение показателей может быть дробным (одно число после запятой). Каково же было мое удивление, когда вместо аккуратного интерфейса я увидел показатели типа 3.5000000000000001 атаки и 6.00000000000000004 защиты.
Более того, такой метод можно использовать и для округления длинных чисел, если нам надо получить число с ограниченным количеством знаков после запятой. Например, такой метод позволить округлить число «Пи» как до 3.14159 так и до 3.14, оставаясь при этом уверенными, что мы не получим 3.1400000000000001.
Вот как выглядит сам метод (в моем случае он статический, чтобы было удобнее использовать в любом нужном месте):
В двух словах о том, что происходит в методе.
Функция принимает два параметра:
base – это число, которое мы будем округлять;
decimalPlace – это число, кратное 10, которое определяет, сколько знаков после запятой мы желаем получить (10 – 1 знак после запятой; 100 – 2 знака после запятой; 1000 – 3 знака после запятой и так далее).
Допустим, что мы пытаемся округлить число 5.324569 до двух знаков после запятой.
1. roundToDecimal (5.324569, 100);
2. 5.324569 * 100 = 532.4569
3. Math.round(532.4569) = 532
4. 532/100 = 5.32
Как видите, такие нехитрые действия позволяют получить нужный результат при этом гарантируя, что мы получим именно два, а не пятнадцать знаков после запятой.
Буду благодарен, если вы поделитесь в комментариях о том, как решаете проблему погрешностей при работе с Number и в каких случаях это критично для вас (если критично, конечно).
Наверное, большинство камрадов сталкивались с непонятными «артефактами» и неточностями при работе с переменными типа Number. Попытайтесь умножить 0.1 на 3 и вы поймете, о чем идет речь. Хотя здравый смысл подсказывает, что результат должен быть 0.3, все же, как оказывается, алгоритмы AS3 не всегда поддаются логике и выдают что-то наподобие 0.30000000000000004.

Оказывается, такая погрешность возникает вследствие ограничения размера памяти, которую AS3 выделяет для хранения переменной Number. Если бы такого ограничения не существовало, банальная попытка сохранить значение числа «Пи» привела бы к полному истощению ресурсов.
К сожалению результатом этого ограничения становятся неприятные погрешности в тех местах, где мы их совсем не ожидали увидеть.
Рассмотрим следующий код:
var i:int;
var divisor:Number = 10;
var multiplier:Number = 0.1;
for (i = 0; i<10; i++) {
trace (i/divisor);
}
trace("");
for (i = 0; i<10; i++) {
trace (i*multiplier);
}
Результат выполнения:
0
0.1
0.2
0.3
0.4
0.5
0.6
0.7
0.8
0.9
0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6000000000000001
0.7000000000000001
0.8
0.9
В первом цикле мы делили числа от 0 до 9 на 10, во втором – умножали на 0.1. Поскольку с точки зрения математики эти действия являются идентичными, результат должен был быть одинаковым. Но, как мы видим, при умножении целого числа на дробное, появились погрешности. Коих нет при использовании целых чисел (даже если результат, как в первом цикле – число с плавающей точкой).
Зачем это нужно
В большинстве случаев точности алгоритмов AS3 вполне хватит для наших потребностей. Погрешность в 0.0000000000000001 вряд ли окажется критичной.Но бывают случаи, при которых возникновение подобных неточностей очень нежелательно. Именно с этим я столкнулся, занимаясь разработкой своей новой игры. Мне нужно было встроить в пользовательский интерфейс характеристики юнитов, таких как атака, защита и т. п. При этом предполагалось, что значение показателей может быть дробным (одно число после запятой). Каково же было мое удивление, когда вместо аккуратного интерфейса я увидел показатели типа 3.5000000000000001 атаки и 6.00000000000000004 защиты.
Как исправить
Зная, что деления целого числа на кратное десяти никогда не дает погрешностей, мы можем создать метод, который всегда будет возвращать значение с определенным количеством знаков после запятой.Более того, такой метод можно использовать и для округления длинных чисел, если нам надо получить число с ограниченным количеством знаков после запятой. Например, такой метод позволить округлить число «Пи» как до 3.14159 так и до 3.14, оставаясь при этом уверенными, что мы не получим 3.1400000000000001.
Вот как выглядит сам метод (в моем случае он статический, чтобы было удобнее использовать в любом нужном месте):
public static function roundToDecimal (base:Number, decimalPlace:int):Number
{
return Math.round(base * decimalPlace)/decimalPlace;
}
В двух словах о том, что происходит в методе.
Функция принимает два параметра:
base – это число, которое мы будем округлять;
decimalPlace – это число, кратное 10, которое определяет, сколько знаков после запятой мы желаем получить (10 – 1 знак после запятой; 100 – 2 знака после запятой; 1000 – 3 знака после запятой и так далее).
Допустим, что мы пытаемся округлить число 5.324569 до двух знаков после запятой.
1. roundToDecimal (5.324569, 100);
2. 5.324569 * 100 = 532.4569
3. Math.round(532.4569) = 532
4. 532/100 = 5.32
Как видите, такие нехитрые действия позволяют получить нужный результат при этом гарантируя, что мы получим именно два, а не пятнадцать знаков после запятой.
Буду благодарен, если вы поделитесь в комментариях о том, как решаете проблему погрешностей при работе с Number и в каких случаях это критично для вас (если критично, конечно).
- +11
- jarofed
Комментарии (15)
Проблема решается использованием целых чисел.
В подавляющем большинстве случаев нам необходима только определенная точность (в среднем 2-3 знака после запятой), поэтому достаточно хранить x*100 и все.
Это из личного опыта, как у других — не знаю.
abs(a-b) < 0.00000001
правда там погрешность вызывалась уже при сложении, если оно производилось в цикле