Перш, ніж я перейду до опису суті проблеми, дозвольте мені запропонувати вам просте математичне завдання. Скільки буде 3 помножити на 0.1?
Дивне запитання! Навіть першокласник відповість, що результатом цього виразу буде 0.3. Втім, як виявилося, алгоритми ActionScript 3 не завжди дружать з традиційною логікою. Якщо ви спробуєте провести подібне обчислення у AS3, то отримаєте результат: 0.30000000000000004.
Подібна похибка виникає через обмеження розміру пам’яті, яку AS3 виділяє для збереження змінних типу Number (тобто чисел з комою). Наприклад, щоб точно зберегти значення числа “Пі”, системі знадобилася б безкінечна кількість ресурсів. Звісна річ, що це неможливо. Тому в AS3 (як і в будь-якій іншій мові програмування) під кожен тип змінної відведена строго обмежена кількість байтів. Отже так чи інакше, доводиться округлювати “безкінечні” числа.
На жаль, наслідки такого скорочення іноді проявляються навіть там, де ми їх не очікували побачити.
Давайте розглянемо наступний код:
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 до 9 на 10, а в другому – множимо той самий ряд чисел на 0.1. Оскільки результат ділення на 10 повинен бути ідентичним до результату множення на 0.1 – ми повинні бачити однаковий ряд чисел. Втім, при виконанні цього коду отримаємо наступний результат:
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.1) ми отримали кілька “артефактів” у вигляді незначних похибок. Спрогнозувати їх появу практично неможливо, але одне можна сказати напевне – при роботі з типом Number у ActionScript час від часу будуть проявлятися подібні неточності.
Втім, не менш важливо звернути увагу і на те, що при діленні ряду цілих чисел на 10 (у першому циклі) жодних похибок ми не отримали.
Пояснюється це просто: в першому випадку обидва числа були цілими. А в другому ми множили ціле число на десятковий дріб, що призвело до некоректної роботи алгоритму і вилилося у похибки.
Як виправити неточності при роботі з десятковими дробами
У більшості випадків точність алгоритмів AS3 буде для нас повністю задовільною. Адже погодьтеся, похибка у 0.0000000000000001 не вплине на фінальний результат роботи нашої програми.
Але бувають ситуації, в яких виникнення подібних “артефактів” все-таки дуже небажане. І з однією із них я зіткнувся, працюючи над грою Symbiosis.
Уявіть, що вам потрібно показувати гравцеві певні характеристики його юнітів (наприклад, атаку чи захист) із точністю до одного числа після коми. Зрозуміла річ, що відображення цих характеристик буде вбудовано у графічний інтерфейс. І уявіть, що в полі, де повинна показуватися атака, припустимо 3.5, раптом з’явиться число 3.5000000000000001. Неприємний глюк, чи не так?
Округлення числа Number до десяткового дробу з визначеною кількістю знаків після коми
Знаючи, що ділення цілого числа на ціле число кратне десяти ніколи не дає похибок, ми можемо створити функцію, яка завжди повертатиме коректне значення з визначеною кількістю знаків після коми.
До речі, цю саму функцію можна використовувати замість штатного в ActionScript 3 методу Math.round, який округлює числа до цілого значення. В нашому випадку функція повертатиме контрольоване дробове значення. Наприклад, таким чином ми зможемо округлити число “Пі”, як до 3.14159, так і до 3.14, тоді як Math.round завжди повертатиме строго 3.
Ось як виглядає сама функція:
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 до двох знаків після коми. В такому випадку ми повинні передати методу відповідні параметри:
roundToDecimal (5.324569, 100);
base = 5.324569
decimalPlace = 100
Розглянемо, що відбувається далі:
1. Перша дія – множення base на decimalPlace: 5.324569 * 100 = 532.4569
2. Після цього в методі використовується штатний Math.round щоб округлити число. Отримуємо 532
3. Наступний крок – ділення на decimalPlace: 532/100 = 5.32
Як бачите, ми отримали потрібний результат – десятковий дріб з двома знаками після коми – при цьому не зіткнувшись з жодними “вибриками” AS3 у вигляді незрозумілих похибок.
Можна проапгрейдити функцію roundToDecimal, щоб замість не зовсім очевидного числа кратного десяти вказувати конкретну кількість знаків після коми (1, 2, 3 … і т. п.). Ось як вона виглядатиме:
public static function roundToDecimal (base:Number, decimalPlace:int):Number { var divisor:int = Math.pow(10, decimalPlace); return Math.round(base * divisor)/divisor; }
Функція Math.pow() повертає значення першого числа – 10 – піднесеного до ступеню decimalPlace. Таким чином, якщо decimalPlace = 1 – отримуємо 10; 2 – 100; 3 – 1000 і так далі.
Сподіваюся, ця стаття була корисною і допомогла розібратися з деякими похибками типу Number, які виникають у AS3. Якщо у вас залишилися запитання щодо роботи функцій – ласкаво прошу у коментарі.
Ну і традиційно, буду радий, якщо ви станете постійними читачами блога про флеш, підписавшись на RSS.
Грудень 12, 2011 о 19:46
якщо це потрібно тільки для відображення, то у Number є така функція:
AS3 function toFixed(fractionDigits:uint):String
Returns a string representation of the number in fixed-point notation. Fixed-point notation means that the string will contain a specific number of digits after the decimal point, as specified in the fractionDigits parameter. The valid range for the fractionDigits parameter is from 0 to 20.