前言先说清楚版本fdiv()确实是PHP 8.0引入的函数这个标题里的版号没问题。它和 PHP 8.0 的另一项改动是配套出现的——从 PHP 8.0 起1 / 0不再像 PHP 7 那样发个 Warning 然后给你 INF而是直接抛DivisionByZeroError。很多从 7.x 升上来的项目升级后第一批发现在线的 500就是除零导致的。但业务里确实存在除数是 0 也要算出一个值的场景评分、增长率、权重、单价。用fdiv()就能拿到一个 IEEE 754IEEE 二进制浮点算术标准意义上的结果——INF、-INF或NAN——而不抛异常。问题随之而来INF和NAN不是能直接用的值。它们能参与运算、能比较、能输出但一旦进入 JSON、数据库、模板或者前端展示行为就变得非常反直觉。本文讲清楚三件事fdiv()到底返回什么、INF/NAN在 PHP 各处的真实表现、以及一套能落地的处理范式。一、fdiv()与除零四个 API 的差别分子分母都是 0 或者分母为 0 时PHP 里四个除法类函数的行为完全不同这是最容易记混的地方。写法版本除数为 0 时返回类型$a / $bPHP 8.0 起抛DivisionByZeroErrorint或floatintdiv($a, $b)PHP 7.0抛DivisionByZeroErrorintfmod($a, $b)PHP 8.0 起对 0 抛错抛DivisionByZeroErrorfloatfdiv($a, $b)PHP 8.0不抛错返回INF/-INF/NANfloatfdiv()的签名是fdiv(int|float $num1, int|float $num2): float永远返回float永远不抛异常。它返回什么完全遵循 IEEE 754正数除以正零 →INF正无穷负数除以正零或正数除以负零→-INF负无穷零除以零 →NANNot a Number非数溢出比如fdiv(1e308, 1e-10)→INF同样不报错?php // 需要 PHP 8.0 var_dump(fdiv(1, 2)); // float(0.5) var_dump(fdiv(1, 0)); // float(INF) var_dump(fdiv(-1, 0)); // float(-INF) var_dump(fdiv(0, 0)); // float(NAN) var_dump(fdiv(1.0, 0.0)); // float(INF) —— 浮点零一样 // 对比PHP 8.0 起这些都会抛 DivisionByZeroError try { $x 1 / 0; } catch (DivisionByZeroError $e) { echo $e-getMessage(), PHP_EOL; // Division by zero }注意最后一个例子1 / 0抛异常是PHP 8.0的行为。在 PHP 7.x 上它是 Warning: Division by zero 加一个INF。所以判断线上会不会 500之前先确认线上跑的是哪个大版本。二、INF和NAN为什么难缠INF与NAN都是合法的float值is_float()对它们返回true。麻烦在于它们不满足普通数值的直觉1.NAN不等于自己。这是 IEEE 754 的硬规定不是 PHP 的 bug?php var_dump(NAN NAN); // bool(false) var_dump(NAN NAN); // bool(false) var_dump(in_array(NAN, [NAN])); // bool(false)默认宽松比较 var_dump(NAN NAN); // int(1) —— 排序时会出现诡异结果所有涉及NAN的比较都是false除了!。所以唯一正确的判断方式是is_nan()。2.INF参与运算是污染式的。一旦进入计算链结果就再也不是有限数?php var_dump(INF 1); // float(INF) var_dump(INF - INF); // float(NAN) —— 无穷减无穷是 NAN var_dump(INF * 0); // float(NAN) var_dump(1 / INF); // float(0) —— 注意这里不抛错因为 INF 不是零 var_dump(is_finite(INF)); // bool(false)3. 三个判定函数必须用对函数INF-INFNAN普通数is_infinite()truetruefalsefalseis_nan()falsefalsetruefalseis_finite()falsefalsefalsetrue注意is_infinite()对-INF也返回true它只判是不是无穷不判方向要判方向得用$v 0/$v 0。另外is_finite()等价于!is_infinite($v) !is_nan($v)但它一次就够语义也更清楚。4.INF/NAN进 JSON 会直接失败。这是线上最痛的一环?php $data [ratio fdiv(1, 0)]; // INF var_dump(json_encode($data)); // bool(false) var_dump(json_last_error_msg()); // string(28) Inf and NaN cannot be JSON encoded // PHP 7.3 可以让它抛异常比返回 false 好排查 try { json_encode($data, JSON_THROW_ON_ERROR); } catch (JsonException $e) { echo $e-getMessage(), PHP_EOL; } // JSON_PARTIAL_OUTPUT_ON_ERROR 会把非法值替换成 0看起来成功了 // 但数据被悄悄改了 —— 这才是最难查的那种 bug var_dump(json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR)); // {ratio:0}json_encode()在遇到非法值INF/NAN或非法 UTF-8 字符串时返回false而很多框架的响应层并不检查返回值于是接口返回空 body 或者截断的 JSON前端JSON.parse报错排查方向还会跑偏到网络问题上去。三、实战一处收敛的安全除法正确的做法不是到处写if ($b 0)而是把非法值在边界上一次性处理掉入口处用fdiv()拿到结果立刻用一个函数把它规整成可输出的形态。?php // safe-divide.php —— 需要 PHP 8.0 // 用法php safe-divide.php /** * 安全除法永不抛异常永远返回可安全序列化的值。 * * param int|float $a 被除数 * param int|float $b 除数 * param float|null $fallback 结果为 INF/-INF/NAN 时的兜底值null 表示保留原值 */ function safeDiv(int|float $a, int|float $b, ?float $fallback 0.0): float { $result fdiv($a, $b); if (!is_finite($result)) { return $fallback ?? $result; } // 顺手处理 -0.0它在 JSON 里会变成 -0 return $result 0.0 ? 0.0 : $result; } /** 把任意浮点值归一化成 JSON 安全的值 */ function jsonSafe(mixed $value): mixed { if (is_float($value) !is_finite($value)) { return null; // 或者 0取决于业务语义 } if (is_array($value)) { return array_map(jsonSafe, $value); } return $value; } $cases [ [1, 2], [1, 0], [-1, 0], [0, 0], [1e308, 1e-10], [7.5, 2.5], ]; foreach ($cases as [$a, $b]) { $raw fdiv($a, $b); printf( fdiv(%s, %s) %-6s is_finite%s\n, var_export($a, true), var_export($b, true), var_export($raw, true), var_export(is_finite($raw), true) ); } $metrics [ hit_rate safeDiv(120, 0), // 除零 - 兜底 0.0 growth safeDiv(50, 200), // 正常 - 0.25 zero_zero safeDiv(0, 0), // NAN - 兜底 0.0 ]; echo PHP_EOL, safe: , json_encode($metrics, JSON_UNESCAPED_UNICODE), PHP_EOL; // 不加防护时的后果json_encode 直接失败 try { echo json_encode([growth fdiv(1, 0)], JSON_THROW_ON_ERROR), PHP_EOL; } catch (JsonException $e) { echo JSON 失败: , $e-getMessage(), PHP_EOL; }输出大致如下fdiv(1, 2) 0.5 is_finitetrue fdiv(1, 0) INF is_finitefalse fdiv(-1, 0) -INF is_finitefalse fdiv(0, 0) NAN is_finitefalse fdiv(1.0E308, 1.0E-10) INF is_finitefalse fdiv(7.5, 2.5) 3.0 is_finitetrue safe: {hit_rate:0,growth:0.25,zero_zero:0} JSON 失败: Inf and NaN cannot be JSON encoded这段代码的关键点有三个一是在唯一出口做归一化jsonSafe挂在响应层二是兜底值可配置业务上0和null语义完全不同前者表示零后者表示无法计算三是绝不依赖json_encode()的false返回值而是显式检查。对入库场景同样的思路INF写进DECIMAL列会报错或被截断所以入库前也必须过一遍is_finite()。表结构上也要注意INF需要DOUBLE/FLOAT才能存且 MySQL 中-INF与INF的比较行为与 PHP 一致INF 任何有限值但排序时NAN的表现依赖具体版本不要依赖它。常见坑点1. 用 0判断除数漏掉-0.0和浮点误差// ❌ 看似安全其实 -0.0 0 为 true 会被拦下但浮点误差产生的极小值不会 if ($b 0) { return 0; } $r $a / $b; // $b 1e-320 时结果可能是 INF仍然抛不抛错取决于溢出// ✅ 交给 fdiv 拿值再用 is_finite 判定这个结果能不能用 $r fdiv($a, $b); return is_finite($r) ? $r : $fallback;2. 用$x NAN或in_array($x, [...])判 NAN// ❌ 永远为 false等于这段判断完全没生效 if ($value NAN) { $value 0; }// ✅ 只有一个正确写法 if (is_nan($value)) { $value 0; }3. 直接用is_infinite()判正负无穷把方向搞丢// ❌ 只判是不是无穷实际上可能是个负数 if (is_infinite($rate)) { $rate 999; }// ✅ 先判有限性再按方向给兜底 if (!is_finite($rate)) { $rate is_infinite($rate) $rate 0 ? PHP_FLOAT_MAX : 0.0; }4. 把INF存进数据库INT/DECIMAL列// ❌ 取决于列类型与服务端 SQL 模式可能报错也可能被截成 0 或最大值 $pdo-prepare(INSERT INTO stats (rate) VALUES (?))-execute([fdiv(1, 0)]);// ✅ 入库前统一归一到有限值并明确 NULL 的语义 $rate fdiv(1, 0); $stmt $pdo-prepare(INSERT INTO stats (rate) VALUES (?)); $stmt-execute([is_finite($rate) ? $rate : null]);5. 以为is_float()能过筛把INF放进 JSON// ❌ 类型检查通过序列化时炸掉 if (is_float($v)) { $out[rate] $v; }// ✅ 类型对不代表值可用序列化前必须做值域检查 if (is_float($v) is_finite($v)) { $out[rate] $v; }6. 用round()/number_format()指望把INF修好// ❌ INF 经格式化后仍不是合法数值语义且行为依赖版本别指望它兜底 echo number_format(fdiv(1, 0), 2);// ✅ 先归一化再格式化 $v fdiv(1, 0); echo is_finite($v) ? number_format($v, 2) : N/A;7. 用JSON_PARTIAL_OUTPUT_ON_ERROR掩盖问题导致数据静默变成 0// ❌ 序列化成功了但 INF 被替换成 0业务上完全说不通还查不出原因 echo json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR);// ✅ 让问题在开发/测试环境直接抛出来生产环境则先归一化再序列化 try { echo json_encode(jsonSafe($data), JSON_THROW_ON_ERROR); } catch (JsonException $e) { error_log(响应序列化失败: . $e-getMessage()); http_response_code(500); }8. 把fdiv()当唯一手段忘了0 / 0在业务上是无定义而不是零// ❌ 0/0 被兜底成 0前端显示命中率 0%掩盖了根本没有流量这个事实 $hitRate safeDiv($hits, $total, 0.0);// ✅ 区分除数为零和确实为零用 null 表达不可计算 $hitRate $total 0 ? null : safeDiv($hits, $total);总结关注点结论fdiv()版本PHP 8.0 引入标题版号正确除数为 0 时的返回值INF/-INF/NAN不抛异常1 / 0的行为PHP 8.0 起抛DivisionByZeroErrorPHP 7 是 Warning INF判定无穷 / 非数is_finite()一把梭判NAN只能is_nan()NAN的比较与任何值包括自己比较都为false进 JSONjson_encode()返回false需先归一化或显式抛错进数据库必须先is_finite()过滤用NULL表达不可计算结论很简单fdiv()解决的是不要因为除零而炸掉这一个问题它不解决无穷大怎么展示、怎么入库、怎么返回给前端这些问题。把fdiv()的调用点收敛到少数几个函数里并在这些函数的出口统一做is_finite()归一化才是能长期维护的做法。反过来在业务代码各处零散地写fdiv()再各处补判断用不了多久就会漏掉一个出口然后以接口偶发返回空的形式出现在线上。