☰
PHP 8.0中fdiv函数计算浮点数除法时无穷大结果怎么处理
2026/9/30 5:06:41 网站建设 项目流程

前言

先说清楚版本:fdiv()确实是PHP 8.0引入的函数,这个标题里的版号没问题。它和 PHP 8.0 的另一项改动是配套出现的——从 PHP 8.0 起,1 / 0不再像 PHP 7 那样"发个 Warning 然后给你 INF",而是直接抛DivisionByZeroError。很多从 7.x 升上来的项目,升级后第一批发现在线的 500,就是除零导致的。

但业务里确实存在"除数是 0 也要算出一个值"的场景:评分、增长率、权重、单价。用fdiv()就能拿到一个 IEEE 754(IEEE 二进制浮点算术标准)意义上的结果——INF、-INF或NAN——而不抛异常。

问题随之而来:INF和NAN不是能直接用的值。它们能参与运算、能比较、能输出,但一旦进入 JSON、数据库、模板或者前端展示,行为就变得非常反直觉。本文讲清楚三件事:fdiv()到底返回什么、INF/NAN在 PHP 各处的真实表现、以及一套能落地的处理范式。

一、fdiv()与除零:四个 API 的差别

分子分母都是 0 或者分母为 0 时,PHP 里四个"除法类"函数的行为完全不同,这是最容易记混的地方。

写法版本除数为 0 时返回类型
$a / $bPHP 8.0 起抛DivisionByZeroErrorint或float
intdiv($a, $b)PHP 7.0+抛DivisionByZeroErrorint
fmod($a, $b)PHP 8.0 起对 0 抛错抛DivisionByZeroErrorfloat
fdiv($a, $b)PHP 8.0+不抛错,返回INF/-INF/NANfloat

fdiv()的签名是fdiv(int|float $num1, int|float $num2): float,永远返回float,永远不抛异常。它返回什么,完全遵循 IEEE 754:


  • 正数除以正零 →INF(正无穷)

  • 负数除以正零(或正数除以负零)→-INF(负无穷)

  • 零除以零 →NAN(Not 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()truetruefalsefalse
is_nan()falsefalsetruefalse
is_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_finite=true fdiv(1, 0) => INF is_finite=false fdiv(-1, 0) => -INF is_finite=false fdiv(0, 0) => NAN is_finite=false fdiv(1.0E+308, 1.0E-10) => INF is_finite=false fdiv(7.5, 2.5) => 3.0 is_finite=true 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 起抛DivisionByZeroError(PHP 7 是 Warning +INF)
判定无穷 / 非数is_finite()一把梭;判NAN只能is_nan()
NAN的比较与任何值(包括自己)比较都为false
进 JSONjson_encode()返回false,需先归一化或显式抛错
进数据库必须先is_finite()过滤,用NULL表达"不可计算"


结论很简单:fdiv()解决的是"不要因为除零而炸掉"这一个问题,它不解决"无穷大怎么展示、怎么入库、怎么返回给前端"这些问题。把fdiv()的调用点收敛到少数几个函数里,并在这些函数的出口统一做is_finite()归一化,才是能长期维护的做法。反过来,在业务代码各处零散地写fdiv()再各处补判断,用不了多久就会漏掉一个出口,然后以"接口偶发返回空"的形式出现在线上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询