Rust坚持认为,在创建该struct
的新实例时,必须填写struct
中的所有字段。这保证了代码的安全,但确实导致了比理想更冗长的样板代码。
#[derive(Debug, Default)]
struct BaseDetails {
given_name: String,
preferred_name: Option<String>,
middle_name: Option<String>,
family_name: String,
mobile_phone_e164: Option<String>,
}
// ...
let dizzy = BaseDetails {
given_name: "Dizzy".to_owned(),
preferred_name: None,
middle_name: None,
family_name: "Mixer".to_owned(),
mobile_phone_e164: None,
};
这个样板代码也很脆弱,因为未来向struct
添加新字段的更改需要对构建结构的每个地方进行更新。
如之前所述,通过实施和使用Default特征,可以显著减少样板:
let dizzy = BaseDetails {
given_name: "Dizzy".to_owned(),
family_name: "Mixer".to_owned(),
..Default::default()
};
使用Default
还有助于减少添加新字段时所需的更改,前提是新字段本身是实现Default
的类型。
这是一个更普遍的问题:Default
的自动派生实现只有在所有字段类型都实现Default
特征时才有效。如果有一个字段不配合,则derive
步骤不起作用:
#[derive(Debug, Default)]
struct Details {
given_name: String,
preferred_name: Option<String>,
middle_name: Option<String>,
family_name: String,
mobile_phone_e164: Option<String>,
dob: chrono::Date<chrono::Utc>,
last_seen: Option<chrono::DateTime<chrono::Utc>>,
}
error[E0277]: the trait bound `Date<Utc>: Default` is not satisfied
--> builders/src/main.rs:176:9
|
169 | #[derive(Debug, Default)]
| ------- in this derive macro expansion
...
176 | dob: chrono::Date<chrono::Utc>,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `Date<Utc>`
|
= note: this error originates in the derive macro `Default` (in Nightly builds, run with -Z macro-backtrace for more info)
由于孤儿规则,代码无法实现chrono::Utc
的Default
值,因此这意味着所有字段都必须手动填写:
use chrono::TimeZone;
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_e164: None,
dob: chrono::Utc.ymd(1998, 11, 28),
last_seen: None,
};
如果您为复杂的数据结构实现构建器模式,这些人体工程学可以得到改进。
构建器模式的最简单变体是一个单独的struct
,其中包含构建项目所需的信息。为了简单起见,该示例将保存项目本身的实例。
struct DetailsBuilder(Details);
impl DetailsBuilder {
/// Start building a new [`Details`] object.
fn new(
given_name: &str,
family_name: &str,
dob: chrono::Date<chrono::Utc>,
) -> Self {
DetailsBuilder(Details {
given_name: given_name.to_owned(),
preferred_name: None,
middle_name: None,
family_name: family_name.to_owned(),
mobile_phone_e164: None,
dob,
last_seen: None,
})
}
}
然后,构建器类型可以配备辅助方法,以填充新生项目的字段。每种这样的方法都会消耗self
,但会发出一个新的Self
,允许不同的构造方法被链接起来。
/// Set the preferred name.
fn preferred_name(mut self, preferred_name: &str) -> Self {
self.0.preferred_name = Some(preferred_name.to_owned());
self
}
这些助手方法可能比简单的设置程序更有帮助:
/// Update the `last_seen` field to the current date/time.
fn just_seen(mut self) -> Self {
self.0.last_seen = Some(chrono::Utc::now());
self
}
为构建器调用的最终方法消耗构建器并发出构建项。
/// Consume the builder object and return a fully built [`Details`] object.
fn build(self) -> Details {
self.0
}
总的来说,这让建筑商的客户拥有更符合人体工程学的建筑体验:
let also_bob =
DetailsBuilder::new("Robert", "Builder", chrono::Utc.ymd(1998, 11, 28))
.middle_name("the")
.preferred_name("Bob")
.just_seen()
.build();
这种建筑商风格的消耗性会导致一些皱纹。首先,构建过程的各个阶段无法单独完成:
let builder = DetailsBuilder::new(
"Robert",
"Builder",
chrono::Utc.ymd(1998, 11, 28),
);
if informal {
builder.preferred_name("Bob");
}
let bob = builder.build();
error[E0382]: use of moved value: `builder`
--> builders/src/main.rs:249:19
|
241 | let builder = DetailsBuilder::new(
| ------- move occurs because `builder` has type `DetailsBuilder`, which does not implement the `Copy` trait
...
247 | builder.preferred_name("Bob");
| --------------------- `builder` moved due to this method call
248 | }
249 | let bob = builder.build();
| ^^^^^^^ value used here after move
|
note: this function takes ownership of the receiver `self`, which moves `builder`
--> builders/src/main.rs:49:27
|
49 | fn preferred_name(mut self, preferred_name: &str) -> Self {
| ^^^^
这可以通过将消耗的构建器分配回同一变量来解决:
let mut builder =
DetailsBuilder::new("Robert", "Builder", chrono::Utc.ymd(1998, 11, 28));
if informal {
builder = builder.preferred_name("Bob");
}
let bob = builder.build();
此构建器全耗本质的另一个缺点是只能构建一个项目;尝试重复build()
副本:
let smithy =
DetailsBuilder::new("Agent", "Smith", chrono::Utc.ymd(1999, 6, 11));
let clones = vec![smithy.build(), smithy.build(), smithy.build()];
正如您所期望的那样,与借款检查员相勾结:
error[E0382]: use of moved value: `smithy`
--> builders/src/main.rs:269:43
|
267 | let smithy =
| ------ move occurs because `smithy` has type `DetailsBuilder`, which does not implement the `Copy` trait
268 | DetailsBuilder::new("Agent", "Smith", chrono::Utc.ymd(1999, 6, 11));
269 | 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
:
/// Update the `last_seen` field to the current date/time.
fn just_seen(&mut self) -> &mut Self {
self.0.last_seen = Some(chrono::Utc::now());
self
}
这消除了在单独的构建阶段自我分配的需要:
let mut builder = DetailsRefBuilder::new(
"Robert",
"Builder",
chrono::Utc.ymd(1998, 11, 28),
);
if informal {
builder.preferred_name("Bob"); // no `builder = ...`
}
let bob = builder.build();
然而,此版本使得无法将构建器的构建与调用其设置器方法一起链接:
let builder = DetailsRefBuilder::new(
"Robert",
"Builder",
chrono::Utc.ymd(1998, 11, 28),
)
.middle_name("the")
.just_seen();
let bob = builder.build();
error[E0716]: temporary value dropped while borrowed
--> builders/src/main.rs:289:23
|
289 | let builder = DetailsRefBuilder::new(
| _______________________^
290 | | "Robert",
291 | | "Builder",
292 | | chrono::Utc.ymd(1998, 11, 28),
293 | | )
| |_________^ creates a temporary which is freed while still in use
294 | .middle_name("the")
295 | .just_seen();
| - temporary value is freed at the end of this statement
296 | let bob = builder.build();
| --------------- borrow later used here
|
= note: consider using a `let` binding to create a longer lived value
如编译器错误所示,可以通过让构建器项目具有名称来解决这个问题:
let mut builder = DetailsRefBuilder::new(
"Robert",
"Builder",
chrono::Utc.ymd(1998, 11, 28),
);
builder.middle_name("the").just_seen();
if informal {
builder.preferred_name("Bob");
}
let bob = builder.build();
这种突变构建器变体还允许构建多个项目。build()
方法的签名必须不消耗自我,因此必须:
/// Construct a fully built [`Details`] object.
fn build(&self) -> Details {
// ...
}
然后,这个可重复的build()
方法的实现必须在每次调用时构建一个新项目。如果底层项目实现了Clone
,这很容易——构建器可以为每个构建保存一个模板并clone()
它。如果底层项目没有实现Clone
,那么构建器需要有足够的状态,以便能够在每次调用build()
时手动构建底层项目的实例。
对于任何风格的构建器模式,样板代码现在局限于一个地方——构建器——而不是每个使用底层类型的地方都需要。
通过使用宏,剩余的样板可能会进一步减少,但如果你走这条路,你还应该检查是否有现有的板条箱(特别是thederivederive_builder板条箱)来提供所需的内容——假设你愿意依赖它。