跳转到内容

Rust 错误 E0063:missing field in initializer(缺少结构体字段)

E0063 的意思是:你在用 Name { ... } 创建一个结构体,但没有给每个字段都赋值。 Rust 从不替你填字段,所以每个字段都需要一个值,哪怕只是 0、false 或 None。错误信息会按名字列出缺少的字段,你一看就知道该补什么。这是 Rust 里最友好的错误之一。(本页属于 Rust 编译错误详解系列。)

error[E0063]: missing field `email` in initializer of `User`
--> src/main.rs:8:16
|
8 | let user = User {
| ^^^^ missing `email`

意思是:User 的初始化里缺少字段 email。

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

struct User {
name: String,
email: String,
active: bool,
}
fn main() {
let user = User {
name: String::from("Ferris"),
active: true,
// ❌ 没有 `email`
};
println!("{}", user.name);
}

标题里写出了缺少的字段(email)和结构体(User)。^^^^ 指向你开始构建它的那个结构体名,而不是某一行,因为问题在于有东西不在那里。

缺少的字段更多时,rustc 会按字母顺序列出来。缺两三个时,你会看到全部:

error[E0063]: missing fields `debug`, `retries` and `timeout_secs` in initializer of `Config`

缺四个或更多时,它只显示三个,剩下的给出数量:

error[E0063]: missing fields `debug`, `port`, `retries` and 1 other field in initializer of `Config`
--> src/main.rs:10:18
|
10 | let config = Config {
| ^^^^^^ missing `debug`, `port`, `retries` and 1 other field

“1 other field”的意思是还有 1 个。去看 struct Config 的定义,就能看到完整的列表。

就像一张每一栏都必填的表格。即使你打算以后再填,也不能空着“邮箱”那一栏就交上去。

在很多语言里,缺少的字段会悄悄变成 null、0 或空字符串。于是程序带着一个没有邮箱的用户跑了起来,然后在很远的地方出问题,而且常常是在生产环境里。Rust 没有 null,也不会背着你编造值。一个 User 永远有一个真正的 email。参见定义和创建结构体。

这并不意味着你每次都得把所有字段敲一遍。Rust 提供了简短的写法,表示“使用默认值”或“其余的从另一个值复制过来”,下面的修复方法会讲到。关键在于由你来选,而且这个选择就写在代码里。

大多数时候都选这个。这个字段存在是有原因的,所以给它一个真正的值。如果你有和字段同名的变量,可以用简写 User { name, email, active }。

struct User {
name: String,
email: String,
active: bool,
}
fn main() {
let user = User {
name: String::from("Ferris"),
email: String::from("[email protected]"), // ✅ 补上了
active: true,
};
println!("{} <{}> 活跃:{}", user.name, user.email, user.active);
}

输出:

Ferris <[email protected]> 活跃:true
▶ 动手试一试

修复 2:derive Default,再用 ..Default::default()

Section titled “修复 2:derive Default,再用 ..Default::default()”

当结构体字段很多,而且大部分字段都有一个明显的“空”值时(比如设置或选项),选这个。给结构体加上 #[derive(Default)],然后只写你关心的字段,最后加上 ..Default::default()。.. 的意思是“我没写的字段,全都从这个值里取”。

#[derive(Debug, Default)]
struct Config {
host: String,
port: u16,
debug: bool,
timeout_secs: u64,
retries: u8,
}
fn main() {
let config = Config {
host: String::from("localhost"),
port: 8080,
..Default::default() // ✅ debug、timeout_secs 和 retries 取默认值
};
println!("{:?}", config);
}

输出:

Config { host: "localhost", port: 8080, debug: false, timeout_secs: 0, retries: 0 }
▶ 动手试一试

derive 出来的默认值是:数字为 0,bool 为 false,String 为空字符串,Option 为 None。如果 0 不是个好的默认值(超时 0 秒多半不是),就自己写一个 impl Default for Config,给出更合适的值。参见 Default:合理的初始值。

如果忘了写 #[derive(Default)],错误会变成 E0277:the trait bound `Config: Default` is not satisfied(Config 不满足 Default 约束),并附带 help: consider annotating `Config` with `#[derive(Default)]`(考虑给 Config 加上 #[derive(Default)])。

当你在很多地方创建这个结构体,或者某些字段总是以同样的值开始时,选这个。把完整的结构体字面量放进一个函数里,其他地方都调用这个函数。以后加字段时,只需要改 new,不用改每一个调用的地方。

struct Player {
name: String,
score: u32,
level: u8,
}
impl Player {
fn new(name: &str) -> Player {
Player {
name: name.to_string(),
score: 0, // 每个新玩家都从 0 分开始……
level: 1, // ……在第 1 级
}
}
}
fn main() {
let ana = Player::new("Ana");
let mut bo = Player::new("Bo");
bo.score += 50;
println!("{}:{} 分,第 {} 级", ana.name, ana.score, ana.level);
println!("{}:{} 分,第 {} 级", bo.name, bo.score, bo.level);
}

输出:

Ana:0 分,第 1 级
Bo:50 分,第 1 级
▶ 动手试一试

new 里面的结构体字面量仍然需要每个字段,所以漏掉的字段会在那里以 E0063 的形式出现。这没关系:你只需要修一次。参见构造函数风格的函数。

修复 4:用 ..other 从另一个值复制其余字段

Section titled “修复 4:用 ..other 从另一个值复制其余字段”

当你已经有一个相似的值,想要一份只改了几处的副本时,选这个。..base 会把你没写的字段全都从 base 里取:

#[derive(Debug, Clone)]
struct Theme {
name: String,
font_size: u8,
dark: bool,
}
fn main() {
let base = Theme { name: String::from("默认"), font_size: 14, dark: false };
// 只改 `name` 和 `dark`,其余的来自 `base`
let night = Theme { name: String::from("夜间"), dark: true, ..base };
// `..base` 会把 `name` 从 base 里移动出来,所以先克隆,才能继续使用 base
let large = Theme { font_size: 20, ..base.clone() };
println!("{:?}", night);
println!("{:?}", large);
println!("{:?}", base);
}

输出:

Theme { name: "夜间", font_size: 14, dark: true }
Theme { name: "默认", font_size: 20, dark: false }
Theme { name: "默认", font_size: 14, dark: false }
▶ 动手试一试

有一点要知道:..base 会把 String 这样的字段从 base 里移动出来。在 night 里这没问题,因为我们自己写了 name,从 base 取来的只有 Copy 的字段(font_size)。在 large 里,name 会来自 base,所以我们用 ..base.clone(),让 base 还能继续用。如果不克隆,之后又用了 base,就会得到 E0382。

有时候一个字段确实可以为空:用户还没填的电话号码、昵称、结束日期。把这样的字段定义成 Option。你还是得把它写出来(Rust 不会替你填 None),但现在 None 是一个诚实的、被允许的值。参见 Option。

#[derive(Debug, Default)]
struct Contact {
name: String,
email: String,
phone: Option<String>, // 可能没有
nickname: Option<String>,
}
fn main() {
let a = Contact {
name: String::from("Ana"),
email: String::from("[email protected]"),
phone: None, // 明确写出来:“还没有电话”
nickname: Some(String::from("Annie")),
};
// 有了 Default,你省略的每个 Option 都会变成 None
let b = Contact { name: String::from("Bo"), ..Default::default() };
println!("{} 的电话:{:?},昵称:{:?}", a.name, a.phone, a.nickname);
println!("{} 的邮箱:{:?},电话:{:?}", b.name, b.email, b.phone);
}

输出:

Ana 的电话:None,昵称:Some("Annie")
Bo 的邮箱:"",电话:None
▶ 动手试一试

注意 b.email 是 ""。这就是 Default 的陷阱:一个必填的字段悄悄变成了空的。只对真正可选的东西用 Option,像 email 这样的必填字段就保持普通类型,每次都填上。

刚给结构体加了一个字段之后。 你给 User 加了 created_at,项目里的每一个 User { ... } 突然都报 E0063。这是 Rust 在帮你:每一处都是你需要考虑的地方。用 new 构造函数(修复 3)可以让这个列表保持很短。

带命名字段的枚举变体。 Shape::Rectangle { width: 2.0 } 会报 missing field `height` in initializer of `Shape`。注意,信息里写的是枚举名,而不是变体名。修复:给这个变体的每个字段都赋值。参见枚举可以携带数据。

返回结构体的函数。 在 fn new_player(name: &str) -> Player { Player { name: name.to_string(), score: 0 } } 里,箭头指向函数里的 Player,并显示 missing `level`。修复:在那个字面量里补上 level。

小测验

1. rustc 说:missing fields a, b, c and 2 other fields。一共缺了几个字段?

2. 你的结构体有一个暂时用不到的 Option<String> 字段。你该给它写什么?

3. 要让 Config { port: 80, ..Default::default() } 通过编译,需要什么?

得分: 0 / 3