使用Builder模式时,我应该按值取"self"还是可变引用



到目前为止,我在官方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会限制构建器长期重用的灵活性

最新更新