AI Library · 知识库

# 数值转换:先确定截断和范围契约

学习目标:把浮点测量值转换为有符号 16 位整数,区分向零截断、舍入、越界和文本解析。前置知识:函数、分支、浮点数;本篇直接接收数值,不负责解析字符串。

# 问题与边界

设备协议只容纳 −32768~32767 的整数。输入是双精度浮点值;先排除 NaN 和无穷,再向零截断小数部分,检查截断结果范围,最后转换。不是四舍五入,也不是对原值直接检查整数区间。

输入 结果
12.9、−12.9 12、−12
32767、32767.9 32767
−32768、−32768.9 −32768
32768、−32769 失败
NaN、正负无穷 失败

因此 32767.9 可以成功;如果设备要求“原值不能越界”或者“不允许小数”,应修改契约,而非把本函数原样迁移。所有程序验证上述边界,打印 12 与 invalid sample 两行。失败不能用合法的 0 替代。

# 关键差异

机制 直接转换的风险 本例策略
Rust as 浮点转整数可饱和,NaN 转 0 先有限性、截断、范围检查
Java / Kotlin 浮点到整数与随后窄化有不同规则 检查后才缩窄,避免饱和或低位截取
Go / C++ 越界浮点转换不能作为可移植错误通道;C++ 可为未定义行为 不执行任何越界转换
JS / TS 没有独立的普通 int16 数值类型;位运算会隐式缩窄 返回满足区间的 number,不用 `x
Python int 本身可增长,不会替协议检查 16 位范围 显式限制业务范围

这里的双精度能精确表示所有目标整数;不是每种整数到浮点的转换都无损。例如把 64 位整数转换为双精度可能丢掉低位。类型转换表达式不会自动证明业务范围。

# 各语言实现

# Rust

Rustfn sample(x: f64) -> Option<i16> {
    let n = x.trunc();
    if !x.is_finite() || !(-32768.0..=32767.0).contains(&n) { return None; }
    Some(n as i16)
}
fn main() {
    for (x, expected) in [(12.9, 12), (-12.9, -12), (32767.0, 32767),
        (32767.9, 32767), (-32768.0, -32768), (-32768.9, -32768)] {
        assert_eq!(sample(x), Some(expected));
    }
    for x in [32768.0, -32769.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
        assert_eq!(sample(x), None);
    }
    println!("{}", sample(12.9).unwrap());
    match sample(32768.0) { Some(n) => println!("{n}"), None => println!("invalid sample") }
}

as 放在检查之后,不把饱和值伪装成有效读数。整数之间的检查式转换可用 TryFrom,但浮点转换仍需选择舍入契约。参见 Rust 数值转换规则。

# Go

Gopackage main
import ("fmt"; "math")
func sample(x float64) (int16, bool) {
    n := math.Trunc(x)
    if math.IsNaN(x) || math.IsInf(x, 0) || n < -32768 || n > 32767 { return 0, false }
    return int16(n), true
}
func main() {
    for _, c := range [][2]float64{{12.9, 12}, {-12.9, -12}, {32767, 32767},
        {32767.9, 32767}, {-32768, -32768}, {-32768.9, -32768}} {
        n, ok := sample(c[0]); if !ok || float64(n) != c[1] { panic("value") }
    }
    for _, x := range []float64{32768, -32769, math.NaN(), math.Inf(1), math.Inf(-1)} {
        if _, ok := sample(x); ok { panic("range") }
    }
    n, ok := sample(12.9); if !ok { panic("valid") }; fmt.Println(n)
    if _, ok := sample(32768); !ok { fmt.Println("invalid sample") } else { panic("range") }
}

int16(n) 是数值转换,不是位重解释。超出目标表示范围的浮点转换不应当被用作跨平台范围检查,见 Go 非常量数值转换。

# Java

Javaimport java.util.OptionalInt;
public class Main {
    static OptionalInt sample(double x) {
        double n = x < 0 ? Math.ceil(x) : Math.floor(x);
        if (!Double.isFinite(x) || n < -32768 || n > 32767) return OptionalInt.empty();
        return OptionalInt.of((short) n);
    }
    public static void main(String[] args) {
        for (double[] c : new double[][]{{12.9,12},{-12.9,-12},{32767,32767},
            {32767.9,32767},{-32768,-32768},{-32768.9,-32768}}) {
            assert sample(c[0]).orElseThrow() == c[1];
        }
        for (double x : new double[]{32768,-32769,Double.NaN,
            Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY}) assert sample(x).isEmpty();
        System.out.println(sample(12.9).orElseThrow());
        if (sample(32768).isEmpty()) System.out.println("invalid sample");
        else throw new AssertionError("range");
    }
}

OptionalInt 承载结果,内部检查后转 short 明确协议宽度。Java 没有 Math.trunc,以正数 floor、负数 ceil 表达向零截断;见 JLS 窄化基本类型转换。

# JavaScript

JavaScriptfunction sample(x) {
    const n = Math.trunc(x);
    if (!Number.isFinite(x) || n < -32768 || n > 32767) return null;
    return n;
}
function check(ok) { if (!ok) throw new Error("check"); }
for (const [x, expected] of [[12.9,12],[-12.9,-12],[32767,32767],
    [32767.9,32767],[-32768,-32768],[-32768.9,-32768]]) check(sample(x) === expected);
for (const x of [32768, -32769, NaN, Infinity, -Infinity]) check(sample(x) === null);
console.log(sample(12.9));
if (sample(32768) === null) console.log("invalid sample"); else throw new Error("range");

输入已经是 number;Number.isFinite 不会像全局 isFinite 那样先转换字符串。负零可保留,但本整数协议将它与零视作同值。参见 ECMAScript Math.trunc 和 Number.isFinite。

# TypeScript

TypeScriptfunction sample(x: number): number | null {
    const n = Math.trunc(x);
    if (!Number.isFinite(x) || n < -32768 || n > 32767) return null;
    return n;
}
function check(ok: boolean): void { if (!ok) throw new Error("check"); }
for (const [x, expected] of [[12.9,12],[-12.9,-12],[32767,32767],
    [32767.9,32767],[-32768,-32768],[-32768.9,-32768]]) check(sample(x) === expected);
for (const x of [32768, -32769, NaN, Infinity, -Infinity]) check(sample(x) === null);
console.log(sample(12.9));
if (sample(32768) === null) console.log("invalid sample"); else throw new Error("range");

返回类型不能证明某个 number 在 16 位范围内;实际保证来自函数体,不是 as number。TS 类型断言不会增加运行时检查。

# Kotlin

Kotlinimport kotlin.math.truncate
fun sample(x: Double): Short? {
    val n = truncate(x)
    if (!x.isFinite() || n < -32768 || n > 32767) return null
    return n.toInt().toShort()
}
fun main() {
    for ((x, expected) in listOf(12.9 to 12, -12.9 to -12, 32767.0 to 32767,
        32767.9 to 32767, -32768.0 to -32768, -32768.9 to -32768)) {
        check(sample(x)?.toInt() == expected)
    }
    for (x in listOf(32768.0, -32769.0, Double.NaN, Double.POSITIVE_INFINITY,
        Double.NEGATIVE_INFINITY)) check(sample(x) == null)
    println(sample(12.9))
    if (sample(32768.0) == null) println("invalid sample") else error("range")
}

先转 Int 再缩窄 Short,范围检查让两步都安全;隐式数值转换不是 Kotlin 的一般赋值规则。参见 Kotlin 显式数字转换。

# Python

Pythonimport math

def sample(x: float) -> int | None:
    if not math.isfinite(x):
        return None
    n = math.trunc(x)
    return n if -32768 <= n <= 32767 else None

for x, expected in [(12.9,12),(-12.9,-12),(32767,32767),(32767.9,32767),
                    (-32768,-32768),(-32768.9,-32768)]:
    assert sample(x) == expected
for x in [32768, -32769, math.nan, math.inf, -math.inf]:
    assert sample(x) is None
print(sample(12.9))
if sample(32768) is None:
    print("invalid sample")
else:
    raise AssertionError("range")

Python 的 math.trunc 返回整数,所以必须先排除非有限值,而不能期待它返回一个可检查的 NaN。参见 math.trunc。

# C++

要求实现提供精确 16 位的 std::int16_t(常见桌面实现提供)。

C++#include <cassert>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <limits>
#include <optional>
std::optional<std::int16_t> sample(double x) {
    double n = std::trunc(x);
    if (!std::isfinite(x) || n < -32768 || n > 32767) return std::nullopt;
    return static_cast<std::int16_t>(n);
}
int main() {
    const double cases[][2] = {{12.9,12},{-12.9,-12},{32767,32767},
        {32767.9,32767},{-32768,-32768},{-32768.9,-32768}};
    for (const auto& c : cases) assert(sample(c[0]) == c[1]);
    for (double x : {32768.0, -32769.0, std::numeric_limits<double>::quiet_NaN(),
        std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity()}) {
        assert(!sample(x));
    }
    std::cout << *sample(12.9) << '\n';
    if (!sample(32768)) std::cout << "invalid sample\n";
    else return 1;
}

static_cast 不是安全转换器;如果截断结果无法表示,浮点到整数转换可能产生未定义行为。参见 C++ 工作草案浮点整数转换,在线内容会更新,本文不依赖 C++17 之后的特性。

# 迁移误区与适用限制

# 运行与验证

版本见主题基线,实测状态见验证记录。各程序预期为 12\ninvalid sample\n;覆盖正负截断、端点及端点外、NaN 和两种无穷。Rust/C++ 可执行文件在 PowerShell 用 .\main.exe,POSIX 用 ./main。不同语言文件放在独立目录,不关闭断言。本文没有把预期结果写成运行结论。

# 相关阅读

数字与运算 · 字符串转端口号 · 转成字符串:格式化与显示形式