结构体
约 2285 字大约 8 分钟
2026-02-05
关键词:结构体(struct)、字段(field)、实例(instance)、点号访问(dot notation)、字段初始化简写(field init shorthand)、结构体更新语法(struct update syntax)、元组结构体(tuple struct)、类单元结构体(unit-like struct)、移动(move)、拷贝(Copy trait)
1) 背景引入:为什么我们需要 struct,而不是一直用元组?
在 Rust 里,元组(tuple)能把多个值打包在一起,比如 (String, u64, bool)。 但问题也很明显:
- 可读性差:你看到
user.1很难立刻知道它代表什么 - 依赖顺序:值的意义绑定在“第几个位置”,一旦字段增减或调换,代码就容易出错
- 扩展性一般:当数据越来越多,靠“记索引”会越来越痛苦
这时就轮到结构体登场。
**结构体(struct)**和元组相似:都能把多个相关值组合成一个整体。 但结构体的关键优势是:每个数据都有名字(字段 field),因此表达更清晰、更安全。
一句话总结: 元组像“没有标签的快递箱”,结构体像“每个格子都贴了标签的收纳盒”。
2) 核心原理解析:struct 到底是什么?你在定义什么?
2.1 结构体(struct)是什么?
**结构体(struct)**是一种自定义数据类型,用来把“多个相关数据”组织成一个整体类型。
定义结构体时你需要做两件事:
- 给结构体一个名字(这个名字描述“这些数据组合在一起的意义”)
- 在大括号里声明各个字段(field)的名称和类型
2.2 字段(field)是什么?
字段(field)就是结构体里的“带名字的成员变量”。 与元组只记“第几个”不同,结构体字段是“用名字访问”。
这带来两个直接好处:
- 可读性:
user.email比user.2强太多 - 灵活性:创建实例时字段不必按声明顺序填写(因为靠名字匹配)
3) 实操步骤:从定义、创建、访问、修改,到“更省代码”的写法
3.1 定义一个结构体:User
// 定义结构体:User(用户账户)
// - active:是否激活
// - username:用户名
// - email:邮箱
// - sign_in_count:登录次数
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}术语提示:
- 结构体(struct):自定义类型
- 字段(field):结构体内部的命名成员
3.2 创建结构体实例(instance):用 “字段名: 值”
fn main() {
// 创建 User 的一个实例 user1
// 注意:字段顺序可以不按 struct 里声明的顺序写,只要 key 对得上即可
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}术语提示: 实例(instance):某个结构体类型的“具体值”。
3.3 访问字段:点号语法(dot notation)
想拿到某个字段,用点号:
// user1.email 表示访问 user1 这个实例的 email 字段
// 这比元组的 user1.2 直观得多
let mail = user1.email;3.4 修改字段:整个实例必须是可变(mut)
这里是 Rust 新手常见坑:
Rust 不允许“只把某些字段标记为可变”。 要改任何字段,必须让整个实例是 mut。
fn main() {
// 让整个实例可变
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
// 使用点号语法修改字段
user1.email = String::from("anotheremail@example.com");
}为什么要这样设计? 因为“可变性(mutability)”是 Rust 的重要安全边界:可变意味着可能出现并发修改、别名问题等;Rust 倾向于让“修改意图”更明确。
3.5 用函数构造结构体:返回 User 实例
我们通常会写一个构造函数(不是类!只是普通函数):
// build_user:接收 email 和 username,返回一个 User 实例
fn build_user(email: String, username: String) -> User {
User {
active: true,
// 这里写 username: username 有点啰嗦
username: username,
email: email,
sign_in_count: 1,
}
}这段代码背后隐含一个所有权细节:
email: String、username: String作为参数传入 所有权会 move(移动 move)到函数内部- 然后这些 String 被放进返回的
User中 最终所有权 move 出函数,交给调用者
4) 进阶省代码:两种“更 Rust”的语法糖
4.1 字段初始化简写(field init shorthand)
当“字段名”和“变量名”完全一致时,可以省略重复写法。
字段初始化简写(field init shorthand):
email: email可以直接写成
fn build_user(email: String, username: String) -> User {
User {
active: true,
// 由于字段名与变量名一致,可以简写
username,
email,
sign_in_count: 1,
}
}这不是“魔法”,只是语法糖,等价于你手写 username: username。
4.2 结构体更新语法(struct update syntax):..user1
当你想“基于一个旧实例,复制大部分字段,只改少数几个”,可以用:
结构体更新语法(struct update syntax):
..user1表示“剩余没写的字段,统统从 user1 里拿”
先看“笨办法”:字段一个个抄
fn main() {
// 假设 user1 已存在
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}再看“更新语法”:少写很多
fn main() {
// 只设置你想改的字段
let user2 = User {
email: String::from("another@example.com"),
// 其余字段从 user1 获取
..user1
};
}注意:
..user1必须写在最后 因为它代表“剩余字段的来源”,语义上要收尾。
5) 关键转折:为什么用了 ..user1 之后,user1 有时不能再用?
这是本节最容易踩坑、也是最有价值的一点: 结构体更新语法会触发 move(移动 move)。
原因很简单:
username、email是StringString默认不实现 Copy- 当
user2从user1“拿走”这些字段时,本质是移动所有权
5.1 用图理解 ..user1 的 move 规则

5.2 你什么时候还能继续用 user1?
看是否“被 move 的字段”还有没有剩下:
- 如果
user2使用了..user1,并且 move 走了username: String那么user1.username就没了 →user1很可能整体不可再用 - 但如果某些字段你没有从 user1 move(比如你给 user2 提供了新的 String),那么 user1 的那部分仍有效
原文强调的一个点是:
bool、u64这类实现了 Copy trait(拷贝特性 Copy) 的类型 从user1取值时是“复制”而不是“移动”String这类堆上数据通常是 move
中文解释 + 原文标注:
- Copy trait(Copy trait):表示类型可以按位复制,复制后原值仍然可用
- move(move):所有权转移,原变量失效
6) 结构体的三种“形态”:普通 struct、元组 struct、类单元 struct
Rust 的 struct 不止一种写法,而是三种常见形态,用于不同的表达需求。
6.1 普通结构体(带字段名)
就是我们前面用的 User { email: ..., username: ... } 适合字段多、语义强、需要可读性的场景。
6.2 元组结构体(tuple structs):有“类型名”,但字段没名字
元组结构体(tuple structs): 形状像元组
(i32, i32, i32),但有自己的类型名Color用来表达“这三个 i32 的组合有特定含义”。
// 元组结构体:字段没有名字,只有类型
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
// black 和 origin 虽然底层都是三个 i32,但它们是不同类型
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}重点:类型不同! 即使字段类型完全相同,Color 也不能当 Point 用。
这解决了历史上很多“类型混用”的 bug:
- 在 C/C++ 里你可能用
typedef或 struct 包一层 - 在 Rust 里 tuple struct 就是非常轻量的“强类型封装”
如何访问与解构?
- 访问:
black.0、black.1 - 解构(destructure):需要写出类型名
fn main() {
let origin = Point(0, 0, 0);
// 解构必须写类型名 Point(...)
let Point(x, y, z) = origin;
// 现在 x,y,z 都是 i32
println!("{x}, {y}, {z}");
}6.3 类单元结构体(unit-like structs):没有任何字段
类单元结构体(unit-like structs): 类似
()(unit type 单元类型),但有类型名。 用于“我需要一个类型来挂 trait(trait)/实现行为,但不需要存数据”。
// 定义一个没有字段的结构体
struct AlwaysEqual;
fn main() {
// 创建实例时也不需要括号或大括号
let subject = AlwaysEqual;
}这在后续讲 trait(trait)时会非常常见: 你可能只需要“一个类型作为标记(marker)”,而不是一堆字段。
7) 扩展思考:什么时候该用哪种结构体?
7.1 选择指南(非常实用)
- 需要高可读性、字段含义明确 → 普通结构体
struct User { ... } - 需要“强类型封装”,但字段名反而显得多余 → 元组结构体
struct Color(i32,i32,i32) - 只需要一个类型来实现 trait 或做标记 → 类单元结构体
struct AlwaysEqual;
7.2 与所有权(ownership)联动的经验法则
String、Vec<T>这类字段经常触发 move 使用..user1时要特别注意:旧实例还能不能用,取决于哪些字段被 move。如果你希望“复制而不是移动”,考虑:
字段类型是否实现 Copy
或者显式 clone(克隆 clone)
clone(clone):创建深拷贝,代价更高,但保留原值
贡献者
flycodeu
版权所有
版权归属:flycodeu
