当使用Builder模式时,我应该按值传递还是可变引用传递`self`?

12

到目前为止,我在官方 Rust 代码和其他 crate 中看到了两种构建器模式:

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 { ... }
}

我正在编写一个新的crate,但我有点困惑应该选择哪种模式。如果之后更改一些API将很痛苦,因此我想现在做出决定。

我了解它们之间的语义差异,但在实际情况下应该优先选择哪种?或者我们应该如何在它们之间进行选择?为什么?


3
derive_builder crate列出了一些优点和缺点,具体内容请见这里:https://docs.rs/derive_builder/latest/derive_builder/#builder-patterns。 - GManNickG
1个回答

12

从同一个构建器构建多个值是否有益?

  • 如果是,使用&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()`出`name`,但是后续对`spawn()`的调用将不会创建相同的结果,这是一种不好的设计。它可以选择`clone()`名称,但是这样会增加额外且通常不需要的线程生成成本。使用`&mut self`将会是一个不利因素。
考虑一下`std::process::Command`,它作为`std::process::Child`的构建器。它有包含程序、参数、环境和管道配置的字段:
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::OpenOptions,它作为std::fs::File的构建器。它只存储基本配置信息。
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)显然无法编译通过 - 将(self) -> Self链接到build(&mut self)将限制构建器的长期重用灵活性
另请参阅:如何在Rust中使用链式方法调用编写惯用的构建模式?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接