如何根据编译功能标志向枚举添加生命周期

How to add a lifetime to an enum depending on a compilation feature flag

我目前正在尝试实现一个协议库,其中协议的许多部分都隐藏在功能标志之后。为此,我想知道 Rust 中是否有一种符合人体工程学的方法来让我的枚举之一具有生命周期参数,具体取决于某些功能标志。

在解析方面,我有一个名为 Action 的已解析项目,如下所示:

enum Action<'a> {
    Nop(Nop),
    WriteFileData(WriteFileData<'a>),
    ...
}

struct Nop {
    pub group: bool,
    pub response: bool,
}

struct WriteFileData<'a> {
    ...
    pub data: &'[u8], // Not exactly the final code, but equivalent
}

到目前为止一切都很好。 但是在我的库中,我希望这些动作类型中的每一个都在一个功能标志后面:

enum Action<'a> {
    #[cfg(feature = "decode_nop")]
    Nop(Nop),
    #[cfg(feature = "decode_write_file_data")]
    WriteFileData(WriteFileData<'a>),
    ...
}

在这一点上,如果你只选择没有生命周期的解码动作类型的特征(例如只有decode_nop),你最终编译:

enum Action<'a> {
    Nop(Nop),
}

无法编译,因为枚举声明中存在无用的生命周期。

我第一次发现有条件的生命周期有前途:

enum Action<#[cfg(feature = "decode_action_lifetime")] 'a> {
   ...
}

impl<#[cfg(feature = "decode_action_lifetime")] 'a> Action<#[cfg(feature = "decode_action_lifetime")] 'a> {
   ...
}

不幸的是,这不会编译,因为:

error: expected one of `>`, const, identifier, lifetime, or type, found `#`
   --> src/v1_2/action/mod.rs:123:63
    |
123 | impl<#[cfg(feature = "decode_action_lifetime")] 'a> Action<#[cfg(feature = "decode_action_lifetime")]'a> {
    |                                                               ^ expected one of `>`, const, identifier, lifetime, or type

由于这是一个语法错误,我什至不确定我在这里尝试做的事情在 Rust 中是否合法。

然后我想出了 2 种方法来解决我的问题:

  1. 在适当的功能标志组合后面复制具有生命周期和非生命周期变体的操作定义。

    #[cfg(feature = "decode_with_lifetime")]
    enum Action<'a> {
        #[cfg(feature = "decode_nop")]
        Nop(Nop),
        #[cfg(feature = "decode_write_file_data")]
        WriteFileData(WriteFileData<'a>),
        ...
    }
    
    #[cfg(not(feature = "decode_with_lifetime"))]
    enum Action {
        #[cfg(feature = "decode_nop")]
        Nop(Nop),
        ...
    }
    

    问题是它需要复制所有引用该类型的代码:

    • 实现块。
    • 使用此类型的其他函数。
    • 在尝试支持相同功能标志时会使用此库的外部代码。

    所以我显然不喜欢该解决方案,因为它对我的板条箱和使用该板条箱的代码的维护成本很高。

  2. 在其结构中使用 PhantomData<&'a ()> 标记向所有没有子项 (Nop<'a>) 添加生命周期参数。

    struct Nop<'a> {
        pub group: bool,
        pub response: bool,
        pub phantom: PhantomData<&'a ()>,
    }
    

    但是 public 那些不需要生命周期的结构的声明,我认为它是我的 API 的一部分(所有字段都可以访问,以便可以直接构建它们),现在突然包含一个不方便的和不直观的幻影场。

    所以如果我想让我的 API 对用户来说更清楚,这可能意味着我必须为每个结构添加构建器函数,这样用户就不必关心我的库实现细节(幻影场)。

    此外,Rust 在其函数中不支持任何类型的命名参数(据我所知),这使得具有大量字段(> 4 个字段)的结构的构建器难以阅读且容易出错.解决方案可能是创建一个参数类型,它与第一个没有幻影参数的结构完全一样,但这意味着要添加更多代码。

    struct Nop<'a> {
        pub group: bool,
        pub response: bool,
        pub phantom: PhantomData<&'a ()>,
    }
    
    struct NopBuilderParam {
        pub group: bool,
        pub response: bool,
    }
    
    impl<'a> Nop<'a> {
       pub new(group: bool, response: bool) -> Self {
           Self {
               group,
               response,
               phantom: phantomData,
           }
       }
    }
    

    而且,虽然这应该不是真正的问题,但将此生命周期约束添加到完全拥有的结构(这意味着对用户代码的额外约束)让我有点困扰。但我想我可以忍受那个。

结论:

我目前正在使用解决方案 2。但我非常想知道是否有其他方法可以完成我正在尝试做的事情。

根据特性更改类型的数量或类型的生命周期参数通常不是一个好主意。如果您在同一个项目中有多个 crate 使用不同的功能,那么所有被任何一个激活的功能都将被激活。这将是脆弱的,因为一个 crate 可以启用一个功能,这会导致在不同的 crate 中出现编译错误。

根据特性更改 类型 的唯一好时机是针对所有 crate 始终相同的条件,例如目标体系结构或字大小。

您使用 PhantomData 的解决方案似乎不是一个很好的妥协。污染枚举使用的所有类型非常笨拙。一些备选方案:

添加一个额外的枚举变体:

enum Action<'a> {
    Nop(Nop),
    ...
    _NotUsed<PhantomData<&'a ()>>,
}

这对于 #[non_exhaustive] 实际上并没有那么糟糕(假设这是有道理的),因为它会迫使用户在 _ 上匹配并且不必看到丑陋的额外变体.

另一种避免将生命周期扩展到比此枚举更远的方法是将其添加到每个变体中,如下所示:

enum Action<'a> {
    Nop(Nop, PhantomData<&'a ()>),
    WriteFileData(WriteFileData<'a>),
    ...
}

值得质疑使用功能的想法。如果其中之一为真,则功能门可能很有价值:

  • 您可以在不使用该功能时减少依赖项的数量
  • 您可以显着在不使用该功能时减少编译时间
  • 您真的很在意二进制文件的大小,这真的很重要
  • 存在真正的兼容性差异,例如OS 和目标体系结构,或分配器的可用性。

如果其中 none 为真,那么您可能只是在为您自己和您的用户增加维护开销,而收效甚微。