跳转到内容

Rust 错误 E0616:field of struct is private(读取私有字段)

E0616 的意思是:你要的字段确实存在,但它对定义这个结构体的模块来说是私有的。 你可以拿着这个结构体,只是不能伸手到它里面去。每个人第一次把结构体挪进单独的模块时都会碰到它,每个把 v.len() 敲成 v.len 的人也会碰到它。修法通常是改成调用一个方法。这一页属于 Rust 编译错误详解系列。

error[E0616]: field `balance` of struct `Account` is private
--> src/main.rs:16:50
|
16 | println!("{} has {}", account.owner, account.balance); // ❌ 私有字段
| ^^^^^^^ private field

第一行的意思是“结构体 Account 的字段 balance 是私有的”。

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

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

同一个错误有好几种样子:

你写的是 rustc 怎么说 修法
account.balance field `balance` of struct `Account` is private 修复 2 或修复 3
在 Vec 上写 scores.len field `len` of struct `Vec` is private,外加 help: a method `len` also exists, call it with parentheses 修复 1
在 Duration 上写 timeout.secs field `secs` of struct `Duration` is private 修复 1(as_secs())
account.balance = 500 同样是 E0616:写入也会被拦住 修复 2

箭头指向点号后面的字段名。同一行的 account.owner 没问题,因为 owner 标了 pub。balance 没有标,所以只有 mod bank 里面的代码才能读取或修改它。

注意,Account 本身是 pub 的。这正是让人意外的地方:把结构体设为公有,并不会让它的字段也变成公有。 每个字段都要单独加 pub,否则就保持私有。

想想银行。你可以走进银行,谁都能看到账户上的户名。但你不能绕到柜台后面,打开保险柜自己数钱。你得请柜员办理,而柜员会按银行的规定来。

在 Rust 里,一切默认都是私有的:函数、模块,还有结构体字段(参见模块)。私有性看的是模块,而不是类型。mod bank 里的任何代码都能碰 balance,外面的代码不行。

这样做是为了让结构体能守住自己的规则。比如说,银行余额绝不能是负数。如果程序的每个部分都能写 account.balance = ...,那它们中的任何一个都可能破坏这条规则,你得翻遍整个代码库才能找出是谁干的。如果字段是私有的,就只有 mod bank 里那几个方法能修改它。在那里检查好规则,它在任何地方都成立。这个思想叫做封装(encapsulation)。

标准库也是这样做的。Vec 用私有字段来保存它的长度和内存缓冲区。如果你能在一个只装了 3 个元素的 vector 上写 v.len = 100,那读取 v[50] 就会读到随机的内存。把 len 设为私有,正是 Vec 能安全使用的原因。

修复 1:调用方法,而不是读取字段

Section titled “修复 1:调用方法,而不是读取字段”

当结构体来自标准库或其他 crate 时,选这个方法。你改不了它们的代码,而它们总会提供方法,让你看到允许你看的东西。当 rustc 说 help: a method ... also exists 时,加上 () 就行:

use std::time::Duration;
fn main() {
let scores = vec![10, 20, 30];
println!("共 {} 个分数", scores.len());
let timeout = Duration::from_secs(90);
println!("超时:{} 秒", timeout.as_secs());
}

输出:

共 3 个分数
超时:90 秒
▶ 动手试一试

方法名不一定和字段名相同(secs 要用 as_secs() 读取)。去 docs.rs 上看看这个类型的页面,或者敲完点号后让编辑器列出所有方法。

修复 2:加一个 getter,再加上遵守规则的方法

Section titled “修复 2:加一个 getter,再加上遵守规则的方法”

当这是你自己的结构体,而且字段有规则需要保护时,选这个方法。让字段保持私有,加一个返回字段值的 pub fn(也就是 getter),并且只通过会检查规则的方法来修改它:

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 }
}
// getter:谁都可以读取余额。
pub fn balance(&self) -> i64 {
self.balance
}
pub fn deposit(&mut self, amount: i64) {
if amount > 0 {
self.balance += amount;
}
}
// 规则就写在这里:余额永远不会低于零。
pub fn withdraw(&mut self, amount: i64) -> Result<(), String> {
if amount > self.balance {
return Err(format!("只有 {} 可用", self.balance));
}
self.balance -= amount;
Ok(())
}
}
}
fn main() {
let mut account = bank::Account::new("Ferris");
account.deposit(100);
println!("{} 的余额是 {}", account.owner, account.balance());
match account.withdraw(500) {
Ok(()) => println!("取出了 500"),
Err(e) => println!("无法取出 500:{e}"),
}
println!("余额仍然是 {}", account.balance());
}

输出:

Ferris 的余额是 100
无法取出 500:只有 100 可用
余额仍然是 100
▶ 动手试一试

Rust 允许方法和字段同名,所以 getter 通常就叫 balance()(而不是 get_balance())。有了这个 getter 之后,rustc 甚至会主动建议你用它:help: a method `balance` also exists, call it with parentheses。deposit 和 withdraw 就是你的 setter:它们会修改字段,但只按规则允许的方式修改。更多关于方法的内容见结构体。

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

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

当字段取什么值都可以、也没有别的东西依赖它时,选这个方法。网格上的一个点、一种颜色、一个设置结构体,都没有需要保护的规则。给它们写 getter 只是多敲字而已:

mod geometry {
pub struct Point {
pub x: i32,
pub y: i32,
}
}
fn main() {
let mut p = geometry::Point { x: 3, y: 4 };
p.x += 1;
println!("点的位置:({}, {})", p.x, p.y);
}

输出:

点的位置:(4, 4)
▶ 动手试一试

如果只想让你自己的代码看到这个字段,就写 pub(crate) x: i32(这个 crate 里的任何模块)或 pub(super) x: i32(只有父模块)。在程序里它们的效果和 pub 一样;在库里,它们能让这个字段不出现在你的公有 API 中。E0603 页面有一张列出所有级别的表。

要不要干脆设成 pub? 问问自己:“这个字段的某个值,或者单独改动它,会不会让结构体出错?”负数余额、和数据对不上的 len、不再等于所有商品之和的 total:这些都应该保持私有,用修复 2。如果答案是“不会,取什么值都行”,那 pub 就是最坦率的选择。

在 Vec 上用 len。 你写了 scores.len,得到了 field `len` of struct `Vec` is private。这是因为 Vec 真的有一个叫 len 的私有字段。修复是 scores.len()。有个好玩的细节:在 String 上写 name.len 会得到另一个错误 E0615(attempted to take value of method,把方法当成值来取),因为 String 没有叫这个名字的字段。修法一样。

从外面给字段赋值。 account.balance = 500; 也会报 E0616,写入同样会被拦住。别为了赋值就把字段设成 pub,加一个像 deposit 这样的方法,在里面完成修改并做检查。

把结构体挪进单独的模块或文件之后。 main.rs 里的代码几个月来一直在用 user.email。然后你把 User 挪进了 src/user.rs,结果每一处字段访问都坏了。之前没问题的字段,现在对 mod user 来说是私有的。给那些只是普通数据的字段加上 pub,其余的加 getter。

测试。 写在同一个文件里的 #[cfg(test)] mod tests 是一个子模块,所以它能读取私有字段。tests/ 文件夹里的测试在你的 crate 外面,和其他使用者一样,只能用 pub 的 API。

  • E0603:function `x` is private(函数是私有的)。同样的道理,用在函数、结构体和模块上。
  • E0451:同样是私有字段,只不过发生在用 Account { ... } 创建结构体时。
  • E0624:method `x` is private(方法是私有的)。方法(或 new)也需要 pub。
  • E0609:no field `x` on type `T` (类型上没有这个字段)。字段根本不存在(常常是拼写错误)。
  • E0615:attempted to take value of method。你写的是 x.len,想要的是 x.len()。
  • 所有编译错误:错误页面的完整列表。

小测验

1. pub struct Account { balance: u64 } 写在 mod bank 里。main 能读取 account.balance 吗?

2. 你得到了 `field len of struct Vec is private`。怎么修?

3. Score 结构体有一个 points 字段,它必须保持在 0 到 100 之间。最好的设计是什么?

得分: 0 / 3