原文标题: Init Struct Pattern
原文链接: https:// xaeroxe.github.io/init- struct-pattern/
Introduction
让我们来讨论一下Rust中复杂结构体的初始化。在这方面已经有了一些比较流行的做法,其中包括常见的pub fn new()
和生成器模式(builder pattern)。在这篇文章中,我会对比这些方式,并提出一种新的模式,我称之为初始化结构模式(Init Struct Pattern)
。
New
初始化结构体最开始也是最常见的方式是通过在结构体中声明一个签名如下面这样的函数。
pub fn new() -> Self {
Self {
// init me
}
}
这是相当直观的,并且对于简单的结构体也很适用。但是当结构体逐渐变得复杂的时候,它开始出现一些问题。例如:
impl YakShaver {
pub fn new(clipper_size: u32, gas_powered_clippers: bool, solar_powered_clippers: bool, color_to_dye_yak: &str) -> Self {
// Body is irrelevant
}
}
// In some other file, or maybe even another crate we now have to construct this type.
// Unless you've looked at the definition for `fn new` recently, you might not remember that the second argument
// creates some CO2 emissions if flipped.
let yak_shaver = YakShaver::new(3, false, true, "magenta");
这个模式还有另一个问题,它会让事物在不必要的时候打破变化。例如,我知道我的大多数用户想要他们的牦牛帆船(yak clippers)是黑色的,为什么你想要点不一样的呢?Bob想要点不一样的。Bob是我们的下游用户,他对帆船的颜色有意见。他想要红色的帆船。好,现在让我们为帆船的颜色添加一个参数:
impl YakShaver {
pub fn new(...same as above..., clipper_color: &str) -> Self {
// Body is irrelevant
}
}
let yak_shaver = YakShaver::new(3, false, true, "magenta", "red");
但是现在我们有了一个问题。尽管超过99%的用户都想要帆船的颜色是黑色的,但是所有人都得指定帆船的颜色。这看起来愚蠢且啰嗦。而且直到下一个大版本之前,我们还不能发布Bob的新功能。否则,我们就会破坏所有人的代码。Bob对于要等一段时间感到非常不开心,显然我们也不太想让我们所有的用户都去指定颜色参数。
生成器模式(build pattern)可以解决这个问题。我们可以在将来精心策划来避免这种问题。
Builder Pattern
生成器很优雅,因为它不需要我们在构建结构体的时候指定所有的东西,这意味着我们可以在一个小版本里发布Bob要的功能而不用破坏任何东西。它还在每个字段加上自己名字,这让代码变得更具有可读性。例如:
pub struct YakShaverBuilder {
clipper_size: u32,
gas_powered_clippers: bool,
solar_powered_clippers: bool,
color_to_dye_yak: String,
clipper_color: String,
}
impl YakShaverBuilder {
pub fn new() -> Self {
Self {
clipper_size: 3,
gas_powered_clippers: false,
solar_powered_clippers: true,
color_to_dye_yak: String::from("brown"),
clipper_color: String::from("black"),
}
}
pub fn clipper_size(mut self, v: u32) -> Self {
self.clipper_size = v;
self
}
pub fn gas_powered_clippers(mut self, v: bool) -> Self {
self.gas_powered_clippers = v;
self
}
pub fn solar_powered_clippers(mut self, v: bool) -> Self {
self.solar_powered_clippers = v;
self
}
pub fn color_to_dye_yak(mut self, v: String) -> Self {
self.color_to_dye_yak = v;
self
}
pub fn clipper_color(mut self, v: String) -> Self {
self.clipper_color = v;
self
}
pub fn build(self) -> YakShaver {
YakShaver {
clipper_size: self.clipper_size,
gas_powered_clippers: self.gas_powered_clippers,
solar_powered_clippers: self.solar_powered_clippers,
color_to_dye_yak: self.color_to_dye_yak,
clipper_color: self.clipper_color,
}
}
}
let yak_shaver = YakShaverBuilder::new()
.clipper_size(4)
.color_to_dye_yak(String::from("hot pink"))
.clipper_color(String::from("red"))
.build();
但是这又暴露除了生成器模式的一个很大的缺点。它很啰嗦。代码量大约是pub fn new()->Self
的两到三倍,当然这取决于你的统计方式。所以对于比较简单的结构体来讲,生成器模式可能过犹不及,但是对于大型结构体来讲,与带来的诸多好处相比,它的缺点则可以忽略不计。那么有没有两全其美的办法呢?我希望我的下一个建议可以做到。
Init Struct Pattern
我们可以组合Rust的一些特性来得到与生成器模式相同的好处,但是又不用那么啰嗦。我会从一个例子开始:
pub struct YakShaverInit {
pub clipper_size: u32,
pub gas_powered_clippers: bool,
pub solar_powered_clippers: bool,
pub color_to_dye_yak: String,
pub clipper_color: String,
#[doc(hidden)]
pub __non_exhaustive: () // This is a hack, we might be able to stop using it in the future.
}
impl Default for YakShaverInit {
fn default() -> Self {
Self {
clipper_size: 3,
gas_powered_clippers: false,
solar_powered_clippers: true,
color_to_dye_yak: String::from("brown"),
clipper_color: String::from("black"),
}
}
}
impl YakShaverInit {
pub fn init(self) -> YakShaver {
YakShaver {
clipper_size: self.clipper_size,
gas_powered_clippers: self.gas_powered_clippers,
solar_powered_clippers: self.solar_powered_clippers,
color_to_dye_yak: self.color_to_dye_yak,
clipper_color: self.clipper_color,
}
}
}
let yak_shaver = YakShaverInit {
clipper_size: 4,
color_to_dye_yak: String::from("hot pink"),
clipper_color: String::from("red"),
..Default::default(),
}.init();
这看起来和生成器模式很像!的确,它也具备很多相同的好处。我们不需要为每个字段定义函数,并且它也不需要多次返回Self
。如果我们的初始化需要用给定的输入来完成复杂的工作,那么可以在fn init()
里面完成。所以我们不需要发布大版本就可以在结构体中添加新字段,我们不需要所有人都指定全部字段,并且相比于生成器模式,我们的定义已经精简了很多。我觉得这是个胜利!
我在这里使用了一些可能并不是所有人都熟悉的特性(features)。..Default::default()
是什么?这个叫做结构体更新语法(struct update syntax),它告诉编译器从impl Default for YakShaverInit
中定义的Default::fault()
的输出结果拷贝所有剩余的字段。其中还在一个pub字段上使用了#[doc(hidden)]
属性,这是在文档中隐藏这个字段,从而不鼓励人们在对YakShaverInit
进行结构体初始化的时候添加这个字段。如果他们添加了这字段,然后结构体构造可能提前结束从而无法执行在尾部指定的..Default::default()
,那意味着如果我们在YakShaverInit
结构体中添加新字段,别人的代码就会被破坏。我们现在还无法阻止这种情况,只能不鼓励使用。如果Rust能为我们添加更多的方式来使用non exhaustive 结构体,即用#[non_exhaustive]
定义的结构体,我们可能就能在将来更简便地阻止这种情况。如果大家喜欢这个方法,我可能会写一个相关的rust-lang RFCs来让它进一步变得可能。
在初始化结构中添加其他的私有字段也会打破变化!所有的字段都必须是公开的(public)。这似乎很愚蠢,因为下面的代码是合法的。
pub struct HalfPublic {
pub a: i32,
b: u32,
}
impl Default for HalfPublic {
fn default() -> Self {
Self {
a: 0,
b: 0,
}
}
}
let mut half_public = HalfPublic::default();
half_public.a = 10;
所以我准备去写一个RFC给rust-lang,建议下面的代码是合法的:
let half_public = HalfPublic {
a: 10,
..Default::default(),
}
请留意我提到的RFCs,不管怎样,我希望你喜欢这篇文章,如果有任何建议或者意见请通过邮箱kieseljake+blog@gmail.com
发送给我。
欢迎关注我的微信公众号: