首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >解码 TypePHP〡05. 如何用 PHP 写出不丢精度的金额计算

解码 TypePHP〡05. 如何用 PHP 写出不丢精度的金额计算

作者头像
Tinywan
发布2026-09-15 14:39:39
发布2026-09-15 14:39:39
1290
举报
文章被收录于专栏:开源技术小栈开源技术小栈

如何用 PHP 写出不丢精度的金额计算

写在前面|0.1 + 0.2 != 0.3,这个浮点精度问题每个 PHP 开发者都被坑过。电商金额算错一分,对账的时候就是灾难。

TypePHP 的类型系统里藏着一个很少被提起的杀手锏:内置 BigInt、Decimal、BigFloat 三种高精度数值类型,底层是 mpdecimal 大数库。这篇用一个完整的金额计算案例,把 TypePHP 的类型系统从头到尾过一遍。

01. 实操案例:订单金额计算器

先看完整代码。场景:计算订单总额,单价、折扣、税费全程不丢精度。

代码语言:javascript
复制
<?php
// order.php
declare(strict_types=1);
use decimal_types;   // 本文件所有浮点字面量自动成为 Decimal

function main(): void
{
    $price = 19.9;            // Decimal,不是 float!
    $qty = 3;
    $discount = 0.85;         // Decimal

    $total = $price * $qty * $discount;
    $tax = $total * 0.06;

    echo "小计: " . $total->toString() . "\n";
    echo "税费: " . $tax->toString() . "\n";
    echo "应付: " . ($total + $tax)->toString() . "\n";
}

编译运行:

代码语言:javascript
复制
tpc order.php -O2 -o order
./order

预期输出:

代码语言:javascript
复制
小计: 50.745
税费: 3.0447
应付: 53.7897

关键点在 use decimal_types 这一行:文件里所有浮点字面量自动成为 Decimal 类型,十进制运算,天生没有二进制浮点误差。换成原生 PHP 跑一下对比,你会看到熟悉的 53.78970000000001 之类的尾巴。

同理还有 use bigint_types——所有整数字面量变成任意精度整数,超大 ID、雪花算法溢出这类问题直接消失。

02. TypePHP 的类型全景

讲完案例看全貌。TypePHP 编译器内部维护了一套 C++ 存储类型(php::*),挑重点列:

C++ 类型

对应 PHP

说明

php::Var

mixed

动态类型,zval 存储,默认状态

php::Int / php::Float / php::Bool

int/float/bool

原生类型,需 use native_types

php::Str / php::Array

string/array

原生字符串/数组

php::Object

object

通用对象,无具体类信息

php::Stream

resource

流资源,TypePHP 专有

php::BigInt / php::Decimal / php::BigFloat

高精度三件套

php::StdVector / php::StdMap 等

C++ 标准库容器

注意一个反直觉的点:不声明任何东西时,所有变量默认都是 php::Var

也就是说 TypePHP 默认行为和原生 PHP 一样动态,性能优化全部靠你主动声明换取。这和 Rust 那种默认严格的路线完全相反,是刻意的设计选择——先保证现有代码能编译,再让你渐进式收紧类型。

03. 类型声明里藏着三个陷阱

PHP 类型声明到 AOT 类型的映射大部分很直白(int → php::Int,string → php::Str),但有三个声明会退化为动态类型,文档里专门标了:

  • null 类型声明 → php::Var
  • callable → php::Var(编译器无法追踪闭包和回调)
  • iterable → php::Var

热点函数上用了这三种声明,等于白开 native_types。实操检查一下:

代码语言:javascript
复制
<?php
use native_types;

// 反例:callable 参数让一切退回动态
function apply(callable $fn, int $x): int {
    return $fn($x);   // 动态调用,无优化
}

// 正例:类型确定的直接调用
function square(int $x): int {
    return $x * $x;
}

04. 类型推断:编译器怎么猜你的类型

TypePHP 在编译期对每个表达式做类型推断,规则很机械,记住几条就够:

字面量自动识别有个彩蛋:超过 19 位的整数字面量自动识别为 BigInt,超过 16 位有效数字的浮点自动识别为 Decimal:

代码语言:javascript
复制
$id = 12345678901234567890;   // 19 位,自动 BigInt
$pi = 0.1234567890123456;     // 16 位有效数字,自动 Decimal

二元运算按优先级提升:BigFloat > Decimal > BigInt > Float > Int,任一操作数命中高级类型,结果就提升。但有个例外:两个 Int 相除还是 Int(整数除法),除不尽时退化为 Var。

最严格的一条:Float 变量禁止隐式转 Decimal(会报错),必须走字符串构造。这是防止你先在 float 里丢了精度、再假装高精度。文档给的正规写法:

代码语言:javascript
复制
$a = std::bigInt("12345678901234567890");  // 字符串构造,推荐
$b = std::decimal("0.01");
$c = std::bigFloat("1.2345e100");

05. to* 方法:类型转换的一等公民

TypePHP 给类型转换专门设计了一套关键词方法 to*,比 PHP 的强制转换强在两点:能链式调用、编译器能精确推断返回类型。

代码语言:javascript
复制
declare(strict_types=1);
use native_types;

function convert_demo(mixed $input): void
{
    $i = $input->toInt();       // 等价于 (int) $input
    $f = $input->toFloat();
    $s = $input->toString();
    $b = $input->toBool();
    $a = $input->toArray();
}

其中 toArray() 有个贴心设计:如果对象类里定义了 toArray() 方法,优先调用它;没有才退回到"属性表转数组"。给 DTO 类加个 toArray() 就能控制序列化输出:

代码语言:javascript
复制
class User {
    public int $id;
    public string $name;

    public function toArray(): array {
        return ['uid' => $this->id, 'display_name' => $this->name];
    }
}

$user = new User(1, 'admin');
$arr = $user->toArray();   // ['uid' => 1, 'display_name' => 'admin']

06. 类型接续:从数组里捞对象不丢性能

这是类型系统里最实用的技巧。从数组取元素、调用返回 mixed 的函数后,编译器丢了类型信息,后续方法调用只能动态分发。用 toObject() 把类型"接"回来:

代码语言:javascript
复制
$user = $array['user']->toObject(User::class);
echo $user->greet();   // 编译器重新认识它,生成 Native Call

Stream 资源同理,管道、socket 从数组里取出来先接类型再链式操作:

代码语言:javascript
复制
$sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0);
$client = $sockets[0]->toStream();
$client->write("hello");

热点循环里从数组取对象做计算,加不加这一行性能差很多——这是第二篇说的"Native Call 条件"的补救手段。

07. any():类型系统的逃生舱

最后讲个反向操作。编译器按第一次赋值推断类型,之后赋不同类型会报错。需要"这个变量就是什么都可能装"的时候,用 any() 主动放弃类型跟踪:

代码语言:javascript
复制
function main(): void
{
    $rand = random_int(0, 10000);
    if ($rand % 2) {
        $o = any(new Foo1());
    } else {
        $o = any(new Foo2());   // 不加 any() 这里会报类型冲突
    }
    if (method_exists($o, 'run')) {
        $o->run();   // 动态分发
    }
}

any() 只影响编译期推断,不产生运行时检查开销——但降级为 Var 之后,这个变量也告别了 Native Call 优化。性能和灵活性的交易,又一次明码标价。

08. 小结

  • 金额、金融场景用 use decimal_types,超大整数用 use bigint_types,字面量自动变高精度
  • Big* 类型不可变,每次运算返回新值;Float 不能隐式转 Decimal,必须字符串构造
  • callable/null/iterable 声明会退化为动态类型,热点代码避开
  • toObject()/toStream() 把数组里捞出来的值接回强类型,是性能的关键细节
  • 真需要动态就用 any(),但要知道代价

下一篇讲编译期注解:NotNull、NotEmpty、Validate 这些怎么把空指针和非法参数扼杀在编译阶段,同样带完整案例。

参考资料

  1. 类型系统:https://www.swoole.com/aot/docs/types
  2. 类型转换:https://www.swoole.com/aot/docs/type-convert
  3. 对象类型转换:https://www.swoole.com/aot/docs/object-type-conversion
  4. 高精度运算:https://www.swoole.com/aot/docs/math
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-08-22,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 如何用 PHP 写出不丢精度的金额计算
    • 01. 实操案例:订单金额计算器
    • 02. TypePHP 的类型全景
    • 03. 类型声明里藏着三个陷阱
    • 04. 类型推断:编译器怎么猜你的类型
    • 05. to* 方法:类型转换的一等公民
    • 06. 类型接续:从数组里捞对象不丢性能
    • 07. any():类型系统的逃生舱
    • 08. 小结
    • 参考资料
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档