Skip to content

Rust 结构体 struct

结构体用来把多个不同类型的数据打包成一个自定义复合类型,类似其它语言的类的属性部分(Rust 没有类,用结构体+impl实现方法)。

Rust 有三种结构体:

  1. 命名字段结构体(最常用):每个字段有名字
  2. 元组结构体:字段没有名字,只有顺序
  3. 单元结构体:没有任何字段,用于实现trait

1. 命名字段结构体

rust
// 定义结构体
#[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

结构体更新语法(简写)

基于一个旧结构体,只修改部分字段快速生成新实例:

rust
let u3 = User {
    age:30,
    ..u1 // 剩下字段全部复用u1的值,必须放最后
};

注意:..u1会把u1里面的String所有权转移,之后u1就不能再使用。 完整代码

rust
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. 元组结构体

字段没有名字,只有类型,用索引访问,适合简单小结构。

rust
#[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 = 45

3. 单元结构体

没有字段,占0字节,适合只需要类型、不需要存数据,用来实现trait。

rust
#[derive(Debug)]
struct Unit;

fn main(){
    let u = Unit;
}

给结构体添加方法 impl

方法写在impl块中,第一个参数固定为&self/&mut self/self,代表结构体实例本身。

rust
#[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引用,否则需要生命周期标注:

rust
// 带生命周期,结构体持有引用
struct Person<'a> {
    name: &'a str,
}

日常开发优先用String,避免生命周期麻烦。

Copy约束

只有全部字段都是Copy类型(i32、bool等基础类型),结构体才能派生Copy。 包含String的结构体不能Copy,String没有Copy。

Rust结构体常见坑

  1. 不能单独某个字段mut,必须整个实例mut
  2. ..旧实例会移动所有权,旧实例内的非Copy字段失效
  3. 存引用必须写生命周期,尽量优先String
  4. 方法第一个参数必须是self系列,不要省略

struct 与 enum 对比 + 结构体所有权实操示例

struct(结构体)

用于把一组有关系的字段打包成一个整体,描述「一个事物拥有哪些属性」。

rust
#[derive(Debug)]
struct User {
    id: u64,
    name: String,
}

核心:一个实例同时拥有全部字段

enum(枚举)

用于多选一,一个实例只能是其中某一个变体,适合状态、分类。

rust
#[derive(Debug)]
enum Status {
    Online,
    Offline,
    Error(String),
}

简单对比表

structenum
含义聚合多个属性多种互斥的选项,二选一/多选一
实例内容同时拥有所有字段只能是其中一个变体
使用场景描述实体(用户、订单、点、颜色)状态、类型分支、错误、可选值
变体携带数据每个实例固定字段每个变体可以有不同字段
rust
fn main() {
    let u = User{id:1, name:"alice".into()};
    println!("{:#?}", u);

    let s = Status::Error("网络超时".to_string());
    println!("{:#?}", s);
}

结构体所有权移动、借用完整示例

重点看:移动、不可变借用、可变借用、..结构体更新语法带来move。

rust
#[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仍然可用
}

关键点总结

  1. &self:只读借用,原结构体继续存活。
  2. &mut self:可变借用,同一时间只能有一个可变借用。
  3. self(不带&):拿走所有权,原变量失效。
  4. ..old_struct
    • 字段是String等非Copy:所有权移动,old_struct失效
    • 字段全是基础Copy类型:只是复制,旧实例继续可用

结构体生命周期小例子(存引用)

尽量优先使用String;只有迫不得已才用引用+生命周期。

rust
#[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,否则无限大小)
  • 只想转移指针,不需要拷贝整个结构体
rust
#[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,编译器需要知道确定大小

rust
#[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

rust
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提供内部可变性,运行时检查借用规则。

rust
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>,原子引用计数,线程安全。

rust
use std::sync::Arc;
let a = Arc::new(User{name:"xxx".into(),age:18});

如果多线程还要修改,搭配Mutex

rust
use std::sync::{Arc, Mutex};

let shared = Arc::new(Mutex::new(User {
    name: "test".into(),
    age: 10,
}));

容易踩坑点总结

  1. Box只是把数据放到堆,仍然唯一所有权,赋值会move。
  2. Rc::clone()不是深拷贝结构体,只是增加引用计数,复制指针。
  3. Rc不能跨线程,跨线程必须Arc。
  4. Rc+RefCell会把借用检查从编译期推到运行时,写错会panic。
  5. 递归结构体必须Box,否则编译器无法计算结构体大小。

结构体 + Trait

Trait 就是行为接口,定义一组方法签名,然后给结构体(enum、基础类型)实现这个 trait。

类比其他语言:trait ≈ interface,Rust没有继承,靠trait做抽象。

1. 定义Trait

rust
// 定义一个trait,描述“可以打印信息”的行为
trait Info {
    // 方法签名,没有实现;分号结尾
    fn print_info(&self);

    // trait可以提供默认实现
    fn desc(&self) -> String {
        String::from("这是一个实体")
    }
}

2. 给结构体实现Trait

rust
#[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的类型

rust
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)里。

rust
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约束(泛型)

rust
// 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也可以给多个结构体实现。

rust
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。

rust
#[derive(Debug)]
struct S;
// 等价于编译器自动生成 impl Debug for S { ... }

7. trait的约束组合

+ 多个trait同时要求

rust
// 参数必须同时实现 Info + Clone
fn test(item: impl Info + Clone) {}

8. 重要规则(孤儿规则)

Orphan rule: 只能在下面两种情况实现trait:

  1. trait 在你自己写的crate
  2. 要实现的结构体/类型 在你自己写的crate

❌ 不能:给标准库的结构体,实现标准库的trait。 比如:impl std::fmt::Display for Vec<i32>,编译报错。 目的:防止不同库的实现冲突。

综合完整示例:结构体 + Box<dyn Trait>

rust
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 TraitBox<dyn Trait>
分发静态分发,编译期单态化动态分发,运行虚表
性能高,无开销微小运行开销
异构集合不能,类型必须固定可以,Vec存不同类型
语法fn f(x:impl T)Box<dyn T>

结构体 + Trait常见坑

  1. dyn trait对象必须放在指针后面,不能直接 dyn Info 作为变量;必须Box<dyn Info>/Rc<dyn Info>
  2. 孤儿规则:不能外来trait +外来类型。
  3. trait默认方法可以被重写。
  4. 泛型impl Trait只是语法糖,等价泛型T:Trait。