3.27 亿美元的单位换算错误
1999 年 9 月 23 日,NASA 的火星气候探测器点火准备进入火星轨道,然后就再也没有消息了。飞船进入高度太低——57 公里而不是计划的 226 公里——要么在大气层中烧毁,要么弹射到了太空。原因:洛克希德·马丁的地面软件输出推力数据用的是磅力·秒,但 NASA 的导航软件期望的是牛顿·秒。9 个月的飞行中没人发现这个不匹配。
换算系数很简单:1 磅力 = 4.44822 牛顿。软件在每次轨道修正机动中都偏差了 4.45 倍。每个小误差在数百万公里的飞行中不断累积。调查委员会称之为"地面软件文件中未使用公制单位的失败"。但真正的失败是系统性的——没有接口规范定义单位,没有验证检查数值是否在预期范围内,没有集成测试捕获这个差异。
这不是个例。单位换算错误还导致了 Gimli 滑翔机事件(1983 年,加拿大航空 767 因地勤人员用磅而非公斤计算燃油而耗尽燃料)、瓦萨号战舰沉没(1628 年,左右舷使用了两种不同的度量系统建造)、以及东京迪士尼乐园太空山脱轨事故(2003 年,车轴规格的英寸被当作毫米解读)。规律总是一样的:两个系统、两种单位约定、它们之间没有明确的契约。
为什么单位换算错误一直在发生
根本问题在于:没有单位的数字毫无意义,但大多数编程语言把它们当作裸数字处理。当你写 distance = 384400 时,这是公里(地球到月球)还是英里(那会让你超过月球 235,000 公里)?变量名可能写着 "distance_km",但类型系统不会强制执行。一个期望米的函数会欣然接受一个英尺值,然后产生垃圾输出,不报任何错误。
公制-英制的分裂让问题更严重。美国、缅甸和利比里亚是仅有的三个没有正式采用公制的国家。但即使在公制国家,遗留系统也在使用英制单位——全球航空业用英尺表示高度、用海里表示距离。医学两种都用(血压用 mmHg,体温在美国用华氏度,其他地方用摄氏度)。这意味着任何国际系统都必须处理两种单位,而换算边界就是 bug 栖息的地方。
软件特有的陷阱:CSS 使用 px、em、rem、vh、vw——全是不同的长度单位。API 返回温度时可能用开尔文、摄氏度或华氏度,取决于提供商。时间戳有秒、毫秒或微秒之分。角度可以是度、弧度或百分度。每次你跨越系统、库或 API 之间的边界,都面临单位不匹配的风险。
人为因素:单位换算错误很无聊。它们不是什么巧妙的 bug——只是看起来太简单以至于不可能发生的低级错误。这恰恰是它们能通过代码审查的原因。审查者看到 "thrust = calculateThrust(data)" 不会想到要问"data 的单位是什么?"火星探测器团队有数百名工程师和 9 个月的飞行运营时间,没人问这个问题。
软件中常见的单位换算错误
温度:公式 C = (F - 32) × 5/9 很简单,但某些语言中的整数除法会截断结果。真正的陷阱在于:开尔文到摄氏度只是 K - 273.15,但开尔文到华氏度需要两步。还有兰氏度(某些美国工程领域使用)是华氏度 + 459.67。我们的 scientific-calculator 工具支持所有四种温标。
距离:1 英里 = 1.60934 公里,1 英尺 = 0.3048 米(1959 年起的精确定义),1 英寸 = 25.4 毫米(精确值)。"精确"这个词很重要——这些是定义的换算,不是测量的近似值。但海里(1.852 公里)和法定英里(1.609 公里)不同,英国的"英里"在标准化之前和美国的也不一样。永远要明确是哪种英里。
重量 vs 质量:日常用语中"重量"和"质量"可以互换。在物理学中,质量(kg)是固有属性,重量(牛顿)取决于重力。在地球上,1 kg 质量的重量是 9.81 N。在火星上是 3.72 N。这个区别在航天、物理模拟以及任何在不同重力环境中建模物体的代码中都很重要。火星探测器的错误具体就是关于力(牛顿 vs 磅力),而不是质量。
数据存储:1 KB = 1,000 字节(SI 标准,硬盘厂商使用)或 1,024 字节(二进制,操作系统使用)。这就是为什么你的"1 TB"硬盘在 Windows 里显示为 931 GB。IEC 标准引入了 KiB(1,024 字节)、MiB(1,048,576 字节)等来消除歧义,但采用程度参差不齐。当你的代码报告文件大小时,要明确说明使用的是哪种约定。
真正有效的预防策略
策略一:使用能编码单位的类型系统。在 TypeScript 中,你可以使用品牌类型:type Meters = number & { __brand: "meters" }。期望 Meters 的函数不会接受普通 number,除非显式转换。ts-units、unitful(Haskell)或 Boost.Units(C++)等库在编译时强制单位正确性。火星探测器的错误在有适当单位类型的情况下会是一个编译错误。
策略二:在系统边界始终转换为规范单位。选择一种单位制(SI 公制是标准选择),在接收输入时立即转换为该单位制。内部计算只使用规范单位。只在输出边界转换回显示单位。这种"早规范化、晚反规范化"的模式消除了内部的单位混淆。
策略三:在变量名和 API 契约中包含单位。不是 "distance" 而是 "distance_meters"。不是 "temperature" 而是 "temp_celsius"。在 API 文档中明确指定单位:"altitude: number(海拔米数)"。在数据库 schema 中添加注释或使用 "weight_kg" 这样的列名。这是低技术含量的方法,但能在代码审查中捕获错误。
策略四:在边界验证范围。天气读数 5,000°C 显然是错的——可能是 5,000 开尔文没转换,或者 50.00°C 小数点放错了位置。自驾游距离 384,400 可能是公里被当成了英里(或反过来)。范围检查能捕获产生物理上不可能值的单位错误。我们的 unit-converter 工具并排显示两个值,方便你做量级的合理性检查。
// Branded types prevent unit confusion at compile time
type Meters = number & { readonly __brand: unique symbol };
type Feet = number & { readonly __brand: unique symbol };
function metersToFeet(m: Meters): Feet {
return (m * 3.28084) as Feet;
}
function calculateAltitude(alt: Meters): string {
return `${alt}m above sea level`;
}
const altitude = 10000 as Meters;
calculateAltitude(altitude); // ✅ OK
// calculateAltitude(10000 as Feet); // ❌ Type error!
// Simpler approach: objects with explicit unit field
interface Measurement {
value: number;
unit: 'meters' | 'feet' | 'km' | 'miles';
}
function toMeters(m: Measurement): number {
switch (m.unit) {
case 'meters': return m.value;
case 'feet': return m.value * 0.3048;
case 'km': return m.value * 1000;
case 'miles': return m.value * 1609.34;
}
}科学计算中的单位换算
科学代码有一个额外的挑战:导出单位。速度是米/秒,加速度是米/秒²,力是 kg·m/s²(牛顿),能量是 kg·m²/s²(焦耳)。当你把速度乘以时间,结果应该是米——但你的编程语言不知道这一点。量纲分析(检查单位是否正确抵消)是物理学家在纸上做的事,但很少编码到软件中。
Python 的 Pint 库和类似工具可以处理这个问题:distance = 5 * ureg.meter; time = 2 * ureg.second; speed = distance / time 会给你 2.5 meter/second,带完整的单位追踪。如果你试图把米和秒相加,会得到 DimensionalityError。这能捕获那些否则会默默产生无意义结果的 bug。
浮点精度与单位换算以微妙的方式交互。精确换算 1 英寸 = 25.4 毫米在浮点中可以精确表示。但 1 英尺 = 0.3048 米有循环二进制表示,会引入微小的舍入误差。在数百万次换算中(CAD 软件或物理模拟中很常见),这些误差会累积。当精度重要时,对换算系数使用精确有理数运算。
一个真实例子:GPS 坐标。经纬度用 6 位小数的度表示,精度约 0.11 米。但如果你转换为弧度做三角计算再转回来,浮点舍入可能让你的位置偏移几厘米。对导航来说这没问题。对土地测量(毫米级精度)来说,你需要注意换算精度。始终只转换一次并保留结果,而不是反复来回转换。
当单位换算工具不够用的时候
有些"换算"不是简单的乘法。温度(摄氏度到华氏度涉及偏移量,不只是比率)。分贝(对数刻度——功率翻倍增加 3 dB,不是 dB 翻倍)。pH 值(也是对数——pH 5 比 pH 6 酸性强 10 倍)。里氏震级(每增加一个整数,能量增加 31.6 倍)。这些需要理解底层的刻度,而不只是一个换算系数。
货币换算是一个特殊情况:换算系数每秒都在变。使用固定汇率的"单位换算器"在你使用的那一刻就已经是错的了。真正的货币换算需要实时汇率,而且你得到的汇率取决于金额(点差)、方向(买入价 vs 卖出价)和提供商(银行 vs 外汇经纪商 vs 信用卡)。我们的 unit-converter 处理有固定换算系数的物理单位;对于货币,你需要实时汇率 API。
烹饪计量出人意料地复杂。一"杯"在美国是 236.6 mL,在澳大利亚是 250 mL,在日本是 200 mL。一"汤匙"是 14.8 mL(美国)、15 mL(公制)或 20 mL(澳大利亚)。不同国家的食谱使用不同的"标准"计量。而且体积到重量的换算取决于食材:1 杯面粉重 125g,但 1 杯糖重 200g。没有简单的换算表能处理这些。
所有这些边界情况的教训:单位换算只有在定义明确、比率固定的物理单位之间才是"简单"的。对于其他一切——货币、烹饪、对数刻度、上下文相关的单位——你需要领域知识,而不只是一个乘法系数。好的单位换算工具(包括我们的)会明确说明它能换算什么、不能换算什么,并标记简单系数不够用的情况。