-
Notifications
You must be signed in to change notification settings - Fork 0
Number Model
github-actions[bot] edited this page Aug 13, 2026
·
2 revisions
JSON 数字的保真是 NextJson 的一个显式设计目标。Number 不是裸 f64,而是
一个无损枚举。本页用具体的数字例子说明它为什么重要、以及每一步决策的由来。
如果 Value::Number 直接存 f64,会发生这些事:
-
9007199254740993(2^53 + 1)会被舍入成9007199254740992——静默丢精度; -
18446744073709551615(u64::MAX)会变成1.8446744073709552e19,再读回来 已经不是原来的整数了; - 类型化解码
nextdecode::<u64>一个巨大整数时,得先经过 f64 再转回,边界 判断很容易出错。
NextJson 的 Number 用枚举保住每一种数字的精确语义:
pub enum Number {
I64(i64), // 有符号 64 位
U64(u64), // 无符号 64 位(所有非负整数都用它)
I128(i128), // 超出 i64 范围的有符号 128 位
U128(u128), // 超出 u64 范围的无符号 128 位
F64(f64), // 64 位浮点(含 -0.0 与极端值)
}Number::U64(v) 是所有非负整数的规范形式。原因是一个曾被测试抓到的相等性
撕裂:
let a: Number = 1i32.into(); // 旧实现 → Number::I64(1)
let b: Number = "1".parse()?; // 解析 → Number::U64(1)
assert_eq!(a, b); // 旧实现:不等!(撕裂)同一个值,From 构造出来是有符号、解析出来是无符号——PartialEq 就撕裂了。
统一后 I64 只存负数,解析得到的正数与 From 构造的正数恒等。这条规则
也传染给了格式层:bencode / msgpack 解码正数时同样不能产 I64,否则同一份
数据换种格式就"变了"。
- 解析时手写整数词法 + 溢出检测:超
u128的整数 →NumberOutOfRange错误; - 因此
Value/ 类型化解码都不会出现"大整数被悄悄转成浮点丢精度"。
具体来说,解析整数走的是快路径优先:parse_u64_fast / parse_i64_fast
(原生 64 位 checked_mul/checked_add,溢出返回 None),真正溢出才回退到
128 位解析器。实测同进程 A/B:快路径 5.1 ns/次 vs 宽路径 38.2 ns/次(约 7.4x)。
f64 整数(如 64.0)在 JSON 里序列化为 64.0 而非 64——保留浮点语义,
反序列化回 f64 时不会与整数类型混淆。64 读回 u64,64.0 读回 f64,
两边都不越界。
n.as_i64() // 尽力转 i64(浮点截断)
n.as_u64() // 尽力转 u64
n.as_i128() // 精确:仅当值是整数且范围内
n.as_u128() // 精确:仅当值是整数且范围内
n.as_f64() // 转 f64(可能丢精度,从不失败)-
as_i128/as_u128对F64只在"有限且% 1.0 == 0.0"时成功(no_std下f64::fract()不可用,故用% 1.0判断整性); - 边界检查避免了
u64::MAX as f64这类浮点舍入导致的越界误报/漏报。
几个具体例子:
| 输入 | 存储 | as_u64() |
as_i128() |
as_f64() |
|---|---|---|---|---|
42 |
U64(42) |
Some(42) |
Some(42) |
42.0 |
-42 |
I64(-42) |
None |
Some(-42) |
-42.0 |
18446744073709551615 |
U128(...) |
None |
Some(...) |
舍入的 f64 |
3.14 |
F64(3.14) |
None(非整数) |
None(非整数) |
3.14 |
64.0 |
F64(64.0) |
None(是 f64) |
None(是 f64) |
64.0 |
注意最后一行:64.0 虽然是整数值,但它是 F64,所以精确整数转换返回
None——这正是"保留浮点语义"的体现。
-
FormatEncoder提供write_number(&Number)(保留种类)与宽度方法write_i8..i128/write_u8..u128/write_f32/f64; - 默认宽度方法加宽到
i64/u64;二进制格式覆写为原生宽度写出(线上保留宽度, 如 postcard 的定宽整数); - 解码侧
Number是自描述格式的中间载体,非自描述格式(postcard)通过宽度读取 直接还原。
- 没有任意精度小数/大整数(
u128封顶)——这是文档明示的能力边界,超出u128的整数报错而不是用f64静默降级; - 这比 serde_json 默认的
arbitrary_precision(可选 feature)保守;serde 生态 的bigdecimal/rust_decimal走的是另一条路线。
- 线上表达:Core Contracts / Multi-Format Engine
- 安全:检查式算术见 Safety Model