《Effective Rust》第 7 条:对于复杂的类型,使用构造器

  • King
  • 更新于 2024-06-22 23:02
  • 阅读 116

这条款项描述了构造器模式:对于复杂的数据类型提供对应的构造器类型buildertype,使得用户可以方便地创造该数据数据类型的实例。Rust要求开发者在创建一个新的struct实例的时候,必须填入struct的所有字段。这样可以保证结构体中永远不会存在未初始化的值,从而保证了代码的安

这条款项描述了构造器模式:对于复杂的数据类型提供对应的构造器类型 builder type,使得用户可以方便地创造该数据数据类型的实例。

Rust 要求开发者在创建一个新的 struct 实例的时候,必须填入 struct 的所有字段。这样可以保证结构体中永远不会存在未初始化的值,从而保证了代码的安全,然而这会比理想的情况下产生更多的冗余的代码片段。

例如,任何可选的字段都必须显式地使用 None 来标记为缺失:

/// Phone number in E164 format.
#[derive(Debug, Clone)]
pub struct PhoneNumberE164(pub String);

#[derive(Debug, Default)]
pub struct Details {
    pub given_name: String,
    pub preferred_name: Option<String>,
    pub middle_name: Option<String>,
    pub family_name: String,
    pub mobile_phone: Option<PhoneNumberE164>,
}

// ...

let dizzy = Details {
    given_name: "Dizzy".to_owned(),
    preferred_name: None,
    middle_name: None,
    family_name: "Mixer".to_owned(),
    mobile_phone: None,
};

这样的样板式代码也很脆弱,因为将来要向 struct 中添加一个新字段的时候需要更改所有创建这个结构体的地方。

通过使用和实现 [Default] trait 可以显著地减少这种样板代码,如[第 10 条]中所述:

let dizzy = Details {
    given_name: "Dizzy".to_owned(),
    family_name: "Mixer".to_owned(),
    ..Default::default()
};

使用 Default 还有助于减少结构体新增字段时候导致的修改,前提是新的字段本身的类型也实现了 Default

还有一个更普遍的问题:仅当所有的字段类型都实现了 Default trait 的时候,结构体才能使用自动派生的 Default 实现。如果有任何一个字段不满足,那么 derive 就会失败了:

#[derive(Debug, Default)]
pub struct Details {
    pub given_name: String,
    pub preferred_name: Option<String>,
    pub middle_name: Option<String>,
    pub family_name: String,
    pub mobile_phone: Option<PhoneNumberE164>,
    pub date_of_birth: time::Date,
    pub last_seen: Option<time::OffsetDateTime>,
}
error[E0277]: the trait bound `Date: Default` is not satisfied
  --> src/main.rs:48:9
   |
41 |     #[derive(Debug, Default)]
   |                     ------- in this derive macro expansion
...
48 |         pub date_of_birth: time::Date,
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Default` is not
   |                                       implemented for `Date`
   |
   = note: this error originates in the derive macro `Default`

由于孤儿规则的存在,代码没办法为 chrono::Utc 实现 Default;但就算可以,也无济于事 —— 给出生日期赋一个值默认值几乎总是一个错误的选择。

缺少 Default 意味着所有字段都必须手动填写:

let bob = Details {
    given_name: "Robert".to_owned(),
    preferred_name: Some("Bob".to_owned()),
    middle_name: Some("the".to_owned()),
    family_name: "Builder".to_owned(),
    mobile_phone: None,
    date_of_birth: time::Date::from_calendar_date(
        1998,
        time::Month::November,
        28,
    )
    .unwrap(),
    last_seen: None,
};

如果你为复杂的数据结构实现了构造器模式,那么就可以提高这里的效率和体验。

构造器模式最简单的一种实现方式就是用一个额外的 struct 来保存构造原始复杂数据类型所需的数据。简单起见,这里的实例会直接保存一个该类型的实例:

pub struct DetailsBuilder(Details);

impl DetailsBuilder {
    /// Start building a new [`Details`] object.
    /// 开始构造一个新的 [`Details`] 对象
    pub fn new(
        given_name: &str,
        family_name: &str,
        date_of_birth: time::Date,
    ) -> Self {
        DetailsBuilder(Details {
            given_name: given_name.to_owned(),
            preferred_name: None,
            middle_name: None,
            family_name: family_name.to_owned(),
            mobile_phone: None,
            date_of_birth,
            last_seen: None,
        })
    }
}

随后,我们可以给构造器类型增添辅助函数来填充新的字段。每一个这种函数都会消费 self 同时产生一个新的 Self,以允许对不同的构造方法进行链式调用。

这些辅助函数会比简单的 setter 函数有用多了:

/// Update the `last_seen` field to the current date/time.
/// 把 `last_seen` 字段更新成当前日期/时间
pub fn just_seen(mut self) -> Self {
    self.0.last_seen = Some(time::OffsetDateTime::now_utc());
    self
}

构造器被调用的最后一个函数会消费它自身并输出所构造的对象:

/// Consume the builder object and return a fully built [`Details`]
/// object.
/// 消费构造器对象并返回最后创建的 [`Details`] 对象
pub fn build(self) -> Details {
    self.0
}

总而言之,这让构造器的使用者拥有了更符合工程学的体验:


let also_bob = DetailsBuilder::new(
    "Robert",
    "Builder",
    time::Date::from_calendar_date(1998, time::Mon...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
King
King
0x56af...a0dd
江湖只有他的大名,没有他的介绍。