這是經典的浮點數精度問題,不僅存在於 PHP,幾乎所有採用 IEEE 754 標準的程式語言都會遇到。本文將透過實際程式碼展示問題現象,深入解析根本原因,並提供生產環境可用的解決方案。
$f = 0.58; var_dump(intval($f * 100 * 100)); // 輸出:int(5799) 預期 5800 var_dump((float)($f * 100 * 100)); // 輸出:float(5800) echo (int)((0.1 + 0.7) * 10); // 輸出:7 預期 8 echo (float)((0.1 + 0.7) * 10); // 輸出:8 $a = 0.1; $b = 0.7; var_dump(($a + $b) == 0.8); // 輸出:bool(false)!
十進位的 0.1、0.7 等數值在二進位中無法精確表示,會產生無限循環小數:
0.1 (十進位) = 0.00011001100110011... (二進位,0011 無限循環) 0.7 (十進位) = 0.1011001100110011... (二進位,0011 無限循環)
電腦以有限位數(雙精度 52 位尾數)儲存,必須捨入,導致微小誤差。這些誤差在運算中累積,最終導致看似「不可能」的結果。
$val = 0.58 * 100; // 實際為 57.999999... echo intval($val); // 57!不是 58 // 正確做法:先四捨五入 echo intval(round($val)); // 58
| 方案 | 適用場景 | 優點 | 缺點 |
|---|---|---|---|
| round() / number_format() | 顯示層面、簡單計算 | 內建函式、效能好 | 本質仍為浮點數、複雜運算仍誤差 |
| BCMath 擴展 (bcadd, bcmul...) | 財務、金融、高精度需求 | 任意精度、字串運算、無精度損失 | 需安裝擴展、效能較低、語法較繁瑣 |
| GMP 擴展 | 大整數運算、密碼學 | 極高效能、支援大數 | 僅支援整數、需安裝擴展 |
| 封裝類別 (MoneyPHP 等) | 領域驅動設計、貨幣處理 | 封裝邏輯、型別安全、可測試 | 引入依賴、學習成本 |
$a = '0.1'; $b = '0.7'; // 加法 $result = bcadd($a, $b, 2); // '0.80',第3參數為小數位數 var_dump($result == '0.80'); // bool(true) // 乘法 $price = '19.99'; $quantity = '3'; $total = bcmul($price, $quantity, 2); // '59.97' // 比較 $expected = '59.97'; var_dump(bccomp($total, $expected, 2) === 0); // bool(true) // 除法(需指定小數位) $perItem = bcdiv($total, $quantity, 2); // '19.99'
bcadd($a, $b, $scale):加法bcsub($a, $b, $scale):減法bcmul($a, $b, $scale):乘法bcdiv($a, $b, $scale):除法bccomp($a, $b, $scale):比較(-1/0/1)bcmod($a, $b):取模bcpow($a, $b, $scale):冪運算bcscale($scale):設定預設小數位數abs($a - $b) < 0.0001 容差判斷number_format($val, 2, '.', '') 格式化intval() 前必加 round(),floor()/ceil() 前考慮加微小偏移量class PrecisionMath {
private static $scale = 10;
public static function setScale($scale) {
self::$scale = $scale;
bcscale($scale);
}
public static function add($a, $b) {
return bcadd((string)$a, (string)$b, self::$scale);
}
public static function mul($a, $b) {
return bcmul((string)$a, (string)$b, self::$scale);
}
public static function equals($a, $b) {
return bccomp((string)$a, (string)$b, self::$scale) === 0;
}
// 更多方法...
}
// 使用
PrecisionMath::setScale(4);
$total = PrecisionMath::mul('19.99', '3');
echo $total; // 59.9700
浮點數精度問題源於二進位表示的本質限制,非語言 Bug。理解其成因、識別高風險場景(金融、比較、轉整數),並善用 BCMath 或整數分儲存策略,是 PHP 開發者的必修課。記住:涉及金錢,拒絕浮點數;涉及比較,設定容差門檻;涉及轉整,必先四捨五入。