Rust 结构体 struct
结构体用来把多个不同类型的数据打包成一个自定义复合类型,类似其它语言的类的属性部分(Rust 没有类,用结构体+impl实现方法)。
Rust 有三种结构体:
- 命名字段结构体(最常用):每个字段有名字
- 元组结构体:字段没有名字,只有顺序
- 单元结构体:没有任何字段,用于实现trait
1. 命名字段结构体
// 定义结构体
#[derive(Debug)] // 开启debug打印
struct User {
username: String,
age: u32,
is_active: bool,
}
fn main() {
// 实例化
let u1 = User {
username: String::from("zhangsan"),
age: 22,
is_active: true,
};
// 访问字段 .
println!(
"name:{} age:{} is_active:{}",
u1.username, u1.age, u1.is_active
);
// 可变实例,整体可变,不能只单独某个字段可变
let mut u2 = User {
username: String::from("lisi"),
age: 18,
is_active: false,
};
u2.age = 19;
println!("{:#?}", u2);
}结果:
name:zhangsan age:22 is_active:true
User {
username: "lisi",
age: 19,
is_active: false,
}⚠️ 注意:Rust不能只让结构体某个字段可变,必须整个实例标记
mut。
结构体更新语法(简写)
基于一个旧结构体,只修改部分字段快速生成新实例:
let u3 = User {
age:30,
..u1 // 剩下字段全部复用u1的值,必须放最后
};注意:
..u1会把u1里面的String所有权转移,之后u1就不能再使用。 完整代码
use std::u32;
// 定义结构体
#[derive(Debug)] // 开启debug打印
struct User {
username: String,
age: u32,
is_active: bool,
}
fn main() {
// 实例化
let u1 = User {
username: String::from("zhangsan"),
age: 22,
is_active: true,
};
// 访问字段 .
println!(
"name:{} age:{} is_active:{}",
u1.username, u1.age, u1.is_active
);
// 可变实例,整体可变,不能只单独某个字段可变
let mut u2 = User {
username: String::from("lisi"),
age: 18,
is_active: false,
};
u2.age = 19;
println!("{:#?}", u2);
// 复用
let u3 = User {
age: 30,
..u1 // 剩下的字段全部利用u1的值,必须放最后
};
println!("{:#?}", u3);
}结果
name:zhangsan age:22 is_active:true
User {
username: "lisi",
age: 19,
is_active: false,
}
User {
username: "zhangsan",
age: 30,
is_active: true,
}2. 元组结构体
字段没有名字,只有类型,用索引访问,适合简单小结构。
#[derive(Debug)]
struct Color(u8, u8, u8);
struct Point(f64, f64);
fn main() {
let rgb = Color(255, 0, 0);
println!("r = {}, g = {}, b = {}", rgb.0, rgb.1, rgb.2);
let point = Point(45.0, 45.0);
println!("x = {}, y = {}", point.0, point.1);
}运行结果:
r = 255, g = 0, b = 0
x = 45, y = 453. 单元结构体
没有字段,占0字节,适合只需要类型、不需要存数据,用来实现trait。
#[derive(Debug)]
struct Unit;
fn main(){
let u = Unit;
}给结构体添加方法 impl
方法写在impl块中,第一个参数固定为&self/&mut self/self,代表结构体实例本身。
#[derive(Debug)]
struct Book {
title: String,
price: f64,
}
impl Book {
// 实例方法,借用self,只读
fn info(&self) {
println!("《{}》 价格:{}", self.title, self.price);
}
// 可变方法,修改结构体字段
fn raise_price(&mut self, add:f64) {
self.price += add;
}
// 关联函数(类似静态方法),不带self,用::调用,常用来做构造器
fn new(title:&str, price:f64) -> Self {
Self {
title: title.to_string(),
price
}
}
}
fn main(){
let mut b = Book::new("Rust编程",59.0);
b.info();
b.raise_price(10.0);
b.info();
}运行结果:
《Rust编程》价格:59
《Rust编程》价格:69&self:不可变借用,只读,最常用&mut self:可变借用,可以修改结构体成员self:获取所有权,调用之后原实例被move,不再可用Self:代表当前impl对应的结构体类型,等价Book
结构体所有权
结构体里面如果放String这种拥有所有权类型,结构体拥有它的所有权; 不要放&str引用,否则需要生命周期标注:
// 带生命周期,结构体持有引用
struct Person<'a> {
name: &'a str,
}日常开发优先用
String,避免生命周期麻烦。
Copy约束
只有全部字段都是Copy类型(i32、bool等基础类型),结构体才能派生Copy。 包含String的结构体不能Copy,String没有Copy。
Rust结构体常见坑
- 不能单独某个字段mut,必须整个实例mut
..旧实例会移动所有权,旧实例内的非Copy字段失效- 存引用必须写生命周期,尽量优先String
- 方法第一个参数必须是
self系列,不要省略
struct 与 enum 对比 + 结构体所有权实操示例
struct(结构体)
用于把一组有关系的字段打包成一个整体,描述「一个事物拥有哪些属性」。
#[derive(Debug)]
struct User {
id: u64,
name: String,
}核心:一个实例同时拥有全部字段。
enum(枚举)
用于多选一,一个实例只能是其中某一个变体,适合状态、分类。
#[derive(Debug)]
enum Status {
Online,
Offline,
Error(String),
}简单对比表
| struct | enum | |
|---|---|---|
| 含义 | 聚合多个属性 | 多种互斥的选项,二选一/多选一 |
| 实例内容 | 同时拥有所有字段 | 只能是其中一个变体 |
| 使用场景 | 描述实体(用户、订单、点、颜色) | 状态、类型分支、错误、可选值 |
| 变体携带数据 | 每个实例固定字段 | 每个变体可以有不同字段 |
fn main() {
let u = User{id:1, name:"alice".into()};
println!("{:#?}", u);
let s = Status::Error("网络超时".to_string());
println!("{:#?}", s);
}结构体所有权移动、借用完整示例
重点看:移动、不可变借用、可变借用、..结构体更新语法带来move。
#[derive(Debug)]
struct Car {
brand: String,
price: u32,
}
impl Car {
// 只读方法:&self,借用,不拿走所有权
fn show(&self) {
println!("{} 价格:{}", self.brand, self.price);
}
// 修改方法:&mut self,可变借用
fn set_price(&mut self, p: u32) {
self.price = p;
}
// 获取所有权 self,调用后原变量失效
fn into_brand(self) -> String {
self.brand
}
}
fn main() {
let c1 = Car {
brand: String::from("BMW"),
price: 300000,
};
c1.show(); // &self,只是借用,c1还能用
let mut c2 = Car {
brand: String::from("Audi"),
price: 250000,
};
c2.set_price(280000);
c2.show();
// 1. self 获取所有权,c2被move,后续不能再使用c2
let b = c2.into_brand();
println!("提取品牌:{}", b);
// c2.show(); // ❌编译报错,c2已经被移走
// 2. 结构体更新语法 .. 会移动非Copy字段所有权
let c3 = Car {
brand: String::from("Tesla"),
price: 400000,
};
// c3.brand是String,不是Copy,所有权转移到c4,c3失效
let c4 = Car {
price: 420000,
..c3
};
c4.show();
// println!("{:?}",c3); // ❌报错,c3的brand被move走了
// 如果全部字段都是Copy类型(u32,i32,bool),..不会move,只是复制
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
let p1 = Point{x:10,y:20};
let p2 = Point{x:100,..p1};
println!("p1:{:?},p2:{:?}",p1,p2); // ✅p1仍然可用
}关键点总结
&self:只读借用,原结构体继续存活。&mut self:可变借用,同一时间只能有一个可变借用。self(不带&):拿走所有权,原变量失效。..old_struct:- 字段是
String等非Copy:所有权移动,old_struct失效 - 字段全是基础Copy类型:只是复制,旧实例继续可用
- 字段是
结构体生命周期小例子(存引用)
尽量优先使用
String;只有迫不得已才用引用+生命周期。
#[derive(Debug)]
struct Msg<'a> {
text: &'a str,
}
fn main() {
let s = String::from("hello");
let m = Msg{text:&s};
println!("{}",m.text);
}'a约束:Msg实例不能比它引用的s活得更久。
Struct + Box / Rc
结构体默认存栈上;Box<T>把结构体搬到堆;Rc<T>实现多所有权共享同一个结构体实例。
Box<T>:把结构体放到堆
结构体本身变量在栈,里面存一个指向堆的指针,真正数据放在堆内存。 适用场景:
- 结构体很大,不希望占栈空间
- 递归结构体(链表、树,必须Box,否则无限大小)
- 只想转移指针,不需要拷贝整个结构体
#[derive(Debug)]
struct User {
name: String,
age: u32,
}
fn main() {
// 栈上
let u_stack = User {
name: "alice".into(),
age: 20,
};
// Box,数据放到堆,变量box_u栈上存指针
let box_u: Box<User> = Box::new(User {
name: "bob".into(),
age: 22,
});
// Box可以直接用.访问字段,自动解引用
println!("{} {}", box_u.name, box_u.age);
// Box移动:只是移动栈上指针,堆上数据不复制
let box_u2 = box_u;
// println!("{:?}", box_u); // ❌ box_u所有权被move
println!("{:?}", box_u2);
}递归结构体必须用Box,编译器需要知道确定大小
#[derive(Debug)]
// 链表节点,Box给递归类型一个固定大小
struct Node {
val: i32,
next: Option<Box<Node>>,
}
fn main() {
let n3 = Node { val:3, next: None };
let n2 = Node { val:2, next: Some(Box::new(n3)) };
let n1 = Node { val:1, next: Some(Box::new(n2)) };
println!("{:#?}", n1);
}Rc<T>:多所有权共享结构体
Rc = Reference Count,引用计数,单线程环境,允许多个变量共享同一个堆上结构体。 每clone一次,计数+1;变量销毁计数-1;计数到0才释放堆内存。
⚠️ Rc只读,不能修改内部数据;想要可修改需要搭配
RefCell。
use std::rc::Rc;
#[derive(Debug)]
struct Book {
title: String,
}
fn main() {
let b1: Rc<Book> = Rc::new(Book {
title: "Rust圣经".into(),
});
println!("引用计数:{}", Rc::strong_count(&b1)); // 1
// clone不是复制Book数据,只是增加引用计数,复制指针
let b2 = Rc::clone(&b1);
println!("引用计数:{}", Rc::strong_count(&b1)); //2
let b3 = Rc::clone(&b1);
println!("引用计数:{}", Rc::strong_count(&b1)); //3
println!("{} {} {}", b1.title, b2.title, b3.title);
// b1,b2,b3都指向堆上同一个Book实例
drop(b1);
println!("drop b1后计数:{}", Rc::strong_count(&b2)); //2
drop(b2);
println!("drop b2后计数:{}", Rc::strong_count(&b3)); //1
drop(b3);
// 计数归零,堆上Book被释放
}Rc + RefCell:共享且可修改
Rc只提供共享只读;RefCell提供内部可变性,运行时检查借用规则。
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Student {
score: u32,
}
fn main() {
let s = Rc::new(RefCell::new(Student { score: 60 }));
let s1 = Rc::clone(&s);
let s2 = Rc::clone(&s);
// 修改
s1.borrow_mut().score = 90;
// 读取
println!("{}", s2.borrow().score); //90
}注意:
borrow_mut()运行时检查,如果同时存在多个可变借用会panic。
Box / Rc 简单对比
Box<T> | Rc<T> | |
|---|---|---|
| 所有权 | 唯一所有权 | 多所有权,引用计数共享 |
| 线程安全 | ✅可跨线程 | ❌只能单线程,多线程要用Arc |
| 可变性 | Box<Struct>,&mut直接改 | Rc本身只读;要修改套RefCell |
| 开销 | 几乎无开销 | clone有计数增减开销 |
3. Arc 简单提一嘴
多线程共享结构体用 Arc<T>,原子引用计数,线程安全。
use std::sync::Arc;
let a = Arc::new(User{name:"xxx".into(),age:18});如果多线程还要修改,搭配Mutex。
use std::sync::{Arc, Mutex};
let shared = Arc::new(Mutex::new(User {
name: "test".into(),
age: 10,
}));容易踩坑点总结
- Box只是把数据放到堆,仍然唯一所有权,赋值会move。
- Rc::clone()不是深拷贝结构体,只是增加引用计数,复制指针。
- Rc不能跨线程,跨线程必须Arc。
- Rc+RefCell会把借用检查从编译期推到运行时,写错会panic。
- 递归结构体必须Box,否则编译器无法计算结构体大小。
结构体 + Trait
Trait 就是行为接口,定义一组方法签名,然后给结构体(enum、基础类型)实现这个 trait。
类比其他语言:trait ≈ interface,Rust没有继承,靠trait做抽象。
1. 定义Trait
// 定义一个trait,描述“可以打印信息”的行为
trait Info {
// 方法签名,没有实现;分号结尾
fn print_info(&self);
// trait可以提供默认实现
fn desc(&self) -> String {
String::from("这是一个实体")
}
}2. 给结构体实现Trait
#[derive(Debug)]
struct Book {
title: String,
price: f64,
}
// 为Book实现Info trait
impl Info for Book {
fn print_info(&self) {
println!("书籍:《{}》,价格:{}", self.title, self.price);
}
// 不重写desc,就会使用trait里的默认实现
}
#[derive(Debug)]
struct Phone {
brand: String,
}
impl Info for Phone {
fn print_info(&self) {
println!("手机品牌:{}", self.brand);
}
// 重写默认方法
fn desc(&self) -> String {
format!("手机设备:{}", self.brand)
}
}
fn main() {
let b = Book {
title: "Rust编程指南".into(),
price: 68.0,
};
b.print_info();
println!("{}", b.desc());
let p = Phone { brand: "华为".into() };
p.print_info();
println!("{}", p.desc());
}3. trait作为函数参数(动态分发 & 静态分发)
方式1:impl Trait 静态分发(推荐,编译单态化,性能好)
函数接收任意实现了Info trait的类型
fn show(item: impl Info) {
item.print_info();
}
fn main() {
let book = Book{title:"rust".into(),price:50.0};
let phone = Phone{brand:"小米".into()};
show(book);
show(phone);
}方式2:trait对象 Box<dyn Trait> 动态分发
需要在运行时确定具体类型,适合集合存多种不同结构体。
dyn 代表动态trait对象,必须包在指针(Box/Rc/Arc)里。
fn main() {
// Vec里面可以放Book、Phone,只要实现Info
let list: Vec<Box<dyn Info>> = vec![
Box::new(Book{title:"a".into(),price:10.0}),
Box::new(Phone{brand:"苹果".into()}),
];
for item in list {
item.print_info();
}
}impl Info:静态分发,编译生成多份函数代码,无运行开销,不能存到同一个Vec(类型不一样)Box<dyn Info>:动态分发,运行查表调用方法,可以异构集合,有极小运行开销
4. 在impl块使用trait约束(泛型)
// T必须实现Info trait
fn show_generic<T: Info>(item: T) {
item.print_info();
}
// where语法,约束多的时候可读性更好
fn show_where<T>(item:T)
where
T: Info,
{
item.print_info();
}5. 给结构体实现多个trait
一个结构体可以实现很多trait;一个trait也可以给多个结构体实现。
trait DisplayExt {
fn brief(&self);
}
impl Info for Book {
fn print_info(&self) { /* */ }
}
impl DisplayExt for Book {
fn brief(&self) {
println!("brief:{}", self.title);
}
}6. derive派生宏本质
#[derive(Debug)] 等价于自动帮你实现了std::fmt::Debug这个trait。
#[derive(Debug)]
struct S;
// 等价于编译器自动生成 impl Debug for S { ... }7. trait的约束组合
+ 多个trait同时要求
// 参数必须同时实现 Info + Clone
fn test(item: impl Info + Clone) {}8. 重要规则(孤儿规则)
Orphan rule: 只能在下面两种情况实现trait:
- trait 在你自己写的crate
- 要实现的结构体/类型 在你自己写的crate
❌ 不能:给标准库的结构体,实现标准库的trait。 比如:impl std::fmt::Display for Vec<i32>,编译报错。 目的:防止不同库的实现冲突。
综合完整示例:结构体 + Box<dyn Trait>
trait Animal {
fn speak(&self);
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn speak(&self) {
println!("汪汪");
}
}
impl Animal for Cat {
fn speak(&self) {
println!("喵喵");
}
}
fn main() {
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
];
for a in animals {
a.speak();
}
}impl Trait vs Box<dyn Trait>总结
| impl Trait | Box<dyn Trait> | |
|---|---|---|
| 分发 | 静态分发,编译期单态化 | 动态分发,运行虚表 |
| 性能 | 高,无开销 | 微小运行开销 |
| 异构集合 | 不能,类型必须固定 | 可以,Vec存不同类型 |
| 语法 | fn f(x:impl T) | Box<dyn T> |
结构体 + Trait常见坑
- dyn trait对象必须放在指针后面,不能直接
dyn Info作为变量;必须Box<dyn Info>/Rc<dyn Info>。 - 孤儿规则:不能外来trait +外来类型。
- trait默认方法可以被重写。
- 泛型
impl Trait只是语法糖,等价泛型T:Trait。