# 数值转换:先确定截断和范围契约
学习目标:把浮点测量值转换为有符号 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 之后的特性。
# 迁移误区与适用限制
- 把
floor当作截断:−12.9 会变成 −13,与本协议不符。 - 先转整数再检查:检查发生时可能已经饱和、回绕或触发未定义行为,原信息已经消失。
- 只写
n < min || n > max:NaN 与这两个界限的比较都为假,因此还需有限性检查。 - 把本函数用于
uint16:负数和上界语义已经改变,应重写边界用例,不能只改返回类型。
# 运行与验证
版本见主题基线,实测状态见验证记录。各程序预期为 12\ninvalid sample\n;覆盖正负截断、端点及端点外、NaN 和两种无穷。Rust/C++ 可执行文件在 PowerShell 用 .\main.exe,POSIX 用 ./main。不同语言文件放在独立目录,不关闭断言。本文没有把预期结果写成运行结论。