Struct 结构体
struct 是 Rust 中最重要的自定义数据类型,用于将相关联的数据组织在一起。它类似于面向对象语言中的"类",struct 的实例类似于"对象"——都以键值对的方式存储数据,并可以附加方法。
Rust 中有三种结构体: 经典结构体、元组结构体、类单元结构体。
定义 Struct
Rust 社区推荐使用以下命名规范:
- 结构体名使用帕斯卡命名法(
PascalCase) - 字段名使用蛇形命名法(
snake_case)
经典结构体(Named Struct)
字段有名称的结构体,最常用的形式。
struct User {
name: String,
age: u32,
email: String,
}- 每个字段必须显式指定类型
- 结构体定义本身不占用内存,只有实例化后才占用
元组结构体(Tuple Struct)
字段没有名称、只有类型的结构体,通过索引访问字段。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);类单元结构体(Unit-like Struct)
没有任何字段的空结构体,通常用于为某个类型实现 Trait 而不需要存储数据的场景。
struct AlwaysEqual;实例化 Struct
经典结构体实例化
经典结构体实例化时需要为每个字段指定值,字段顺序不重要。
struct User {
name: String,
age: u32,
email: String,
}
let san = User {
name: String::from("zhangsan"),
age: 18,
email: String::from("123@gmail.com"),
};元组结构体实例化
元组结构体实例化时直接传入字段值,字段顺序必须与定义一致。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
black和origin是不同的类型,尽管内部字段完全相同。元组结构体为相同结构赋予了不同的语义,适合表达"颜色"和"坐标"这类有明确含义但结构相同的数据。
类单元结构体实例化
类单元结构体实例化时直接使用类型名即可,无需括号或字段值。
struct AlwaysEqual;
let subject = AlwaysEqual; // 直接使用类型名实例化字段初始化简写
当变量名与字段名相同时,可以省略字段名:
let name = String::from("zhangsan");
let email = String::from("123@gmail.com");
let san = User {
name, // 等价于 name: name
email, // 等价于 email: email
age: 18,
};访问与修改
使用点语法访问字段,修改字段需要整个实例声明为 mut:
// 访问经典结构体字段
println!("{}", san.name);
// 访问元组结构体字段(索引)
println!("{}", black.0);
// 修改字段(实例必须是 mut)
let mut san = User {
name: String::from("zhangsan"),
age: 18,
email: String::from("123@gmail.com"),
};
san.name = "lisi".to_string();
san.age = 20;Rust 不支持只将某个字段标记为可变,整个实例必须声明为
mut才能修改任何字段。
更新语法
基于现有实例创建新实例时,可以用 .. 语法复用未手动指定的字段,..base 必须放在末尾:
let new_san = User {
email: String::from("new@gmail.com"),
..san // 从 san 复用 name 和 age
};所有权注意事项: .. 语法本质上是字段级别的赋值,遵循所有权规则:
- 实现了
Copy的字段(如u32)会自动复制,原实例中的该字段仍然有效 - 未实现
Copy的字段(如String)会发生 Move,原实例中的该字段将失效
let user2 = User {
name: String::from("lisi"),
..user1 // user1.age (u32, Copy) 仍可用,user1.email (String) 已被 Move
};
println!("{}", user1.age); // ✅ age 是 Copy 类型,仍然有效
// println!("{}", user1.email); // ❌ email 已被 Move
// println!("{:?}", user1); // ❌ user1 处于"不完整"状态,不能整体使用若希望保留原实例完整可用,可以先 clone():
let user2 = User {
name: String::from("lisi"),
..user1.clone() // 深拷贝 user1,user1 的所有权完全不受影响
};部分移动(Partial Move): 结构体中的非
Copy字段被 Move 后,整个实例进入"不完整"状态。此时无法整体使用该实例(整体赋值、整体传参等),但仍然可以单独访问未被移走的字段。若后续对已移走的字段重新赋值,该字段重新归该实例所有,实例恢复完整状态。
调试输出
直接用 {} 格式化输出结构体会编译报错。需要在结构体定义前加上 #[derive(Debug)] 派生宏,然后使用 {:?}(单行)或 {:#?}(多行缩进)格式:
#[derive(Debug)]
struct User {
name: String,
age: u32,
}
let san = User { name: String::from("zhangsan"), age: 18 };
println!("{:?}", san); // User { name: "zhangsan", age: 18 }
println!("{:#?}", san); // 多行格式化输出多行输出结果:
User {
name: "zhangsan",
age: 18,
}也可以使用 dbg! 宏,它会额外输出文件名和行号,并返回值本身,适合调试中间值:
dbg!(&san); // 打印引用,san 所有权不变
let age = dbg!(san.age); // 打印 san.age 并将值绑定给 ageimpl Struct
使用 impl 关键字为结构体定义方法和关联函数,一个结构体可以拥有多个 impl 块:
impl Struct {
fn xxx(&self) → 方法(method)
fn xxx() → 关联函数(associated function)
}方法
方法的第一个参数是 self 的某种形式,代表调用该方法的实例:
| 形式 | 含义 |
|---|---|
self | 获取实例所有权,调用后实例失效 |
&self | 不可变借用,只读,调用后实例仍可用 |
&mut self | 可变借用,可修改字段,调用后实例仍可用 |
&self是self: &Self的语法糖。Self(大写)表示当前结构体类型本身,self(小写)表示调用方法的具体实例。
impl User {
// &self: 不可变借用,只读
fn say(&self) {
println!("我叫 {},今年 {} 岁", self.name, self.age);
}
// &mut self: 可变借用,可修改字段
fn update_name(&mut self, new_name: String) {
self.name = new_name;
}
// self: 获取所有权,调用后实例失效
fn consume(self) {
println!("{} 已注销", self.name);
}
}
let mut san = User {
name: "zhangsan".to_string(),
age: 18,
email: "123@gmail.com".to_string(),
};
san.say();
san.update_name("lisi".to_string());
san.consume(); // 调用后 san 不能再使用方法本质上也是关联函数,可以用 Type::method(&instance) 的形式显式调用,效果完全等价:
san.say(); // 常用写法
User::say(&san); // 等价写法,手动传递 &self 参数关联函数
没有 self 参数的函数称为关联函数,属于类型本身而非某个实例,类似于面向对象中的静态方法,通过 :: 调用:
impl User {
fn new(name: String, age: u32, email: String) -> Self {
Self { name, age, email }
}
}
let san = User::new("zhangsan".to_string(), 18, "123@gmail.com".to_string());
String::from()就是String类型的一个关联函数。关联函数常用于定义构造函数,约定俗成命名为new。
自动引用与解引用
Rust 中调用方法时(. 运算符),编译器会自动添加 &、&mut 或 * 来匹配方法签名,无需手动处理:
p.distance(&q); // Rust 自动转换为 (&p).distance(&q)
box_val.method(); // 自动解引用 Box<T>,等价于 (*box_val).method()这是 Rust 与 C/C++ 的重要区别: C++ 需要区分 .(对象调用)和 ->(指针调用),而 Rust 统一用 .,编译器自动完成引用与解引用的推导。
除方法调用外,以下场景也会触发自动解引用:
- 字段访问:
ref_val.name自动解引用到目标类型 - 比较操作符:
a > b若两侧都是引用,自动解引用后再比较
综合例子
#[derive(Debug)]
struct User {
name: String,
age: u8,
email: String,
}
impl User {
// 关联函数: 构造器
fn new(name: String, age: u8, email: String) -> Self {
Self { name, age, email }
}
// 方法: 不可变借用
fn say(&self) {
println!("我叫 {},今年 {} 岁", self.name, self.age);
}
// 方法: 可变借用
fn update_email(&mut self, new_email: String) {
self.email = new_email;
}
}
fn main() {
let mut san = User {
name: String::from("zhangsan"),
age: 18,
email: "old@gmail.com".to_string(),
};
// 更新语法: san.name (String) 被 Move 到 si,san.age (u8/Copy) 仍可访问
let mut si = User {
email: "si@gmail.com".to_string(),
..san
};
println!("{}", san.age); // ✅ age 是 Copy 类型,san 的该字段仍然有效
// 重新赋值 san.name,补全不完整状态
san.name = "zhangsan_new".to_string();
san.update_email("new@gmail.com".to_string());
san.say();
si.say();
// 使用关联函数构造新实例
let wu = User::new("wu".to_string(), 25, "wu@gmail.com".to_string());
println!("{:#?}", wu);
}