更新具有私有字段的Rust结构体的公共字段

9

我有一个名为Foo的结构体,它代表了一种外部序列化格式。 Foo有数十个字段,而且还在不断添加新的字段。值得庆幸的是,所有新添加的字段都保证有合理的默认值。

Rust语言提供了一种很好的语法,可以使用默认值创建结构体,然后更新选定的几个值:

Foo {
  bar: true,
  ..Default::default()
} 

同样地,我们可以使用类型为PhantomData的私有字段来表示“这个结构在未来版本中可能会有更多的字段”的想法。

但是如果我们将这两种习惯用法结合起来,就会出现错误:

use std::default::Default;

mod F {
    use std::default::Default;
    use std::marker::PhantomData;

    pub struct Foo {
        pub bar: bool,
        phantom: PhantomData<()>,
    }

    impl Default for Foo {
        fn default() -> Foo {
            Foo {
                bar: false,
                phantom: PhantomData,
            }
        }
    }
}

fn main() {
    F::Foo {
        bar: true,
        ..Default::default()
    };
}

这会给我们带来错误提示:
error: field `phantom` of struct `F::Foo` is private [--explain E0451]
  --> <anon>:23:5
   |>
23 |>     F::Foo {
   |>     ^

从逻辑上讲,我认为这应该是可行的,因为我们只更新公共字段,并且这是一个有用的习惯用法。另一种选择是支持类似以下内容:

Foo::new()
  .set_bar(true)

如果有数十个字段,那么这将变得很繁琐。

我该如何解决这个问题?


3
phantom 重命名为 __phantom,将其公开并添加 #[doc(hidden)] 属性。具体示例:std::io::ErrorKind::__Nonexhaustive - mcarton
2
请注意,std 有点特殊,因为它可以声明 __Nonexhaustive 变体不稳定,这样如果调用者使用的是稳定版 Rust,则无法使用它。采用相同技巧(这是我绝对会做的事情)的库编写者必须依赖于约定。 (实际上我认为这不是一个真正的问题,我只是在挑剔。) - BurntSushi5
@emk,我刚刚学习了PhantomData。感谢您提供的这个有用的示例! - ljedrz
@BurntSushi5 有点丑,但如果需要警告,#[deprecated] 在这里可以工作。 - mcarton
1
@mcarton 如果您将您的评论转换为顶级答案,我会接受它。 - emk
2个回答

7

默认的字段语法不起作用,因为您仍然会创建一个新实例(即使您尝试从其他对象中获取一些字段值)。

The alternative is to support something like:

Foo::new()
  .set_bar(true)

...which will get tedious with dozens of fields.

我不确定即使有许多领域,也能做到这一点:

Foo::new()
   .set_bar(true)
   .set_foo(17)
   .set_splat("Boing")

相比之下,这要复杂得多:

Foo {
   bar: true,
   foo: 17,
   splat: "Boing",
   ..Foo::default()
}

或者,你可以将公共字段分离到它们自己的类型中:

pub struct FooPub {
    pub bar: bool,
    // other pub fields
}

pub struct Foo {
    pub bar: bool,
    // other pub fields
    // alternatively, group them: pub public: FooPub,

    foo: u64,
}

impl Foo {
    pub fn new(init: FooPub) {
        Foo {
            bar: init.bar,
            // other pub fields
            // alternative: public: init

            // private fields
            foo: 17u64,
        }
    }
}

你需要这样调用它:

Foo::new(FooPub{ bar: true })

或者添加一个fn FooPub::default()以便您默认一些字段:
Foo::new(FooPub{ bar: true, ..FooPub::default()})

谢谢!这些都是针对高级API的可爱解决方案,其字段相对较少。但在我的情况下,我正在使用一个表示第三方文件格式反序列化的更低级别结构,并且需要处理数十个很少使用的字段,并且不需要将结构拆分成两个部分。但您的答案将帮助那些有更简单问题的人。谢谢! - emk

5
phantom 改名为 __phantom,将其公开并添加 #[doc(hidden)] 标记。
use std::default::Default;

mod foo {
    use std::default::Default;
    use std::marker::PhantomData;

    pub struct Foo {
        pub bar: bool,

        // We make this public but hide it from the docs, making
        // it private by convention.  If you use this, your
        // program may break even when semver otherwise says it
        // shouldn't.
        #[doc(hidden)]
        pub _phantom: PhantomData<()>,
    }

    impl Default for Foo {
        fn default() -> Foo {
            Foo {
                bar: false,
                _phantom: PhantomData,
            }
        }
    }
}

fn main() {
    foo::Foo {
        bar: true,
        ..Default::default()
    };
}

这是一种不太常见的模式,以下是一个实际例子:std::io::ErrorKind::__Nonexhaustive
当然,如果用户选择使用__named字段,则不会收到任何警告提示,但__可以明确表达意图。如果需要警告,则可以使用#[deprecated]

谢谢!我接受了你的答案,因为它直接回答了问题,但是@Chris Emerson的答案提供了更高级API的好的替代方案。 - emk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接