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

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

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

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

#![allow(unused)]
fn main() {
/// 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 条中所述:

#![allow(unused)]
fn main() {
let dizzy = Details {
    given_name: "Dizzy".to_owned(),
    family_name: "Mixer".to_owned(),
    ..Default::default()
};
}

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

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

#![allow(unused)]
fn main() {
#[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 意味着所有字段都必须手动填写:

#![allow(unused)]
fn main() {
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 来保存构造原始复杂数据类型所需的数据。简单起见,这里的实例会直接保存一个该类型的实例:

#![allow(unused)]
fn main() {
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 函数有用多了:

#![allow(unused)]
fn main() {
/// 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
}
}

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

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

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

#![allow(unused)]
fn main() {
let also_bob = DetailsBuilder::new(
    "Robert",
    "Builder",
    time::Date::from_calendar_date(1998, time::Month::November, 28)
        .unwrap(),
)
.middle_name("the")
.preferred_name("Bob")
.just_seen()
.build();
}

构造器“消费自己”的性质也导致了一些问题。首先,对象的构造过程不能独立完成:

#![allow(unused)]
fn main() {
let builder = DetailsBuilder::new(
    "Robert",
    "Builder",
    time::Date::from_calendar_date(1998, time::Month::November, 28)
        .unwrap(),
);
if informal {
    builder.preferred_name("Bob");
}
let bob = builder.build();
}
error[E0382]: use of moved value: `builder`
   --> src/main.rs:256:15
    |
247 |     let builder = DetailsBuilder::new(
    |         ------- move occurs because `builder` has type `DetailsBuilder`,
    |                 which does not implement the `Copy` trait
...
254 |         builder.preferred_name("Bob");
    |                 --------------------- `builder` moved due to this method
    |                                       call
255 |     }
256 |     let bob = builder.build();
    |               ^^^^^^^ value used here after move
    |
note: `DetailsBuilder::preferred_name` takes ownership of the receiver `self`,
      which moves `builder`
   --> src/main.rs:60:35
    |
27  |     pub fn preferred_name(mut self, preferred_name: &str) -> Self {
    |                               ^^^^

这个问题可以通过把被消费的构造器重新赋值给同一个变量来解决:

#![allow(unused)]
fn main() {
let mut builder = DetailsBuilder::new(
    "Robert",
    "Builder",
    time::Date::from_calendar_date(1998, time::Month::November, 28)
        .unwrap(),
);
if informal {
    builder = builder.preferred_name("Bob");
}
let bob = builder.build();
}

构造器的性质带来的另一个问题是你只能构造一个最终对象,对同一个构造器重复调用 build() 函数来创建多个实例会违反编译器的检查规则,如同你能想到的那样:

#![allow(unused)]
fn main() {
let smithy = DetailsBuilder::new(
    "Agent",
    "Smith",
    time::Date::from_calendar_date(1999, time::Month::June, 11).unwrap(),
);
let clones = vec![smithy.build(), smithy.build(), smithy.build()];
}
error[E0382]: use of moved value: `smithy`
   --> src/main.rs:159:39
    |
154 |   let smithy = DetailsBuilder::new(
    |       ------ move occurs because `smithy` has type `base::DetailsBuilder`,
    |              which does not implement the `Copy` trait
...
159 |   let clones = vec![smithy.build(), smithy.build(), smithy.build()];
    |                            -------  ^^^^^^ value used here after move
    |                            |
    |                            `smithy` moved due to this method call

另一种实现构造器的途径是让构造器的方法接受 &mut self 并返回一个 &mut Self

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

这可以让代码免于分步构造场景下的自赋值:

#![allow(unused)]
fn main() {
let mut builder = DetailsBuilder::new(
    "Robert",
    "Builder",
    time::Date::from_calendar_date(1998, time::Month::November, 28)
        .unwrap(),
);
if informal {
    builder.preferred_name("Bob"); // no `builder = ...`
}
let bob = builder.build();
}

然而,这个版本的实现使得构造器的构造方法和它的 setter 函数无法被链式调用:

#![allow(unused)]
fn main() {
let builder = DetailsBuilder::new(
    "Robert",
    "Builder",
    time::Date::from_calendar_date(1998, time::Month::November, 28)
        .unwrap(),
)
.middle_name("the")
.just_seen();
let bob = builder.build();
}
error[E0716]: temporary value dropped while borrowed
   --> src/main.rs:265:19
    |
265 |       let builder = DetailsBuilder::new(
    |  ___________________^
266 | |         "Robert",
267 | |         "Builder",
268 | |         time::Date::from_calendar_date(1998, time::Month::November, 28)
269 | |             .unwrap(),
270 | |     )
    | |_____^ creates a temporary value which is freed while still in use
271 |       .middle_name("the")
272 |       .just_seen();
    |                   - temporary value is freed at the end of this statement
273 |       let bob = builder.build();
    |                 --------------- borrow later used here
    |
    = note: consider using a `let` binding to create a longer lived value

如同编译器错误所示,你可以通过 let 为构造器指定一个名字来解决这个问题:

#![allow(unused)]
fn main() {
let mut builder = DetailsBuilder::new(
    "Robert",
    "Builder",
    time::Date::from_calendar_date(1998, time::Month::November, 28)
        .unwrap(),
);
builder.middle_name("the").just_seen();
if informal {
    builder.preferred_name("Bob");
}
let bob = builder.build();
}

这种修改自身的构造器实现允许你构造多个最终对象。build() 方法的签名需要消费 self,因此必须如下所示:

#![allow(unused)]
fn main() {
/// Construct a fully built [`Details`] object.
/// 生成一个构造完毕的 [`Details`] 对象。
pub fn build(&self) -> Details {
    // ...
}
}

这个可重复调用的 build() 的实现必须在每次被调用的时候构造一个全新的实例。如果底层类型实现了 Clone,这就很简单了 —— 构造器可以持有一个模板然后在每一次 build() 的时候执行一次 clone()。如果底层类型没有实现 Clone,那么构造器需要保留足够的状态信息,在每一次 build() 的时候手动创建一个实例返回。

不管是哪种构造器模式的实现,样板代码都集中在一个地方 —— 构造器本身 —— 而不是每个需要操作底层类型的地方。

剩下的样板代码或许还可以通过宏(第 28 条)进一步减少,但如果你打算在这条路上走下去,你应该看看是否有现成的包(尤其是 derive_builder)已经提供了你需要的功能——如果你愿意添加一个依赖的话(第 25 条)。

原文点这里查看