《赞美穷尽式解构》
In Praise of Exhaustive Destructuring

原始链接: https://antoine.vandecreme.net/blog/exhaustive-destructuring-praise/

尽管与 TypeScript 或 Haskell 等语言相比,Rust 在解构时要求显式列出结构体字段可能会显得繁琐,但这却是维护软件安全的一项有力工具。 通过避免使用 `..`(忽略剩余字段)语法,转而对每个字段进行显式模式匹配,你可以强制 Rust 编译器在结构体发生变动时发出提醒。例如,如果你在 `WeatherReading` 结构体中添加了一个 `wind_speed` 字段,那么任何使用穷举式解构的函数都将无法通过编译,直到你明确指定如何处理这个新字段。 这种方法防止了因新数据被现有逻辑忽略而产生的“隐性”错误。在跨架构层(例如 API 模型与业务逻辑之间)映射数据时,这种方法尤为宝贵,因为它能确保任何架构变更都得到妥善处理。此外,识别出经常被一同解构的字段组合,可以作为重构代码的信号,提示你将这些字段提取为更小、更紧凑的结构体。虽然这需要编写更多的样板代码,但穷举式解构将编译器变成了主动式的维护助手。

抱歉。
相关文章

原文

When I learned Rust, I was frustrated by the fact that one had to list all fields or use the .. syntax when destructuring a struct.

To illustrate this and because I am writing this during a heatwave, let's suppose we have the following struct:

struct WeatherReading {
    station_id: String,
    recorded_at: DateTime<Utc>,
    temperature: f64,
    humidity: f64,
    pressure: f64,
}

Note: Except for recorded_at, all fields should be newtypes. Not done here for brevity.

Now I just want to retrieve station_id and recorded_at, why should I need to write:

let WeatherReading {
    station_id,
    recorded_at,
    ..
} = weather_reading;

In TypeScript, I could just do:

const { station_id, recorded_at } = weather_reading;

And in Haskell:

-- With NamedFieldPuns extension
let WeatherReading {
    station_id,
    recorded_at,
} = weather_reading

-- Or with RecordWildCards
let WeatherReading {..} = weather_reading

Not that much of a hassle, but that made me favor the weather_reading.station_id/weather_reading.recorded_at syntax.

However, in the past few months, I have started to greatly favor struct destructuring over accessing fields with the . syntax because that makes maintaining the software safer.

For example, we want to detect dangerous weather:

fn is_dangerous(weather_reading: &WeatherReading) -> bool {
    if weather_reading.temperature > 40.0 { // assuming °C, that's 104°F or 313.15K
        return true;
    }
    if weather_reading.humidity < 5.0 { // assuming percentage
        return true;
    }
    if weather_reading.pressure < 960.0 { // assuming hPa, that's 28.35 inHg
        return true;
    }
    return false;
}

That function is dangerous! To see why, let's suppose some stations now have an anemometer and are able to record wind speed. So, we add a new wind_speed field to the WeatherReading struct:

struct WeatherReading {
    station_id: String,
    recorded_at: DateTime<Utc>,
    temperature: f64,
    humidity: f64,
    pressure: f64,
    wind_speed: Option<f64>, // Again, this should be a newtype
}

All good! Let's ship!

Uh oh. Did you remember to update the is_dangerous function? Surely heavy wind should be reported as dangerous. Unfortunately, the Rust compiler did not warn us about it.

The good news is that we could have been warned if we wrote the function like this:

fn is_dangerous(
    WeatherReading {
        station_id: _,
        recorded_at: _,
        temperature,
        humidity,
        pressure,
    }: &WeatherReading
) -> bool {
    if *temperature > 40.0 { // assuming °C, that's 104°F or 313.15K
        return true;
    }
    if *humidity < 5.0 { // assuming percentage
        return true;
    }
    if *pressure < 960.0 { // assuming hPa, that's 28.35 inHg
        return true;
    }
    return false;
}

We would now get error[E0027]: pattern does not mention field 'wind_speed'.

Note that you must be explicit about unused fields and resist the temptation of using the .. pattern for this trick to work.

Because self can't be destructured from the arguments list in Rust, if is_dangerous was written as a method, a bit of ceremony is necessary:

impl WeatherReading {
    fn is_dangerous(&self) -> bool {
        let WeatherReading {
            station_id: _,
            recorded_at: _,
            temperature,
            humidity,
            pressure,
        } = self;
        if *temperature > 40.0 { // assuming °C, that's 104°F or 313.15K
            return true;
        }
        if *humidity < 5.0 { // assuming percentage
            return true;
        }
        if *pressure < 960.0 { // assuming hPa, that's 28.35 inHg
            return true;
        }
        return false;
    }
}

I find this trick especially useful when writing From implementations between the different layers (data access, business logic, API) of a CRUD web service. If you add a field in one layer, the compiler will force you to decide whether that field should be propagated to the other layers or not.

Another advantage is that if you see the same bunch of fields always destructured together while the others are ignored, that can be a good indication that maybe those fields should be extracted into their own struct.

If you are using TypeScript, there is a trick using the Required type.

For Haskell, there is surprisingly no solution at the moment, though there is a proposal.

联系我们 contact @ memedata.com