这条款项描述了构造器模式:对于复杂的数据类型提供对应的构造器类型,使得用户可以方便地创造该数据类型的实例。
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 {
/// 开始构造一个新的 [`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
,以允许对不同的构造方法进行链式调用:
/// 设置 `preferred_name`
pub fn preferred_name(mut self, preferred_name: &str) -> Self {
self.0.preferred_name = Some(preferred_name.to_owned());
self
}
/// 设置 `middle_name`
pub fn middle_name(mut self, middle_name: &str) -> Self {
self.0.middle_name = Some(middle_name.to_owned());
self
}
下面这样的辅助函数会比简单的 setter
函数更有用:
/// 把 `last_seen` 字段更新成当前日期/时间
pub fn just_seen(mut self) -> Self {
self.0.last_seen = Some(time::OffsetDateTime::now_utc());
self
}
构造器被调用的最后一个函数会消费它自身并输出所构造的对象:
/// 消费构造器对象并返回最后创建的 [`Details`] 对象
pub fn build(self) -> Details {
self.0
}
总而言之,这让构造器的使用者拥有了更符合人体工程学的体验:
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();
构造器“消费自己”的性质也导致了一些问题。首先,对象的构造过程不能独立完成:
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 {
| ^^^^
这个问题可以通过把被消费的构造器重新赋值给同一个变量来解决:
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()
函数来创建多个实例会违反编译器的检查规则,如同你能想到的那样:
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
:
/// 把 `last_seen` 字段更新成当前日期/时间
pub fn just_seen(&mut self) -> &mut Self {
self.0.last_seen = Some(time::OffsetDateTime::now_utc());
self
}
这可以让代码免于分步构造场景下的自赋值:
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
函数无法被链式调用:
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
为构造器指定一个名称来解决这个问题:
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
,因此必须如下所示:
/// 生成一个构造完毕的 [`Details`] 对象。
pub fn build(&self) -> Details {
// ...
}
这个可重复调用的 build()
的实现必须在每次被调用的时候构造一个全新的实例。如果底层类型实现了 Clone
,这就很简单了 —— 构造器可以持有一个模板然后在每一次 build()
的时候执行一次 clone()
。如果底层类型没有实现 Clone
,那么构造器需要保留足够的状态信息,在每一次 build()
的时候手动创建一个实例返回。
不管是哪种构造器模式的实现,样板代码都集中在一个地方 —— 构造器本身 —— 而不是每个需要操作底层类型的地方。
剩下的样板代码或许还可以通过宏(第 28 条)进一步减少,但如果你打算在这条路上走下去,你应该看看是否有现成的包(尤其是 derive_builder)已经提供了你需要的功能 —— 如果你愿意添加一个依赖的话(第 25 条)。
原文点这里查看