跳转到内容

Rust 错误 E0451:field of struct is private(构造时遇到私有字段)

E0451 的意思是:你想用 Account { ... } 这样的字面量来构建结构体,但它有一个字段是私有的。 写结构体字面量必须把每个字段都填上,所以只要有一个私有字段,你就被挡在门外了。这是故意的:模块希望你通过它自己的构造函数来创建。修法几乎总是调用(或者添加)一个 new 函数。这一页属于 Rust 编译错误详解系列。

error[E0451]: field `balance` of struct `Account` is private
--> src/main.rs:21:9
|
19 | let account = bank::Account {
| ------------- in this type
20 | owner: String::from("Ferris"),
21 | balance: 1_000_000, // ❌ 私有字段
| ^^^^^^^ private field

第一行的意思是“结构体 Account 的字段 balance 是私有的”。in this type 标出了你正在构建的类型。

产生这个错误的程序是这样的:

mod bank {
pub struct Account {
pub owner: String,
balance: i64, // 私有
}
impl Account {
pub fn new(owner: &str) -> Account {
Account { owner: owner.to_string(), balance: 0 }
}
pub fn balance(&self) -> i64 {
self.balance
}
}
}
fn main() {
let account = bank::Account {
owner: String::from("Ferris"),
balance: 1_000_000, // ❌ 私有字段
};
println!("{} has {}", account.owner, account.balance());
}

你也可能看到它的几个近亲:

你写的是 rustc 怎么说
字面量里写了私有字段 error[E0451]: field `balance` of struct `Account` is private
Config { verbose: true, ..Default::default() } 同样是 E0451,指向 ..Default::default()(见修复 3)
字面量里没写私有字段 cannot construct `Account` with struct literal syntax due to private fields,常常还带有 help: you might have meant to use the `new` associated function
给元组结构体写 Percent(150) E0603:tuple struct constructor `Percent` is private

它们的修法都一样。

箭头指向你无权设置的字段:balance。owner 是 pub 的,所以设置它没问题。但结构体字面量是“要么全有,要么全无”的:你不能把 balance 省掉(Rust 不会替你补上缺的字段),也不能给它赋值。所以在 mod bank 外面,根本没有办法写出 Account { ... }。

想象一下在银行开户。你不能在表格上自己填写初始余额。你填上自己的名字,银行创建账户,由银行决定余额从零开始。Account::new 就是那张表格。

和函数、模块一样,结构体字段默认也是私有的(参见模块)。如果外面的代码能随意用任何余额来构建 Account,模块定下的规则就形同虚设了。有人可以创建一个有一百万的账户,或者一个负五十的账户。正因为只有 mod bank 能写这个字面量,程序里的每一个 Account 都是由了解规则的代码创建的。

还有第二个好处。如果你以后要加一个私有字段,比如 created_at 或 history,模块外面的代码不会坏,因为它从来没写过字面量。它调用的是 new,你只需在一个地方更新 new。

修复 1:使用(或添加)pub fn new 构造函数

Section titled “修复 1:使用(或添加)pub fn new 构造函数”

大多数时候都选这个方法。先找找有没有 new(或 from_...、with_...)函数,rustc 常常会给你指出来。如果这是你自己的结构体,而且还没有,就写一个:

mod bank {
pub struct Account {
pub owner: String,
balance: i64, // 私有
}
impl Account {
// 每个账户都从 0 开始。钱只能通过 deposit() 存进来。
pub fn new(owner: &str) -> Account {
Account { owner: owner.to_string(), balance: 0 }
}
pub fn deposit(&mut self, amount: i64) {
if amount > 0 {
self.balance += amount;
}
}
pub fn balance(&self) -> i64 {
self.balance
}
}
}
fn main() {
let mut account = bank::Account::new("Ferris");
account.deposit(250);
println!("{} 的余额是 {}", account.owner, account.balance());
}

输出:

Ferris 的余额是 250
▶ 动手试一试

在 mod bank 里面可以写字面量,所以 new 可以随意写 Account { ... }。别忘了给 new 本身也加上 pub,否则你会得到 E0624(associated function `new` is private)。如果某些输入应该被拒绝,就让 new 返回 Option<Account> 或 Result<Account, String>,E0603 页面有一个例子。更多关于构造函数的内容见结构体。

修复 2:数据很简单时,把字段设为 pub

Section titled “修复 2:数据很简单时,把字段设为 pub”

当字段的任何组合都没问题时,选这个方法。颜色不过是三个数字,不存在需要防范的“错误”颜色。这时用字面量构建它是最清楚的写法:

mod theme {
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
}
fn main() {
let ferris_orange = theme::Color { r: 247, g: 76, b: 0 };
println!("#{:02x}{:02x}{:02x}", ferris_orange.r, ferris_orange.g, ferris_orange.b);
}

输出:

#f74c00
▶ 动手试一试

每个字段都需要 pub。如果只应该由你自己的 crate 来构建它,给字段加 pub(crate) 也可以。

什么时候应该让字段保持私有: 当某个值可能是错的(负数余额、超过 100 的百分比、没有 @ 的邮箱),或者两个字段必须彼此一致(total 必须等于 items 的总和)时。这时字面量会让人跳过检查,修复 1 才是正确的选择。

修复 3:先从 Default 开始,再修改公有字段

Section titled “修复 3:先从 Default 开始,再修改公有字段”

这适合那些有合理默认值、部分字段公有、部分字段私有的“设置”类结构体。先说陷阱。结构体更新语法 ..Default::default() 看起来应该能用,但它不行:

mod settings {
pub struct Config {
pub verbose: bool,
retries: u32, // 私有:必须保持在 1 到 10 之间
}
impl Default for Config {
fn default() -> Config {
Config { verbose: false, retries: 3 }
}
}
}
fn main() {
let config = settings::Config { verbose: true, ..Default::default() };
println!("{}", config.verbose);
}
error[E0451]: field `retries` of struct `Config` is private
--> src/main.rs:15:54
|
15 | let config = settings::Config { verbose: true, ..Default::default() };
| ^^^^^^^^^^^^^^^^^^ field `retries` is private

..Default::default() 仍然是结构体字面量:它会把 retries 复制进新结构体,这就算是给它赋值了。正确的做法是先创建默认值,然后再修改公有字段:

mod settings {
pub struct Config {
pub verbose: bool,
retries: u32, // 私有:必须保持在 1 到 10 之间
}
impl Default for Config {
fn default() -> Config {
Config { verbose: false, retries: 3 }
}
}
impl Config {
pub fn retries(&self) -> u32 {
self.retries
}
}
}
fn main() {
let mut config = settings::Config::default();
config.verbose = true; // 公有字段可以在之后修改
println!("verbose: {}, retries: {}", config.verbose, config.retries());
}

输出:

verbose: true, retries: 3
▶ 动手试一试

如果每个字段都是 pub 的,..Default::default() 就完全没问题。挡住它的只有私有字段。

当选项有好几个、其中一些还有规则时,选这个方法。每个方法接收结构体,修改一项设置(并做检查),再把结构体交回来,所以这些调用可以很顺地串起来:

mod settings {
pub struct Config {
verbose: bool,
retries: u32,
}
impl Config {
pub fn new() -> Config {
Config { verbose: false, retries: 3 }
}
pub fn verbose(mut self, on: bool) -> Config {
self.verbose = on;
self
}
// 规则就写在这里:重试 1 到 10 次。
pub fn retries(mut self, n: u32) -> Config {
self.retries = n.clamp(1, 10);
self
}
pub fn describe(&self) -> String {
format!("verbose: {}, retries: {}", self.verbose, self.retries)
}
}
}
fn main() {
let config = settings::Config::new().verbose(true).retries(50);
println!("{}", config.describe());
}

输出:

verbose: true, retries: 10
▶ 动手试一试

要求重试 50 次,结果悄悄变成了 10 次,因为规则只写在一个地方。很多 crate 都这样做,有时还会用一个单独的 ConfigBuilder 类型,最后调用一次 .build()。那种写法见设计模式。

把结构体挪进模块之后。 main.rs 里的每一个 User { name, email } 原本都好好的,直到 User 被挪进了 mod user(或 src/user.rs),现在全都报错。逐个结构体来决定:普通数据就给字段加 pub(修复 2);有规则的就加一个 pub fn new(修复 1)。

来自标准库或 crate 的类型。 你没法写 Vec { ... } 或 Duration { ... }。rustc 会说 cannot construct `Vec<_, _>` with struct literal syntax due to private fields(因为有私有字段,不能用结构体字面量构建),并列出 Vec::new()、Vec::with_capacity(_) 这样的函数。用这些函数,或者 Duration::from_secs(5)。

来自其他 crate 的 #[non_exhaustive] 结构体。 这时连公有字段都帮不上忙,错误代码也不一样:error[E0639]: cannot create non-exhaustive struct using struct expression。这个 crate 想在以后加字段时不破坏你的代码,所以请用它的构造函数或 Default,做法和修复 1、修复 3 完全一样。

  • E0616:同样是私有字段,只不过发生在读取它(account.balance)时。
  • E0603:tuple struct constructor `X` is private(元组结构体的构造器是私有的),以及私有的函数、结构体或模块。
  • E0624:associated function `new` is private。你的构造函数也需要 pub。
  • E0560:struct `X` has no field named `y` 。字面量里的字段名拼错了。
  • E0063(英文):missing field `x` in initializer of `Y` 。你漏掉了一个公有字段。
  • 所有编译错误:错误页面的完整列表。

小测验

1. pub struct Ticket { pub title: String, id: u32 } 写在 mod tickets 里。main 怎样才能创建一个 Ticket?

2. 当 Config 有私有字段时,为什么 Config { verbose: true, ..Default::default() } 会失败?

3. 下面哪个结构体适合全部用 pub 字段和普通字面量?

得分: 0 / 3