改进rust代码的35种具体方法-类型(八)-复杂类型使用构建器

上一篇文章


Rust坚持认为,在创建该struct的新实例时,必须填写struct中的所有字段。这保证了代码的安全,但确实导致了比理想更冗长的样板代码。

#[derive(Debug, Default)]
struct BaseDetails {
    given_name: String,
    preferred_name: Option<String>,
    middle_name: Option<String>,
    family_name: String,
    mobile_phone_e164: Option<String>,
}

// ...

    let dizzy = BaseDetails {
        given_name: "Dizzy".to_owned(),
        preferred_name: None,
        middle_name: None,
        family_name: "Mixer".to_owned(),
        mobile_phone_e164: None,
    };

这个样板代码也很脆弱,因为未来向struct添加新字段的更改需要对构建结构的每个地方进行更新。

如之前所述,通过实施和使用Default特征,可以显著减少样板:

    let dizzy = BaseDetails {
        given_name: "Dizzy".to_owned(),
        family_name: "Mixer".to_owned(),
        ..Default::default()
    };

使用Default还有助于减少添加新字段时所需的更改,前提是新字段本身是实现Default的类型。

这是一个更普遍的问题:Default的自动派生实现只有在所有字段类型都实现Default特征时才有效。如果有一个字段不配合,则derive步骤不起作用:

    #[derive(Debug, Default)]
    struct Details {
        given_name: String,
        preferred_name: Option<String>,
        middle_name: Option<String>,
        family_name: String,
        mobile_phone_e164: Option<String>,
        dob: chrono::Date<chrono::Utc>,
        last_seen: Option<chrono::DateTime<chrono::Utc>>,
    }
error[E0277]: the trait bound `Date<Utc>: Default` is not satisfied
   --> builders/src/main.rs:176:9
    |
169 |     #[derive(Debug, Default)]
    |                     ------- in this derive macro expansion
...
176 |         dob: chrono::Date<chrono::Utc>,
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `Date<Utc>`
    |
    = note: this error originates in the derive macro `Default` (in Nightly builds, run with -Z macro-backtrace for more info)

由于孤儿规则,代码无法实现chrono::UtcDefault值,因此这意味着所有字段都必须手动填写:

    use chrono::TimeZone;

    let bob = Details {
        given_name: "Robert".to_owned(),
        preferred_name: Some("Bob".to_owned()),
        middle_name: Some("the".to_owned()),
        family_name: "Builder".to_owned(),
        mobile_phone_e164: None,
        dob: chrono::Utc.ymd(1998, 11, 28),
        last_seen: None,
    };

如果您为复杂的数据结构实现构建器模式,这些人体工程学可以得到改进。

构建器模式的最简单变体是一个单独的struct,其中包含构建项目所需的信息。为了简单起见,该示例将保存项目本身的实例。

struct DetailsBuilder(Details);

impl DetailsBuilder {
    /// Start building a new [`Details`] object.
    fn new(
        given_name: &str,
        family_name: &str,
        dob: chrono::Date<chrono::Utc>,
    ) -> Self {
        DetailsBuilder(Details {
            given_name: given_name.to_owned(),
            preferred_name: None,
            middle_name: None,
            family_name: family_name.to_owned(),
            mobile_phone_e164: None,
            dob,
            last_seen: None,
        })
    }
}

然后,构建器类型可以配备辅助方法,以填充新生项目的字段。每种这样的方法都会消耗self,但会发出一个新的Self,允许不同的构造方法被链接起来。

    /// Set the preferred name.
    fn preferred_name(mut self, preferred_name: &str) -> Self {
        self.0.preferred_name = Some(preferred_name.to_owned());
        self
    }

这些助手方法可能比简单的设置程序更有帮助:

    /// Update the `last_seen` field to the current date/time.
    fn just_seen(mut self) -> Self {
        self.0.last_seen = Some(chrono::Utc::now());
        self
    }

为构建器调用的最终方法消耗构建器并发出构建项。

    /// Consume the builder object and return a fully built [`Details`] object.
    fn build(self) -> Details {
        self.0
    }

总的来说,这让建筑商的客户拥有更符合人体工程学的建筑体验:

    let also_bob =
        DetailsBuilder::new("Robert", "Builder", chrono::Utc.ymd(1998, 11, 28))
            .middle_name("the")
            .preferred_name("Bob")
            .just_seen()
            .build();

这种建筑商风格的消耗性会导致一些皱纹。首先,构建过程的各个阶段无法单独完成:

        let builder = DetailsBuilder::new(
            "Robert",
            "Builder",
            chrono::Utc.ymd(1998, 11, 28),
        );
        if informal {
            builder.preferred_name("Bob");
        }
        let bob = builder.build();
error[E0382]: use of moved value: `builder`
   --> builders/src/main.rs:249:19
    |
241 |         let builder = DetailsBuilder::new(
    |             ------- move occurs because `builder` has type `DetailsBuilder`, which does not implement the `Copy` trait
...
247 |             builder.preferred_name("Bob");
    |                     --------------------- `builder` moved due to this method call
248 |         }
249 |         let bob = builder.build();
    |                   ^^^^^^^ value used here after move
    |
note: this function takes ownership of the receiver `self`, which moves `builder`
   --> builders/src/main.rs:49:27
    |
49  |     fn preferred_name(mut self, preferred_name: &str) -> Self {
    |                           ^^^^

这可以通过将消耗的构建器分配回同一变量来解决:

    let mut builder =
        DetailsBuilder::new("Robert", "Builder", chrono::Utc.ymd(1998, 11, 28));
    if informal {
        builder = builder.preferred_name("Bob");
    }
    let bob = builder.build();

此构建器全耗本质的另一个缺点是只能构建一个项目;尝试重复build()副本:

        let smithy =
            DetailsBuilder::new("Agent", "Smith", chrono::Utc.ymd(1999, 6, 11));
        let clones = vec![smithy.build(), smithy.build(), smithy.build()];

正如您所期望的那样,与借款检查员相勾结:

error[E0382]: use of moved value: `smithy`
   --> builders/src/main.rs:269:43
    |
267 |         let smithy =
    |             ------ move occurs because `smithy` has type `DetailsBuilder`, which does not implement the `Copy` trait
268 |             DetailsBuilder::new("Agent", "Smith", chrono::Utc.ymd(1999, 6, 11));
269 |         let clones = vec![smithy.build(), smithy.build(), smithy.build()];
    |                                  -------  ^^^^^^ value used here after move
    |                                  |
    |                                  `smithy` moved due to this method call

另一种方法是让建筑商的方法采取&mut self并发出&mut Self

    /// Update the `last_seen` field to the current date/time.
    fn just_seen(&mut self) -> &mut Self {
        self.0.last_seen = Some(chrono::Utc::now());
        self
    }

这消除了在单独的构建阶段自我分配的需要:

    let mut builder = DetailsRefBuilder::new(
        "Robert",
        "Builder",
        chrono::Utc.ymd(1998, 11, 28),
    );
    if informal {
        builder.preferred_name("Bob"); // no `builder = ...`
    }
    let bob = builder.build();

然而,此版本使得无法将构建器的构建与调用其设置器方法一起链接:

        let builder = DetailsRefBuilder::new(
            "Robert",
            "Builder",
            chrono::Utc.ymd(1998, 11, 28),
        )
        .middle_name("the")
        .just_seen();
        let bob = builder.build();
error[E0716]: temporary value dropped while borrowed
   --> builders/src/main.rs:289:23
    |
289 |           let builder = DetailsRefBuilder::new(
    |  _______________________^
290 | |             "Robert",
291 | |             "Builder",
292 | |             chrono::Utc.ymd(1998, 11, 28),
293 | |         )
    | |_________^ creates a temporary which is freed while still in use
294 |           .middle_name("the")
295 |           .just_seen();
    |                       - temporary value is freed at the end of this statement
296 |           let bob = builder.build();
    |                     --------------- borrow later used here
    |
    = note: consider using a `let` binding to create a longer lived value

如编译器错误所示,可以通过让构建器项目具有名称来解决这个问题:

    let mut builder = DetailsRefBuilder::new(
        "Robert",
        "Builder",
        chrono::Utc.ymd(1998, 11, 28),
    );
    builder.middle_name("the").just_seen();
    if informal {
        builder.preferred_name("Bob");
    }
    let bob = builder.build();

这种突变构建器变体还允许构建多个项目。build()方法的签名必须消耗自我,因此必须:

    /// Construct a fully built [`Details`] object.
    fn build(&self) -> Details {
        // ...
    }

然后,这个可重复的build()方法的实现必须在每次调用时构建一个新项目。如果底层项目实现了Clone,这很容易——构建器可以为每个构建保存一个模板并clone()它。如果底层项目没有实现Clone,那么构建器需要有足够的状态,以便能够在每次调用build()时手动构建底层项目的实例。

对于任何风格的构建器模式,样板代码现在局限于一个地方——构建器——而不是每个使用底层类型的地方都需要。

通过使用宏,剩余的样板可能会进一步减少,但如果你走这条路,你还应该检查是否有现有的板条箱(特别是thederivederive_builder板条箱)来提供所需的内容——假设你愿意依赖它。

下一篇文章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值