Rust 结构体(struct)完全入门:从“像元组”到“可读性 + 所有权语义”一网打尽

关键词:结构体(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)是一种自定义数据类型,用来把“多个相关数据”组织成一个整体类型。

定义结构体时你需要做两件事:

  1. 给结构体一个名字(这个名字描述“这些数据组合在一起的意义”)
  2. 在大括号里声明各个字段(field)的名称和类型

2.2 字段(field)是什么?

字段(field)就是结构体里的“带名字的成员变量”。 与元组只记“第几个”不同,结构体字段是“用名字访问”。

这带来两个直接好处:

  • 可读性user.emailuser.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: Stringusername: String 作为参数传入 所有权会 move(移动 move)到函数内部
  • 然后这些 String 被放进返回的 User最终所有权 move 出函数,交给调用者

4) 进阶省代码:两种“更 Rust”的语法糖

4.1 字段初始化简写(field init shorthand)

当“字段名”和“变量名”完全一致时,可以省略重复写法。

字段初始化简写(field init shorthand): email: 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)

原因很简单:

  • usernameemailString
  • String 默认不实现 Copy
  • user2user1“拿走”这些字段时,本质是移动所有权

5.1 用图理解 ..user1 的 move 规则

image-20260205211412597
image-20260205211412597

5.2 你什么时候还能继续用 user1

看是否“被 move 的字段”还有没有剩下:

  • 如果 user2 使用了 ..user1,并且 move 走了 username: String 那么 user1.username 就没了 → user1 很可能整体不可再用
  • 但如果某些字段你没有从 user1 move(比如你给 user2 提供了新的 String),那么 user1 的那部分仍有效

原文强调的一个点是:

  • boolu64 这类实现了 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.0black.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)联动的经验法则

  • StringVec<T> 这类字段经常触发 move 使用 ..user1 时要特别注意:旧实例还能不能用,取决于哪些字段被 move。

  • 如果你希望“复制而不是移动”,考虑:

    • 字段类型是否实现 Copy

    • 或者显式 clone(克隆 clone)

      clone(clone):创建深拷贝,代价更高,但保留原值