使用 Builder 模式时,我应该按值还是按可变引用获取 `self`?
Should I take `self` by value or mutable reference when using the Builder pattern?
到目前为止,我在官方 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会很痛苦,所以我想现在就做决定。
我理解它们之间的语义差异,但在实际情况下我们应该更喜欢哪一个?或者我们应该如何选择它们?为什么?
从同一个构建器构建多个值是否有益?
- 如果是,使用
&mut self
- 如果没有,使用
self
考虑 std::thread::Builder
which is a builder for 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::Command
which serves as a builder for a 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
。无论如何,它必须在内部将所有数据复制到 OS,因此没有理由使用 self
。使用相同的配置生成多个子进程也有明显的好处和用例。
考虑 std::fs::OpenOptions
which serves as a builder for 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 代码和其他 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会很痛苦,所以我想现在就做决定。
我理解它们之间的语义差异,但在实际情况下我们应该更喜欢哪一个?或者我们应该如何选择它们?为什么?
从同一个构建器构建多个值是否有益?
- 如果是,使用
&mut self
- 如果没有,使用
self
考虑 std::thread::Builder
which is a builder for 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::Command
which serves as a builder for a 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
。无论如何,它必须在内部将所有数据复制到 OS,因此没有理由使用 self
。使用相同的配置生成多个子进程也有明显的好处和用例。
考虑 std::fs::OpenOptions
which serves as a builder for 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)
会限制构建器的灵活性以供长期重复使用