单位换算是在不改变物理量的前提下,把数值从一个单位映射到另一个单位。可靠软件必须保持量纲,采用权威因子或仿射公式,区分地区制式,并明确精度与舍入策略。源单位、量纲或惯例错误时,数值看似合理也可能完全错误。
2019 年 SI 重定义
2019 年 5 月 20 日,国际单位制被重新定义。所有七个基本单位现在都通过固定基本物理常数的数值来定义:
| 基本单位 | 定义依据 | 固定常数值 |
|---|---|---|
| 秒 (s) | 铯-133 超精细跃迁 | Δν_Cs = 9,192,631,770 Hz |
| 米 (m) | 光速 | c = 299,792,458 m/s |
| 千克 (kg) | 普朗克常数 | h = 6.62607015 × 10⁻³⁴ J·s |
| 安培 (A) | 基本电荷 | e = 1.602176634 × 10⁻¹⁹ C |
| 开尔文 (K) | 玻尔兹曼常数 | k = 1.380649 × 10⁻²³ J/K |
| 摩尔 (mol) | 阿伏伽德罗常数 | N_A = 6.02214076 × 10²³ mol⁻¹ |
| 坎德拉 (cd) | 发光效率 | K_cd = 683 lm/W |
关键变化:千克不再由物理实物(巴黎的国际千克原器)定义,而是由普朗克常数定义。这意味着任何拥有合适设备的实验室都能独立实现千克的完整精度。
对软件的意义
许多 SI 单位关系以及部分依法定义的非 SI 换算是精确的,但 2019 年重定义并没有让每个换算因子都变成精确值。测量值、惯用值、环境相关值与上下文相关值仍可能是近似。以下属于精确定义示例:
- 1 英寸 = 恰好 25.4 毫米(1959 年起的定义)
- 1 磅 = 恰好 0.45359237 千克(定义)
- °C = K − 273.15(精确偏移)
即使因子精确,软件仍可能引入表示与舍入误差;若因子本身来自测量或约定,还需同时考虑系数不确定度和数值表示。
转换链中的浮点精度
问题
# 看似正确的往返转换
meters = 1.0
feet = meters / 0.3048 # 3.280839895013123
back_to_meters = feet * 0.3048 # 0.9999999999999999(不是 1.0!)
这不是转换因子的错误——是 IEEE 754 二进制浮点表示。0.3048 无法在二进制中精确表示。
链式转换累积误差
# 通过中间单位转换会累积误差
km = 1.0
miles = km / 1.60934
yards = miles * 1760
feet = yards * 3
inches = feet * 12
cm = inches * 2.54
back_to_km = cm / 100000
# 结果:0.9999999406318768(误差:~6 × 10⁻⁸)
缓解策略
1. 仅通过基本单位转换(轮辐模式):
def convert(value: float, from_unit: str, to_unit: str) -> float:
# 始终 from_unit → 基本单位 → to_unit(一次乘法、一次除法)
to_base = {'km': 1000, 'm': 1, 'cm': 0.01, 'ft': 0.3048, 'in': 0.0254}
base_value = value * to_base[from_unit]
return base_value / to_base[to_unit]
2. 关键应用使用精确有理数运算:
from fractions import Fraction
# 精确:1 英寸 = 254/10000 米 = 127/5000 米
INCH_TO_METER = Fraction(254, 10000)
FOOT_TO_METER = INCH_TO_METER * 12 # Fraction(3048, 10000)
# 往返是精确的
meters = Fraction(1)
feet = meters / FOOT_TO_METER
back = feet * FOOT_TO_METER # Fraction(1, 1) — 恰好是 1
3. 涉及金额的转换使用 Decimal:
from decimal import Decimal
# 重量计价:$4.99/磅,客户订购 2.5 千克
price_per_lb = Decimal('4.99')
weight_kg = Decimal('2.5')
kg_to_lb = Decimal('2.20462')
total = price_per_lb * weight_kg * kg_to_lb # Decimal('27.527505')
量纲分析:在设计阶段捕获单位 Bug
量纲分析的原理是物理方程必须在量纲上一致。将其作为设计技术使用可以在编译前捕获单位 bug。
火星气候轨道器
1999 年,NASA 的火星气候轨道器任务失败,导航接口单位不一致导致航天器进入了非预期的大气轨迹:
- 洛克希德·马丁的地面软件输出脉冲为磅力·秒(lbf·s)
- NASA 的导航系统期望牛顿·秒(N·s)
- 4.45 倍的差异导致航天器以错误高度进入火星大气层
教训不只是某个函数签名:接口需求、验证、端到端测试和单位感知的数据契约都未能阻止不一致。类型化数量可以消除一类重要错误,但不能取代系统工程。
量纲分析作为代码
核心洞见:如果单位在类型系统中被追踪,单位不匹配就变成编译错误,而非运行时 bug。
速度 = 距离 / 时间 ✓ [m/s] = [m] / [s]
力 = 质量 × 加速度 ✓ [N] = [kg] × [m/s²]
能量 = 力 × 距离 ✓ [J] = [N] × [m]
如果你不小心把米加到秒上,量纲类型系统会在编译时拒绝它。
编程中的类型安全单位
F# 度量单位(编译时,零运行时开销)
[<Measure>] type m // 米
[<Measure>] type s // 秒
[<Measure>] type kg // 千克
let distance = 100.0<m>
let time = 9.58<s>
let speed = distance / time // 推断类型:float<m/s>
// 编译错误:不能将米和秒相加
// let nonsense = distance + time
let mass = 70.0<kg>
let acceleration = 9.81<m/s^2>
let force = mass * acceleration // float<kg m/s^2> = 牛顿
F# 在编译时擦除单位——零运行时开销,完全类型安全。
Rust:幻影类型(uom crate)
use uom::si::f64::*;
use uom::si::length::{meter, foot};
use uom::si::time::second;
let distance = Length::new::<meter>(100.0);
let time = Time::new::<second>(9.58);
let speed = distance / time; // Velocity 类型
// 转换到不同单位
let feet = distance.get::<foot>(); // 328.084...
// 编译错误:不能将 Length 和 Time 相加
// let nonsense = distance + time;
Python:Pint(运行时检查)
import pint
ureg = pint.UnitRegistry()
distance = 100 * ureg.meter
time = 9.58 * ureg.second
speed = distance / time # 10.44 meter / second
# 自动转换
speed_mph = speed.to(ureg.mile / ureg.hour)
print(speed_mph) # 23.35 mile / hour
# 运行时错误:不能添加不兼容的量纲
# distance + time → DimensionalityError
TypeScript:品牌类型(轻量方案)
type Meters = number & { readonly __brand: 'meters' };
type Feet = number & { readonly __brand: 'feet' };
type Seconds = number & { readonly __brand: 'seconds' };
function metersToFeet(m: Meters): Feet {
return (m * 3.28084) as Feet;
}
function speed(distance: Meters, time: Seconds): number {
return distance / time;
}
const d = 100 as Meters;
const t = 10 as Seconds;
const v = speed(d, t);
品牌类型提供部分安全性——它们防止将英尺传递到期望米的地方,但不验证量纲方程。
二进制 vs 十进制前缀
混淆
| 前缀 | SI(十进制) | 二进制(IEC/IEEE 1541) |
|---|---|---|
| kilo/kibi | 1 KB = 1,000 字节 | 1 KiB = 1,024 字节 |
| mega/mebi | 1 MB = 1,000,000 字节 | 1 MiB = 1,048,576 字节 |
| giga/gibi | 1 GB = 1,000,000,000 字节 | 1 GiB = 1,073,741,824 字节 |
| tera/tebi | 1 TB = 10¹² 字节 | 1 TiB = 2⁴⁰ 字节 |
各方使用惯例
| 场景 | 惯例 | 示例 |
|---|---|---|
| 硬盘制造商 | 十进制(SI) | "1 TB" = 1,000,000,000,000 字节 |
| 内存规格 | 二进制 | "16 GB" 实际 = 16 GiB = 17,179,869,184 字节 |
| macOS(10.6 起) | 十进制 | 显示"500 GB"为 500,000,000,000 字节 |
| Windows | 二进制 | 同一硬盘显示"476 GB" |
| Linux(多数工具) | 二进制 | df -h 显示 GiB 但标记为"G" |
| 网络速度 | 十进制 | "1 Gbps" = 1,000,000,000 bits/s |
软件问题
# "我还有多少可用空间?"
# 同一硬盘,不同答案:
disk_bytes = 500_107_862_016
# macOS / 硬盘制造商(十进制)
disk_gb_decimal = disk_bytes / 1_000_000_000 # 500.1 GB
# Windows / 多数 Linux 工具(二进制)
disk_gib = disk_bytes / (1024 ** 3) # 465.8 GiB(显示为"465.8 GB")
# "消失的" 34 GB 纯粹是标注差异
正确实现
def format_bytes(size: int, binary: bool = True) -> str:
if binary:
units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
base = 1024
else:
units = ['B', 'KB', 'MB', 'GB', 'TB']
base = 1000
for unit in units:
if abs(size) < base:
return f"{size:.1f} {unit}"
size /= base
return f"{size:.1f} PiB" if binary else f"{size:.1f} PB"
参考表
长度
| 从 | 到 | 因子(标注精确的为精确值) |
|---|---|---|
| 1 英寸 | 厘米 | 2.54(定义精确) |
| 1 英尺 | 米 | 0.3048(精确) |
| 1 码 | 米 | 0.9144(精确) |
| 1 英里 | 千米 | 1.609344(精确) |
| 1 海里 | 米 | 1852(精确) |
质量
| 从 | 到 | 因子 |
|---|---|---|
| 1 磅(常衡) | 千克 | 0.45359237(精确) |
| 1 盎司 | 克 | 28.349523125(精确) |
| 1 金衡盎司 | 克 | 31.1034768(精确) |
| 1 英石 | 千克 | 6.35029318 |
| 1 短吨(美国) | 千克 | 907.18474(精确) |
温度
| 转换 | 公式 | 说明 |
|---|---|---|
| °C → °F | °F = °C × 9/5 + 32 | 精确(有理系数) |
| °F → °C | °C = (°F − 32) × 5/9 | 精确 |
| °C → K | K = °C + 273.15 | 精确(定义) |
| °F → °R | °R = °F + 459.67 | 兰金温标 |
交叉参考:−40° 在摄氏和华氏中相同。
速度
| 从 | 到 | 因子 |
|---|---|---|
| 1 km/h | m/s | 1/3.6(精确) |
| 1 mph | km/h | 1.609344(精确) |
| 1 节 | km/h | 1.852(精确) |
| 马赫 1 | m/s | ~343(随温度和海拔变化) |
| 1 c(光速) | m/s | 299,792,458(精确,定义米) |
常见转换陷阱
1. 美制加仑 vs 英制加仑
美制加仑 = 3.785411784 升
英制加仑 = 4.54609 升
差异:20%!英国食谱中的"gallon"比美国大 20%。
2. 金衡盎司 vs 常衡盎司
金衡盎司(金、银)= 31.1 克
常衡盎司(其他所有)= 28.35 克
1 金衡磅 = 12 金衡盎司 = 373.2 克
1 常衡磅 = 16 盎司 = 453.6 克
3. 卡路里 vs 千卡
1 卡路里 (cal) = 4.184 焦耳(将 1 克水升高 1°C 的能量)
1 大卡 (Cal) = 1 千卡 = 4184 焦耳
食品标签使用"大卡"(Calories,大写 C) = 千卡
4. 短吨 vs 长吨 vs 公吨
短吨(美国)= 2000 磅 = 907.185 千克
长吨(英国)= 2240 磅 = 1016.047 千克
公吨 = 1000 千克 = 2204.6 磅
使用换算工具
需要快速计算时,可以使用单位转换器处理长度、质量、温度、面积、体积、速度、时间、二进制存储、压力、能量、功率和力。工具会显示舍入结果,并明确美制液量和 IEC 二进制单位;简明定义参见单位换算术语。安全关键、法定计量、贸易、剂量和实验室工作仍须遵循适用标准与经过验证的计算链路。
一手来源
- BIPM SI Brochure,第 9 版 — SI 定义、常数、前缀和单位书写规则
- NIST Guide for the Use of the International System of Units — 美国 SI 使用与换算表达指南
- NASA Mars Climate Orbiter Mishap Investigation Board 报告 — 接口单位不匹配调查结论
- IEEE 754-2019 — 浮点运算标准
总结
软件中的单位换算不只是查表——它是一个类型安全问题:
- 部分换算因子是精确的,测量或上下文相关值则未必;两种情况下浮点表示都可能增加误差
- 轮辐式转换(始终通过基本单位)最小化累积误差
- 量纲分析 在设计阶段捕获单位 bug——火星气候轨道器证明了忽视它的代价
- 类型安全单位系统(F# 度量单位、Rust uom、Python Pint)使单位不匹配成为不可能或可检测
- 二进制 vs 十进制前缀 仍是持续的用户困惑来源——始终明确说明使用哪种惯例
- 同名单位按地区不同(美制 vs 英制加仑、金衡 vs 常衡盎司)——上下文决定解释