Rust语言进阶(结构体)

目录

序言

前面有写过Rust语言入门的内容:Rust语言入门

本篇接着前面的内容进行进阶学习。其实就是通过官方文档(doc_rust-lang.or)去学习归纳总结。

结构体(Struct)

在 Rust 中,结构体(struct)是一种自定义数据类型,用于封装多个相关字段。结构体通过 struct 关键字定义,字段需明确指定名称和类型。比如下面这个demo,

rust 复制代码
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

上面示例结果如下,

其中struct定义了一个Rectangle的结构体,内部包含名称和类型的字段属性,

rust 复制代码
struct Rectangle {
    width: u32,
    height: u32,
}

结构体的定义

命名规范

结构体名必须用 PascalCase(首字母大写),字段名用 snake_case(小写 + 下划线),符合 Rust 社区惯例:

rust 复制代码
// 正确:结构体 PascalCase,字段 snake_case
struct UserProfile {
    user_name: String,
    age: u8,
}

// 错误:结构体首字母小写(编译不报错,但不符合规范)
struct user_profile { /* ... */ }

语法格式

  • 普通结构体:struct 名称 { 字段1: 类型1, 字段2: 类型2, ... }(字段需显式指定类型)。
  • 元组结构体:struct 名称(类型1, 类型2, ...)(无字段名,仅存类型)。
  • 单元结构体:struct 名称;(无字段,仅作标记)。

可见性控制

  • 结构体默认 私有(private):仅当前模块可见,跨模块访问需加 pub
  • 字段默认也私有:即使结构体 pub,字段仍需单独加 pub 才能被外部访问:
rust 复制代码
// 模块内定义结构体
mod my_module {
    // 公开结构体(外部可访问)
    pub struct Person {
        pub name: String, // 公开字段(外部可读写)
        age: u8,          // 私有字段(仅模块内访问)
    }

    // 模块内可访问私有字段
    pub fn new_person(name: String, age: u8) -> Person {
        Person { name, age }
    }
}

fn main() {
    let p = my_module::new_person("Alice".to_string(), 22);
    println!("Name: {}", p.name); // 合法:name 是公开字段
    // println!("Age: {}", p.age); // 错误:age 是私有字段,外部不可访问
}

结构体的实例化

实例化规则------字段必须完全初始化

实例化时需为所有字段赋值(无默认值的情况下),不能遗漏:

rust 复制代码
struct Point { x: i32, y: i32 }

// 正确:所有字段都赋值
let p1 = Point { x: 10, y: 20 };

// 错误:遗漏 y 字段
// let p2 = Point { x: 30 };

实例化规则------可变性

  • 结构体实例默认不可变,需加 mut 关键字才能修改字段(整体可变,不能单独标记某个字段为 mut)。
  • 若需 "部分字段可变",可将可变字段封装为内部结构体,或使用 Cell/RefCell(内部可变性)
rust 复制代码
// 内部可变性(部分字段可变)
use std::cell::Cell;
struct Counter {
    count: Cell<u32>, // 可变性独立于结构体实例
    name: String,     // 不可变字段
}

struct Point { x: i32, y: i32 }

fn main() {
    // 整体可变
	let mut p = Point { x: 10, y: 20 };
	p.x = 30; // 合法:p 是 mut
	println!("Point: x={}, y={}", p.x, p.y); 

    let c = Counter { count: Cell::new(0), name: "test".to_string() };
	c.count.set(1); // 合法:count 是 Cell 类型,支持内部可变
	// c.name = "new".to_string(); // 错误:name 是普通不可变字段
	println!("Counter: count={}, name={}", c.count.get(), c.name); // 读取 count 和 name
}

实例化规则------所有权规则

字段若为非 Copy 类型(如 String、Vec),赋值时会转移所有权,原实例不可再用(除非 clone)

rust 复制代码
struct Person { name: String }

fn main() {
    let p1 = Person { name: "Bob".to_string() };
	let p2 = p1; // 所有权转移给 p2,p1 不可再用
	println!("{}", p1.name); // 错误:p1 已被移动
}

派生规则:#[derive] 的使用限制

  • 只有 支持派生的 trait 才能用 #[derive](如 Debug、PartialEq),自定义 trait 需通过过程宏(Proc Macro)实现派生。

  • 派生 trait 时,所有字段必须也实现该 trait(递归规则),如下面那个示例User里的Address字段也必须派生 Debug。

rust 复制代码
#[derive(Debug)] // 正确:Point 字段 x/y 都是 i32,已实现 Debug
struct Point { x: i32, y: i32 }

#[derive(Debug)] // 正确:User 的字段 name(String)和 addr(Address)都实现 Debug
struct User { name: String, addr: Address }

#[derive(Debug)] // 必须为 Address 也派生 Debug
struct Address { street: String }

前面接触过派生宏,那这里又引入了一个新概念------过程宏(Proc Macro),过程宏是什么?它是一种特殊的 Rust 宏,接收 Rust 代码的语法树(TokenStream)作为输入,处理后输出新的 TokenStream(即生成的代码),运行在编译期。下面给出一个创建过程宏的示例,整体目录结构如下,

json 复制代码
hello_macro_project/
├── Cargo.toml
├── hello_macro/          # 过程宏 crate
│   ├── Cargo.toml
│   └── src/lib.rs
├── hello_macro_trait/    # 普通库 crate(存放 trait)
│   ├── Cargo.toml
│   └── src/lib.rs
└── hello_macro_demo/     # 二进制测试 crate
    ├── Cargo.toml
    └── src/main.rs
  • 1.1 创建print_fields_macro项目,并进入指定目录,
bash 复制代码
# 创建hello_macro_project文件夹
mkdir hello_macro_project

# 创建工作区Cargo.toml
touch Cargo.toml
  • 1.2 编辑工作区的Cargo.toml文件,
toml 复制代码
[workspace]
members = [
    "hello_macro",
    "hello_macro_trait",
    "hello_macro_demo",
]
resolver = "2"
  • 1.3 创建普通库存放 trait
bash 复制代码
cargo new hello_macro_trait --lib
cd hello_macro_trait
  • 1.4 编辑hello_macro_trait/Cargo.toml
rust 复制代码
[package]
name = "hello_macro_trait"
version = "0.1.0"
edition = "2021"
  • 1.5 编辑hello_macro_trait/src/lib.rs
rust 复制代码
// 定义 trait(普通库可正常导出)
pub trait HelloMacro {
    fn hello_macro();
}
  • 1.6 创建过程宏库
bash 复制代码
cargo new hello_macro --lib
cd hello_macro
  • 1.7 修改hello_macro/Cargo.toml,添加过程宏配置和依赖,
rust 复制代码
[package]
name = "hello_macro"
version = "0.1.0"
edition = "2021"

[lib]
# 过程宏 crate 必须在 Cargo.toml 中声明 proc-macro = true,且只能依赖特定库(如 syn、quote、proc-macro2)
proc-macro = true

[dependencies]
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
hello_macro_trait = { path = "../hello_macro_trait" }  # 依赖 trait 库
  • 1.8 然后创建 / 编辑src/lib.rs,写入过程宏代码。
rust 复制代码
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = input.ident;

    // 生成 trait 实现(引用外部库的 trait)
    let expanded = quote! {
        impl hello_macro_trait::HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}!", stringify!(#name));
            }
        }
    };

    TokenStream::from(expanded)
}
  • 1.9 返回工作区根目录,创建测试demo,
bash 复制代码
cd ..
cargo new hello_macro_demo
cd hello_macro_demo
  • 1.10 修改hello_macro_demo/Cargo.toml,添加对过程宏库的依赖,
rust 复制代码
[package]
name = "hello_macro_demo"
version = "0.1.0"
edition = "2021"

[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_trait = { path = "../hello_macro_trait" }  # 依赖 trait 库
  • 1.11 然后编辑src/main.rs,写入测试代码
rust 复制代码
use hello_macro::HelloMacro;          // 引入派生宏
use hello_macro_trait::HelloMacro as _; // 引入 trait(或直接 use hello_macro_trait::HelloMacro;)

#[derive(HelloMacro)]
struct Person;

#[derive(HelloMacro)]
struct Book;

fn main() {
    Person::hello_macro(); // 输出:Hello, Macro! My name is Person!
    Book::hello_macro();   // 输出:Hello, Macro! My name is Book!
}
  • 1.12 运行demo,
bash 复制代码
cargo run -p hello_macro_demo

过程宏这块示例里有很多信息,需要慢慢消化,比如proc-macroproc-macro2两个库,

特性 proc-macro (Rust 内置) proc-macro2 (第三方库)
来源 Rust 标准库,直接通过 proc_macro 模块提供 第三方库,需通过 proc-macro2 crate 引入
稳定性 依赖编译器内部实现,可能不稳定 提供稳定封装,兼容性更好
TokenStream 类型 仅支持 proc_macro::TokenStream 提供 proc_macro2::TokenStream,可转换为标准类型
调试支持 原生调试困难 提供更好的调试工具(如 to_string() 方法)
跨版本兼容性 可能因编译器版本不同出现行为差异 屏蔽底层差异,保证跨版本行为一致
使用场景 直接用于声明式宏或过程宏的导出函数 推荐在宏实现逻辑中使用,再转换为标准类型输出
依赖关系 无需额外依赖 需添加 proc-macro2 = "1.x"Cargo.toml

可变结构体

若需修改结构体字段,实例必须声明为可变。如下所示,

rust 复制代码
let mut rect1 = Rectangle {
        width: 30,
        height: 50,
    };
rect1.width = 40;

元组结构体(Tuple Struct)

元组结构体省略字段名,仅保留字段类型。适用于轻量级封装,通过索引访问字段。

rust 复制代码
// 定义元组结构体:Point(2D坐标,x=索引0,y=索引1)
struct Point(i32, f64);
// 定义元组结构体:Color(RGB颜色,红=0,绿=1,蓝=2)
struct Color(u8, u8, u8);

fn main() {
    // 实例化:按类型顺序传值(必须匹配定义的类型)
    let p = Point(10, 3.14);
    let red = Color(255, 0, 0);

    // 通过索引访问字段(0 对应第一个字段,1 对应第二个,以此类推)
    println!("Point x: {}, y: {}", p.0, p.1); // 输出:Point x: 10, y: 3.14
    println!("Red color: R={}, G={}, B={}", red.0, red.1, red.2); // 输出:Red color: R=255, G=0, B=0
}

单元结构体(Unit Struct)

如果元组结构体没有字段(空括号),称为 "单元结构体",类似 ()(单元类型)。单元结构体(无字段)本身不存储数据,主要用于:

  • 标记类型(比如区分不同的 "状态")
  • 实现 trait 的载体(比如某些无状态的 trait 实现)
rust 复制代码
// 为单元结构体实现 Debug(用于打印)
#[derive(Debug)]
struct VisibleMarker;

fn main() {
    let m = VisibleMarker;
    println!("{:?}", m); // 输出:VisibleMarker
}
  • 为什么需要 Debug?
    {:?} 是Rust 提供的 "调试格式化符",专门用于开发时打印变量信息,依赖 std::fmt::Debug trait。所有基本类型(i32String 等)默认已实现 Debug,但自定义结构体(包括元组结构体、单元结构体)需要手动推导或实现。
  • 扩展:常用可派生的 trait
    除了 Debug,Rust 还支持通过 #[derive] 自动派生其他常用 trait,比如:
    • PartialEq/Eq:用于比较相等性(==/!=
    • Clone/Copy:用于复制值
    • Default:用于生成默认实例

比如下面同时派生出多个 trait

rust 复制代码
// 同时派生多个 trait
#[derive(Debug, PartialEq, Clone)]
struct Point(i32, f64);

fn main() {
    let p1 = Point(10, 3.14);
    let p2 = p1.clone(); // 因为派生了 Clone
    println!("p1: {:?}, p2: {:?}", p1, p2); // 因为派生了 Debug
    println!("p1 == p2: {}", p1 == p2); // 因为派生了 PartialEq(输出 true)
}

嵌套元组结构体

元组结构体的字段可以是任意类型,包括其他元组结构体:

rust 复制代码
// 定义元组结构体:Point(2D坐标,x=索引0,y=索引1)
struct Point(i32, i32);
// 嵌套元组结构体:Rect 由两个 Point 组成(左上角和右下角)
struct Rect(Point, Point);

fn main() {
    let top_left = Point(0, 0);
    let bottom_right = Point(100, 200);
    let rect = Rect(top_left, bottom_right);

    // 嵌套访问:先访问 Rect 的字段,再访问 Point 的字段
    println!("左上角 x: {}", rect.0.0); // rect.0 是 top_left,再 .0 是 x
    println!("右下角 y: {}", rect.1.1); // rect.1 是 bottom_right,再 .1 是 y
}

以下是Rust中各类结构体的主要区别:

类型 定义语法 可变性 字段命名 内存布局 典型用途
普通结构体 struct Name { field: Type } 默认不可变,需加mut声明可变 具名字段 字段顺序不固定 复杂数据建模,需清晰字段名
可变结构体 let mut s = Name { ... } 实例声明为mut后可修改字段 具名字段 同普通结构体 需要修改内部状态的场景
元组结构体 struct Name(Type1, Type2) 类似普通结构体 匿名字段(通过索引访问) 顺序内存布局 轻量级包装,语义化元组
单元结构体 struct Name; 无字段故不可变 无字段 零大小(ZST) 标记 trait 实现或空行为
嵌套元组结构体 struct Nested((Type1, Type2)) 取决于外层结构体 外层具名,内层匿名 嵌套顺序布局 组合已有元组结构
普通元组 (Type1, Type2) 元素独立可变性 匿名字段(通过索引访问) 顺序内存布局 临时数据组合,无需类型命名

泛型结构体

结构体可使用泛型参数,实现类型复用:

rust 复制代码
struct GenericStruct<T, U> {
    data1: T,
    data2: U,
}

fn main() {
    let s1 = GenericStruct { data1: 10, data2: "hello" };
	let s2 = GenericStruct { data1: 3.14, data2: true };
    println!("s1.data1: {}, s1.data2: {}", s1.data1, s1.data2); 
    println!("s2.data1: {}, s2.data2: {}", s2.data1, s2.data2); 
}

结构体更新语法(Struct Update Syntax)

..的作用是,基于已有结构体实例,快速创建新实例------ 新实例可复用原实例的大部分字段值,仅修改需要变更的字段,避免重复编写所有字段。

该语法同时支持 普通结构体(命名字段) 和 元组结构体(无名字段),但用法和限制略有不同,

普通结构体(命名字段)

下面是一个普通结构体的示例,

rust 复制代码
// 定义普通结构体(命名字段)
struct Person {
    name: String,
    age: u8,
    city: String,
    is_student: bool,
}

fn main() {
    // 1. 创建原始实例
    let alice = Person {
        name: "Alice".to_string(),
        age: 22,
        city: "Beijing".to_string(),
        is_student: true,
    };

    // 2. 用 .. 复用 alice 的大部分字段,仅修改 age 和 city
    let bob = Person {
        name: "Bob".to_string(), // 新值:覆盖原字段
        age: 25,                // 新值:覆盖原字段
        ..alice                 // 复用剩余所有字段(city、is_student)
    };

    println!("Bob: name={}, age={}, city={}, student={}",
        bob.name, bob.age, bob.city, bob.is_student);
    // 输出:Bob: name=Bob, age=25, city=Beijing, student=true
}

其结果是通过..直接复用alice对象的大部分字段。

..使用规则必须遵循下面几点:
规则1:.. 必须放在结构体初始化的最后

因为.. 表示 "剩余所有字段",顺序不能颠倒(编译会报错)。

rust 复制代码
	let bob = Person {
        ..alice,
        name: "Bob".to_string(), // 新值:覆盖原字段
        age: 25                // 新值:覆盖原字段
    };

比如按照上面这个顺序,编译会报错,

规则2:复用的字段会转移所有权(非 Copy 类型)

rust 复制代码
// 定义普通结构体(命名字段)
struct Person {
    name: String,
    age: u8,
    city: String,
    is_student: bool,
}

fn main() {
    // 1. 创建原始实例
    let alice = Person {
        name: "Alice".to_string(),
        age: 22,
        city: "Beijing".to_string(),
        is_student: true,
    };

    // 2. 用 .. 复用 alice 的大部分字段,仅修改 age 和 city
    let bob = Person {
        name: "Bob".to_string(), // 新值:覆盖原字段
        age: 25,                // 新值:覆盖原字段
        ..alice                 // 复用剩余所有字段(city、is_student)
    };

    println!("Bob: name={}, age={}, city={}, student={}",
        bob.name, bob.age, bob.city, bob.is_student);
    // 输出:Bob: name=Bob, age=25, city=Beijing, student=true
    // 错误:alice 的 city 字段已转移给 bob,alice 不再完整
	println!("Alice's city: {}", alice.city);
}

还是上面的例子,但是后续再去使用alice的city属性就会报错,

规则3:Copy 类型字段会复制,不转移所有权

如果字段是 u8、bool 等 Copy 类型,... 会直接复制值,原实例仍可正常使用。如下面的例子所示,

rust 复制代码
// 示例:字段均为 Copy 类型
#[derive(Debug, Copy, Clone)]
struct Point { x: i32, y: i32 }

fn main() {
    let p1 = Point { x: 10, y: 20 };
	let p2 = Point { x: 30, ..p1 }; // y 字段复制,p1 仍可用
	println!("p1: {:?}, p2: {:?}", p1, p2); // 正常输出
}

Rust中常见的derive宏可应用的trait分类如下表所示,

类别 Trait名称 功能描述
基本类型 Debug 提供{:?}格式化输出,用于调试打印
Clone 允许显式创建值的深拷贝
Copy 标记类型为按位复制语义(需先实现Clone
Default 提供类型的默认值
比较操作 PartialEq 支持部分相等比较(如浮点数NaN
Eq 标记完全等价关系(需先实现PartialEq
PartialOrd 支持部分排序(如浮点数)
Ord 支持全序排序(需先实现PartialOrd + Eq
哈希与序列化 Hash 支持哈希值计算
Serialize 支持序列化(需配合serde库)
Deserialize 支持反序列化(需配合serde库)
其他 From 支持从其他类型转换(需手动实现反向Into
Into 支持转换为目标类型(通常由From自动实现)

元组结构体(无名字段)

元组结构体无字段名,.. 语法的作用是 复用原实例的剩余字段,但需注意:必须显式指定 "要修改的字段",且 .. 只能放在最后(表示 "剩余所有位置的字段都复用原实例")。

rust 复制代码
struct Point(i32, i32, i32);

fn main() {
    let point = Point(10, 20, 30);
	// 使用 `..` 忽略剩余字段
	let Point(x, ..) = point;
	println!("x: {}", x); // 输出: x: 10
}

下面的提示warning是Point结构体的第二个和第三个参数没有用到过,

嵌套结构体

在嵌套结构体中,.. 语法用于逐层访问内部结构体的成员。通过连续使用点操作符,可以深入访问嵌套的每一级成员。

rust 复制代码
struct Point{
	x : u32, 
	y : u32,
}

struct Size {
    width: u32,
    height: u32,
}

struct Rectangle {
    origin: Point, // 嵌套 Point 结构体
    size: Size,    // 嵌套 Size 结构体
}

fn main() {
    let base_rect = Rectangle {
        origin: Point { x: 0, y: 0 },
        size: Size { width: 100, height: 200 },
    };

    // 基于 base_rect 创建新矩形:仅修改 origin.x 和 size.height
    let new_rect = Rectangle {
        origin: Point { x: 50, ..base_rect.origin }, // 复用 base_rect.origin 的 y
        size: Size { height: 250, ..base_rect.size }, // 复用 base_rect.size 的 width
        ..base_rect // 若其他字段未修改,可直接用 ..base_rect 兜底(此处可省略,仅演示)
    };

    println!(
        "new_rect: origin=({},{}), size=({}, {})",
        new_rect.origin.x, new_rect.origin.y,
        new_rect.size.width, new_rect.size.height
    );
    // 输出:new_rect: origin=(50,0), size=(100, 250)
}

与 Default trait 结合(默认值复用)

.. 语法能复用已有实例的字段值,结合 Default::default() 可快速基于默认值创建自定义实例。

rust 复制代码
#[derive(Debug, Default)]
struct Config {
    host: String,       // String 默认值:空字符串
    port: u16,          // u16 默认值:0
    timeout: u32,       // u32 默认值:0
    enable_ssl: bool,   // bool 默认值:false
}

fn main() {
    // 基于默认值,仅修改部分字段
    let custom_config = Config {
        host: "localhost".to_string(),
        port: 8080,
        enable_ssl: true,
        ..Config::default() // 其余字段用默认值
    };

    println!("{:#?}", custom_config);
    // 输出:
    // Config {
    //   host: "localhost",
    //   port: 8080,
    //   timeout: 0,
    //   enable_ssl: true,
    // }
}

总结

Rust 结构体是数据与行为的结合体,既提供灵活的复合类型定义,又通过所有权、可变性规则保证内存安全。无论是简单数据封装还是复杂业务模型,结构体都是 Rust 编程中不可或缺的核心工具,掌握其设计思想和使用规则是构建健壮 Rust 程序的关键。

相关推荐
q***9941 小时前
index.php 和 php
开发语言·php
oioihoii1 小时前
C++网络编程:从Socket混乱到优雅Reactor的蜕变之路
开发语言·网络·c++
笙年1 小时前
JavaScript Promise,包括构造函数、对象方法和类方法
开发语言·javascript·ecmascript
WZTTMoon1 小时前
Spring Boot 启动全解析:4 大关键动作 + 底层逻辑
java·spring boot·后端
神仙别闹1 小时前
基于C++实现(控制台)应用递推法完成经典型算法的应用
开发语言·c++·算法
kk哥88992 小时前
inout参数传递机制的底层原理是什么?
java·开发语言
小二·2 小时前
Spring框架入门:深入理解Spring DI的注入方式
java·后端·spring
listhi5202 小时前
基于改进SET的时频分析MATLAB实现
开发语言·算法·matlab
毕设源码-钟学长2 小时前
【开题答辩全过程】以 基于springboot和协同过滤算法的线上点餐系统为例,包含答辩的问题和答案
java·spring boot·后端