Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Dimensional Analysis — Making the Compiler Check Your Units 🟢
量纲分析:让编译器帮忙检查单位 🟢

What you’ll learn: How newtype wrappers and the uom crate turn the compiler into a unit-checking engine, preventing the class of bug that destroyed a $328M spacecraft.
本章将学到什么: 如何用 newtype 包装器和 uom crate,把编译器变成单位检查引擎,从而避免那种曾经毁掉一艘 3.28 亿美元航天器的错误。

Cross-references: ch02 (typed commands use these types), ch07 (validated boundaries), ch10 (integration)
交叉阅读: ch02 里的类型化命令会用到这些类型;ch07 讲已验证边界;ch10 讲整体集成。

The Mars Climate Orbiter
火星气候探测器事故

In 1999, NASA’s Mars Climate Orbiter was lost because one team sent thrust data in pound-force seconds while the navigation team expected newton-seconds. The spacecraft entered the atmosphere at 57 km instead of 226 km and disintegrated. Cost: $327.6 million.
1999 年,NASA 的火星气候探测器坠毁,原因是一个团队发送的推力数据单位是 磅力秒,而导航团队期待的却是 牛顿秒。探测器以 57 公里的高度切入大气层,而不是预期的 226 公里,最后直接解体。损失是 3.276 亿美元。

The root cause: both values were double. The compiler couldn’t distinguish them.
根本原因很朴素,也很要命:两边的值都是 double。编译器根本分不出它们的单位差异。

This same class of bug lurks in every hardware diagnostic that deals with physical quantities:
只要硬件诊断里涉及物理量,这一类 bug 就一直潜伏着:

// C — all doubles, no unit checking
double read_temperature(int sensor_id);   // Celsius? Fahrenheit? Kelvin?
double read_voltage(int channel);          // Volts? Millivolts?
double read_fan_speed(int fan_id);         // RPM? Radians per second?

// Bug: comparing Celsius to Fahrenheit
if (read_temperature(0) > read_temperature(1)) { ... }  // units might differ!

Newtypes for Physical Quantities
给物理量包一层 Newtype

The simplest correct-by-construction approach: wrap each unit in its own type.
最简单、也是最“构造即正确”的办法,就是:每种单位都包成自己的类型。

use std::fmt;

/// Temperature in degrees Celsius.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);

/// Temperature in degrees Fahrenheit.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Fahrenheit(pub f64);

/// Voltage in volts.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

/// Voltage in millivolts.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Millivolts(pub f64);

/// Fan speed in RPM.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Rpm(pub f64);

// Conversions are explicit:
impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

impl From<Fahrenheit> for Celsius {
    fn from(f: Fahrenheit) -> Self {
        Celsius((f.0 - 32.0) * 5.0 / 9.0)
    }
}

impl From<Volts> for Millivolts {
    fn from(v: Volts) -> Self {
        Millivolts(v.0 * 1000.0)
    }
}

impl From<Millivolts> for Volts {
    fn from(mv: Millivolts) -> Self {
        Volts(mv.0 / 1000.0)
    }
}

impl fmt::Display for Celsius {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.1}°C", self.0)
    }
}

impl fmt::Display for Rpm {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.0} RPM", self.0)
    }
}

Now the compiler catches unit mismatches:
这样一来,单位错配就会被编译器直接逮住:

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

fn check_thermal_limit(temp: Celsius, limit: Celsius) -> bool {
    temp > limit  // ✅ same units — compiles
}

// fn bad_comparison(temp: Celsius, voltage: Volts) -> bool {
//     temp > voltage  // ❌ ERROR: mismatched types — Celsius vs Volts
// }

Zero runtime cost — newtypes compile down to raw f64 values. The wrapper is purely a type-level concept.
运行时零额外成本。这些 newtype 最终还是会编译成原始的 f64,包装层的意义完全体现在类型级别。

Newtype Macro for Hardware Quantities
给硬件量纲写一个 Newtype 宏

Writing newtypes by hand gets repetitive. A macro eliminates the boilerplate:
newtype 一旦多起来,手写就会很烦。这个时候可以上一个宏,把重复劳动抹平。

/// Generate a newtype for a physical quantity.
macro_rules! quantity {
    ($Name:ident, $unit:expr) => {
        #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
        pub struct $Name(pub f64);

        impl $Name {
            pub fn new(value: f64) -> Self { $Name(value) }
            pub fn value(self) -> f64 { self.0 }
        }

        impl std::fmt::Display for $Name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, "{:.2} {}", self.0, $unit)
            }
        }

        impl std::ops::Add for $Name {
            type Output = Self;
            fn add(self, rhs: Self) -> Self { $Name(self.0 + rhs.0) }
        }

        impl std::ops::Sub for $Name {
            type Output = Self;
            fn sub(self, rhs: Self) -> Self { $Name(self.0 - rhs.0) }
        }
    };
}

// Usage:
quantity!(Celsius, "°C");
quantity!(Fahrenheit, "°F");
quantity!(Volts, "V");
quantity!(Millivolts, "mV");
quantity!(Rpm, "RPM");
quantity!(Watts, "W");
quantity!(Amperes, "A");
quantity!(Pascals, "Pa");
quantity!(Hertz, "Hz");
quantity!(Bytes, "B");

Each line generates a complete type with Display, Add, Sub, and comparison operators. All at zero runtime cost.
每一行都会生成一个完整类型,自带 Display、加减法和比较能力。而且运行时成本仍然是零。

Physics caveat: The macro generates Add for all quantities, including Celsius. Adding absolute temperatures (25°C + 30°C = 55°C) is not physically meaningful — you’d need a separate TemperatureDelta type for differences. The uom crate (shown later) handles this correctly. For simple sensor diagnostics where you only compare and display, you can omit Add/Sub from temperature types and keep them for quantities where addition makes sense (Watts, Volts, Bytes). If you need delta arithmetic, define a CelsiusDelta(f64) newtype with impl Add&lt;CelsiusDelta&gt; for Celsius.
物理学上的提醒: 这个宏会给所有量都生成 Add,包括 Celsius。但绝对温度相加,比如 25°C + 30°C = 55°C,在物理意义上就不严谨了。更合理的做法是单独定义一个 TemperatureDelta 类型。后面会提到的 uom crate 能更正确地处理这类问题。如果当前场景只是简单读取传感器、比较阈值、做展示,那温度类型完全可以不实现 Add/Sub,只给瓦特、电压、字节数这类适合相加的量保留这些操作。如果确实需要做温差运算,可以定义 CelsiusDelta(f64),再实现 impl Add&lt;CelsiusDelta&gt; for Celsius。

Applied Example: Sensor Pipeline
实际例子:传感器处理流水线

A typical diagnostic reads raw ADC values, converts them to physical units, and compares against thresholds. With dimensional types, each step is type-checked:
一个典型的诊断流程,通常会先读取原始 ADC 值,再把它转换成物理量,最后跟阈值做比较。只要把量纲类型引进来,这一整条流水线每一步都能接受类型检查。

macro_rules! quantity {
    ($Name:ident, $unit:expr) => {
        #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
        pub struct $Name(pub f64);
        impl $Name {
            pub fn new(value: f64) -> Self { $Name(value) }
            pub fn value(self) -> f64 { self.0 }
        }
        impl std::fmt::Display for $Name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, "{:.2} {}", self.0, $unit)
            }
        }
    };
}
quantity!(Celsius, "°C");
quantity!(Volts, "V");
quantity!(Rpm, "RPM");

/// Raw ADC reading — not yet a physical quantity.
#[derive(Debug, Clone, Copy)]
pub struct AdcReading {
    pub channel: u8,
    pub raw: u16,   // 12-bit ADC value (0–4095)
}

/// Calibration coefficients for converting ADC → physical unit.
pub struct TemperatureCalibration {
    pub offset: f64,
    pub scale: f64,   // °C per ADC count
}

pub struct VoltageCalibration {
    pub reference_mv: f64,
    pub divider_ratio: f64,
}

impl TemperatureCalibration {
    /// Convert raw ADC → Celsius. The return type guarantees the output is Celsius.
    pub fn convert(&self, adc: AdcReading) -> Celsius {
        Celsius::new(adc.raw as f64 * self.scale + self.offset)
    }
}

impl VoltageCalibration {
    /// Convert raw ADC → Volts. The return type guarantees the output is Volts.
    pub fn convert(&self, adc: AdcReading) -> Volts {
        Volts::new(adc.raw as f64 * self.reference_mv / 4096.0 / self.divider_ratio / 1000.0)
    }
}

/// Threshold check — only compiles if units match.
pub struct Threshold<T: PartialOrd> {
    pub warning: T,
    pub critical: T,
}

#[derive(Debug, PartialEq)]
pub enum ThresholdResult {
    Normal,
    Warning,
    Critical,
}

impl<T: PartialOrd> Threshold<T> {
    pub fn check(&self, value: &T) -> ThresholdResult {
        if *value >= self.critical {
            ThresholdResult::Critical
        } else if *value >= self.warning {
            ThresholdResult::Warning
        } else {
            ThresholdResult::Normal
        }
    }
}

fn sensor_pipeline_example() {
    let temp_cal = TemperatureCalibration { offset: -50.0, scale: 0.0625 };
    let temp_threshold = Threshold {
        warning: Celsius::new(85.0),
        critical: Celsius::new(100.0),
    };

    let adc = AdcReading { channel: 0, raw: 2048 };
    let temp: Celsius = temp_cal.convert(adc);

    let result = temp_threshold.check(&temp);
    println!("Temperature: {temp}, Status: {result:?}");

    // This won't compile — can't check a Celsius reading against a Volts threshold:
    // let volt_threshold = Threshold {
    //     warning: Volts::new(11.4),
    //     critical: Volts::new(10.8),
    // };
    // volt_threshold.check(&temp);  // ❌ ERROR: expected &Volts, found &Celsius
}

The entire pipeline is statically type-checked:
整条流水线都会接受静态类型检查:

  • ADC readings are raw counts (not units)
    ADC 读数只是原始计数值,还不是物理单位
  • Calibration produces typed quantities (Celsius, Volts)
    校准阶段会产出带类型的物理量,比如 Celsius、Volts
  • Thresholds are generic over the quantity type
    阈值结构按“量的类型”泛型化
  • Comparing Celsius against Volts is a compile error
    把摄氏度和电压放到一起比较,会直接变成编译错误

The uom Crate
uom crate

For production use, the uom crate provides a comprehensive dimensional analysis system with hundreds of units, automatic conversion, and zero runtime overhead:
如果是生产环境,uom crate 会提供一整套更完整的量纲分析系统,支持成百上千种单位、自动换算,而且运行时开销同样是零。

// Cargo.toml: uom = { version = "0.36", features = ["f64"] }
//
// use uom::si::f64::*;
// use uom::si::thermodynamic_temperature::degree_celsius;
// use uom::si::electric_potential::volt;
// use uom::si::power::watt;
//
// let temp = ThermodynamicTemperature::new::<degree_celsius>(85.0);
// let voltage = ElectricPotential::new::<volt>(12.0);
// let power = Power::new::<watt>(250.0);
//
// // temp + voltage;  // ❌ compile error — can't add temperature to voltage
// // power > temp;    // ❌ compile error — can't compare power to temperature

Use uom when you need automatic derived-unit support (e.g., Watts = Volts × Amperes). Use hand-rolled newtypes when you need only simple quantities without derived-unit arithmetic.
如果需要自动推导复合单位,比如 Watts = Volts × Amperes,那就上 uom。如果只是处理一些简单量,不需要派生单位运算,手写 newtype 往往更轻更直接。

When to Use Dimensional Types
什么时候适合用量纲类型

Scenario
场景
Recommendation
建议
Sensor readings (temp, voltage, fan)
传感器读数,比如温度、电压、风扇转速
✅ Always — prevents unit confusion
✅ 建议总是使用,能有效防止单位混淆
Threshold comparisons
阈值比较
✅ Always — generic Threshold<T>
✅ 建议总是使用,可以配合泛型 Threshold&lt;T&gt;
Cross-subsystem data exchange
跨子系统数据交换
✅ Always — enforce contracts at API boundaries
✅ 建议总是使用,在 API 边界上把契约钉死
Internal calculations (same unit throughout)
内部计算,而且从头到尾都是同一单位
⚠️ Optional — less bug-prone
⚠️ 可选,这类场景出错概率相对低一些
String/display formatting
字符串展示和格式化
❌ Use Display impl on the quantity type
❌ 不需要单独搞,直接给量纲类型实现 Display 就行

Sensor Pipeline Type Flow
传感器流水线的类型流转

flowchart LR
    RAW["raw: &[u8]<br/>原始字节"] -->|parse| C["Celsius(f64)"]
    RAW -->|parse| R["Rpm(u32)"]
    RAW -->|parse| V["Volts(f64)"]
    C -->|threshold check| TC["Threshold<Celsius>"]
    R -->|threshold check| TR["Threshold<Rpm>"]
    C -.->|"C + R"| ERR["❌ mismatched types<br/>类型不匹配"]
    style RAW fill:#e1f5fe,color:#000
    style C fill:#c8e6c9,color:#000
    style R fill:#fff3e0,color:#000
    style V fill:#e8eaf6,color:#000
    style TC fill:#c8e6c9,color:#000
    style TR fill:#fff3e0,color:#000
    style ERR fill:#ffcdd2,color:#000

Exercise: Power Budget Calculator
练习:功率预算计算器

Create Watts(f64) and Amperes(f64) newtypes. Implement:
创建 Watts(f64) 和 Amperes(f64) 两个 newtype,并完成下面这些功能:

  • Watts::from_vi(volts: Volts, amps: Amperes) -> Watts (P = V × I)
    实现 Watts::from_vi(volts: Volts, amps: Amperes) -> Watts,也就是 P = V × I
  • A PowerBudget that tracks total watts and rejects additions that exceed a configured limit.
    实现一个 PowerBudget,跟踪总瓦数,并在超出配置上限时拒绝继续累加
  • Attempting Watts + Celsius should be a compile error.
    尝试 Watts + Celsius 时,应该得到编译错误
Solution
参考答案
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Watts(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Amperes(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);

impl Watts {
    pub fn from_vi(volts: Volts, amps: Amperes) -> Self {
        Watts(volts.0 * amps.0)
    }
}

impl std::ops::Add for Watts {
    type Output = Watts;
    fn add(self, rhs: Watts) -> Watts {
        Watts(self.0 + rhs.0)
    }
}

pub struct PowerBudget {
    total: Watts,
    limit: Watts,
}

impl PowerBudget {
    pub fn new(limit: Watts) -> Self {
        PowerBudget { total: Watts(0.0), limit }
    }
    pub fn add(&mut self, w: Watts) -> Result<(), String> {
        let new_total = Watts(self.total.0 + w.0);
        if new_total > self.limit {
            return Err(format!("budget exceeded: {:?} > {:?}", new_total, self.limit));
        }
        self.total = new_total;
        Ok(())
    }
}

// ❌ Compile error: Watts + Celsius → "mismatched types"
// let bad = Watts(100.0) + Celsius(50.0);

Key Takeaways
本章要点

  1. Newtypes prevent unit confusion at zero cost — Celsius and Rpm are both f64 inside, but the compiler treats them as different types.
    newtype 能以零成本防止单位混淆:Celsius 和 Rpm 内部虽然都是 f64,但编译器会把它们当成完全不同的类型。
  2. The Mars Climate Orbiter bug is impossible — passing Pounds where Newtons is expected is a compile error.
    火星气候探测器那种错误会变得不可能:该传 Newtons 的地方传了 Pounds,会直接在编译阶段报错。
  3. quantity! macro reduces boilerplate — stamp out Display, arithmetic, and threshold logic for each unit.
    quantity! 宏可以大幅减少样板代码:每种单位的 Display、算术操作和阈值逻辑都能批量生成。
  4. uom crate handles derived units — use it when you need Watts = Volts × Amperes automatically.
    uom crate 适合处理派生单位:如果需要自动推导 Watts = Volts × Amperes 这种关系,它会更省心。
  5. Threshold is generic over the quantity — Threshold<Celsius> can’t accidentally compare to Threshold<Rpm>.
    Threshold 可以按量纲类型泛型化:Threshold&lt;Celsius&gt; 不可能误拿去和 Threshold&lt;Rpm&gt; 混着比较。