到目前为止,我在官方Rust代码和其他板条箱中看到了两种构建器模式:
impl DataBuilder {
pub fn new() -> DataBuilder { ... }
pub fn arg1(&mut self, arg1: Arg1Type) -> &mut Builder { ... }
pub fn arg2(&mut self, arg2: Arg2Type) -> &mut Builder { ... }
...
pub fn build(&self) -> Data { ... }
}
impl DataBuilder {
pub fn new() -> DataBuilder { ... }
pub fn arg1(self, arg1: Arg1Type) -> Builder { ... }
pub fn arg2(self, arg2: Arg2Type) -> Builder { ... }
...
pub fn build(self) -> Data { ... }
}
我正在写一个新的板条箱,我有点困惑我应该选择哪种模式。我知道如果我以后更改一些API会很痛苦,所以我想现在就做出决定。
我理解它们之间的语义差异,但在实际情况下我们应该更喜欢哪一个?或者我们应该如何在它们之间做出选择?为什么?
从同一构建器构建多个值有益吗
- 如果是,请使用
&mut self
- 如果否,则使用
self
考虑std::thread::Builder
,它是std::thread::Thread
的构建器。它在内部使用Option
字段来配置如何构建线程:
pub struct Builder {
name: Option<String>,
stack_size: Option<usize>,
}
它使用self
到.spawn()
作为线程,因为它需要name
的所有权。理论上,它可以在字段外使用&mut self
和.take()
的名称,但随后对.spawn()
的调用不会产生相同的结果,这是一种糟糕的设计。它可以选择.clone()
这个名称,但生成线程会产生额外的、通常不需要的成本。使用&mut self
会造成损害。
考虑作为std::process::Child
的构建器的std::process::Command
。它有包含程序、参数、环境和管道配置的字段:
pub struct Command {
program: CString,
args: Vec<CString>,
env: CommandEnv,
stdin: Option<Stdio>,
stdout: Option<Stdio>,
stderr: Option<Stdio>,
// ...
}
它使用&mut self
到.spawn()
,因为它不拥有这些字段的所有权来创建Child
。无论如何,它必须在内部将所有数据复制到操作系统,因此没有理由使用self
。使用相同的配置生成多个子进程也有明显的好处和用例。
考虑作为std::fs::File
的构建器的std::fs::OpenOptions
。它只存储基本配置:
pub struct OpenOptions {
read: bool,
write: bool,
append: bool,
truncate: bool,
create: bool,
create_new: bool,
// ...
}
它使用&mut self
到.open()
,因为它不需要任何东西的所有权就可以工作。它与线程生成器有点相似,因为有一个路径与文件关联,就像有一个名称与线程关联一样,但是,文件路径只传递给.open()
,而不与生成器一起存储。有一个使用相同配置打开多个文件的用例。
上述注意事项实际上只涵盖了.build()
方法中self
的语义,但有充分的理由表明,如果您选择一种方法,您也应该将其用于临时方法:
- API一致性
- 将
(&mut self) -> &mut Self
链接到build(self)
显然不会编译 - 在
build(&mut self)
中使用(self) -> Self
会限制构建器长期重用的灵活性