使用 Builder 模式时,我应该通过值还是可变引用来获取“self”?

Posted

技术标签:

【中文标题】使用 Builder 模式时,我应该通过值还是可变引用来获取“self”?【英文标题】:Should I take `self` by value or mutable reference when using the Builder pattern? 【发布时间】:2022-01-21 06:37:03 【问题描述】:

到目前为止,我已经在官方 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 会很痛苦,所以我想现在就做出决定。

我理解它们之间的语义差异,但在实际情况下我们应该更喜欢哪一个?或者我们应该如何选择它们?为什么?

【问题讨论】:

FWIW,derive_builder 板条箱列出了一些优点和缺点:docs.rs/derive_builder/latest/derive_builder/#builder-patterns。 【参考方案1】:

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

如果是,请使用&mut self 如果没有,请使用self

考虑std::thread::Builder,它是std::thread::Thread 的构建器。它在内部使用Option 字段来配置如何构建线程:

pub struct Builder 
    name: Option<String>,
    stack_size: Option<usize>,

它使用self.spawn() 线程,因为它需要name 的所有权。它理论上可以使用&amp;mut self.take() 字段外的名称,但随后对.spawn() 的调用不会产生相同的结果,这是一个糟糕的设计。它可以选择.clone() 名称,但是生成线程会产生额外且通常不需要的成本。使用 &amp;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>,
    // ...

它使用&amp;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,
    // ...

它使用&amp;mut self.open(),因为它不需要拥有任何东西才能工作。它有点类似于线程构建器,因为有一个与文件关联的路径,就像有一个与线程关联的名称一样,但是,文件路径仅传递给.open(),而不是与构建器一起存储。有一个用例可以打开具有相同配置的多个文件。


上述注意事项实际上只涵盖了.build() 方法中self 的语义,但有充分的理由表明,如果您选择一种方法,您也应该将其用于临时方法:

API 一致性 将(&amp;mut self) -&gt; &amp;mut Self 链接到build(self) 显然不会编译 将(self) -&gt; Self 用于build(&amp;mut self) 会限制构建器长期重用的灵活性

【讨论】:

以上是关于使用 Builder 模式时,我应该通过值还是可变引用来获取“self”?的主要内容,如果未能解决你的问题,请参考以下文章

需要多个参数输入时-----------------考虑使用变种的Builder模式

Builder模式的演示

如何使用构建器模式通过 Scala 宏返回内部对象

Scala中string对象是可变还是不可变?加入要创建一个可以修改得字符串,应该是那个类

建造者模式(Builder)

设计模式第3篇:生成器模式(Builder)