Skip to content

Number Model

github-actions[bot] edited this page Aug 13, 2026 · 2 revisions

Number Model

JSON 数字的保真是 NextJson 的一个显式设计目标Number 不是裸 f64,而是 一个无损枚举。本页用具体的数字例子说明它为什么重要、以及每一步决策的由来。

先看问题:f64 会把整数悄悄变坏

如果 Value::Number 直接存 f64,会发生这些事:

  • 90071992547409932^53 + 1)会被舍入成 9007199254740992——静默丢精度
  • 18446744073709551615u64::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 与极端值)
}

三个关键设计决策

1. 非负整数统一存 U64(相等性修复)

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,否则同一份 数据换种格式就"变了"。

2. 超出 u128 报错,不静默丢精度

  • 解析时手写整数词法 + 溢出检测:超 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)。

3. 浮点整数补 ".0"

f64 整数(如 64.0)在 JSON 里序列化为 64.0 而非 64——保留浮点语义, 反序列化回 f64 时不会与整数类型混淆。64 读回 u6464.0 读回 f64, 两边都不越界。

转换 API

n.as_i64()   // 尽力转 i64(浮点截断)
n.as_u64()   // 尽力转 u64
n.as_i128()  // 精确:仅当值是整数且范围内
n.as_u128()  // 精确:仅当值是整数且范围内
n.as_f64()   // 转 f64(可能丢精度,从不失败)
  • as_i128/as_u128F64 只在"有限且 % 1.0 == 0.0"时成功(no_stdf64::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 走的是另一条路线。

相关页面

Clone this wiki locally