第五章 结构体、枚举与模块系统#

"Rust's type system and module system work together to help you write code that is both expressive and well-organized." — The Rust Programming Language

在前几章中,我们学习了 Rust 的基础语法和所有权系统。本章将介绍 Rust 中组织数据和代码的三大支柱:结构体(struct)枚举(enum)模块系统(mod)。它们共同构成了 Rust 程序的骨架,也是理解 Tauri 源码的基础。

        mindmap
  root((数据与代码组织))
    struct
      字段
      impl
      方法
    enum
      Option
      Result
      代数数据类型
    match
      穷尽检查
      模式匹配
      解构
    module
      mod
      pub
      crate 边界
    

5.1 结构体(Struct)#

5.1.1 定义与实例化#

结构体是将多个相关数据组合在一起的自定义类型:

// 定义结构体
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

fn main() {
    // 实例化
    let user1 = User {
        username: String::from("walter"),
        email: String::from("walter@example.com"),
        sign_in_count: 1,
        active: true,
    };

    println!("用户名: {}", user1.username);
}

与 C++/Java 的对比:

┌─────────────────────────────────────────────────────────┐
│  C++ class/struct        Java class         Rust struct  │
│  ────────────────        ──────────         ──────────── │
│  默认 public(struct)     默认 package       默认私有      │
│  或 private(class)       private 字段       pub 显式公开  │
│  有构造函数               有构造函数         无构造函数     │
│  有继承                   有继承             无继承(用Trait)│
│  有析构函数               有 finalize        有 Drop trait │
│  方法在类内定义            方法在类内定义     方法在 impl 块 │
└─────────────────────────────────────────────────────────┘

5.1.2 字段简写与结构体更新语法#

fn build_user(username: String, email: String) -> User {
    User {
        username,           // 字段简写:变量名与字段名相同时可省略
        email,              // 等价于 email: email
        sign_in_count: 1,
        active: true,
    }
}

fn main() {
    let user1 = build_user(
        String::from("walter"),
        String::from("walter@example.com"),
    );

    // 结构体更新语法:从已有实例创建新实例
    let user2 = User {
        email: String::from("walter2@example.com"),
        ..user1  // 其余字段从 user1 移动/复制
    };

    // 注意:user1.username 已被移动到 user2,不能再使用
    // println!("{}", user1.username);  // ❌ 编译错误
    println!("{}", user1.sign_in_count); // ✓ u64 实现了 Copy
}

⚠️ 所有权陷阱: ..user1 语法会移动非 Copy 字段。上例中 username: String 被移动到 user2user1.username 失效。但 u64bool 实现了 Copy,所以 user1.sign_in_countuser1.active 仍然可用。

5.1.3 元组结构体与单元结构体#

// 元组结构体(Tuple Struct):有名字的元组
struct Color(u8, u8, u8);
struct Point(f64, f64, f64);

// 即使字段类型相同,Color 和 Point 也是不同类型
let red = Color(255, 0, 0);
let origin = Point(0.0, 0.0, 0.0);

// 单元结构体(Unit Struct):没有任何字段
struct Marker;

// 常用于实现 Trait 但不需要存储数据的场景
impl std::fmt::Display for Marker {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "Marker")
    }
}

5.1.4 为结构体实现方法(impl 块)#

Rust 没有 class 关键字,方法通过 impl 块定义:

#[derive(Debug)]
struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    // 关联函数(Associated Function)—— 类似 C++ 的静态方法 / Java 的 static method
    // 没有 self 参数,通过 Rectangle::new() 调用
    fn new(width: f64, height: f64) -> Self {
        Self { width, height }
    }

    // 方法(Method)—— 第一个参数是 &self
    fn area(&self) -> f64 {
        self.width * self.height
    }

    // 可变借用方法
    fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }

    // 消费 self 的方法(获取所有权)
    fn into_square(self) -> Rectangle {
        let side = self.width.max(self.height);
        Rectangle::new(side, side)
    }

    // 方法可以有多个参数
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let mut rect = Rectangle::new(30.0, 50.0);
    println!("面积: {}", rect.area());        // 1500.0

    rect.scale(2.0);
    println!("缩放后: {:?}", rect);            // Rectangle { width: 60.0, height: 100.0 }

    let small = Rectangle::new(10.0, 20.0);
    println!("能容纳? {}", rect.can_hold(&small)); // true

    let square = rect.into_square();           // rect 被消费,不能再使用
    println!("正方形: {:?}", square);           // Rectangle { width: 100.0, height: 100.0 }
}

self 的三种形式:

┌────────────────┬───────────────────┬──────────────────────────┐
│ 签名            │ 含义               │ 类比                      │
├────────────────┼───────────────────┼──────────────────────────┤
│ &self          │ 不可变借用          │ C++ const method          │
│ &mut self      │ 可变借用            │ C++ non-const method      │
│ self           │ 获取所有权(消费)   │ C++ 右值引用 / move        │
│ (无 self)      │ 关联函数(静态)     │ C++ static / Java static  │
└────────────────┴───────────────────┴──────────────────────────┘

5.1.5 多个 impl 块#

Rust 允许为同一个类型定义多个 impl 块,这在实现不同 Trait 时非常有用:

struct Circle {
    radius: f64,
}

// 基本方法
impl Circle {
    fn new(radius: f64) -> Self {
        Self { radius }
    }

    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

// Display trait 的实现放在单独的 impl 块
impl std::fmt::Display for Circle {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "Circle(r={})", self.radius)
    }
}

5.1.6 derive 宏:自动实现常用 Trait#

#[derive(Debug, Clone, PartialEq)]
struct Config {
    host: String,
    port: u16,
    debug: bool,
}

fn main() {
    let c1 = Config {
        host: String::from("localhost"),
        port: 8080,
        debug: true,
    };

    let c2 = c1.clone();        // Clone
    println!("{:?}", c1);        // Debug
    assert_eq!(c1, c2);          // PartialEq
}

常用 derive 宏一览:

Trait

作用

类比

Debug

{:?} 格式化输出

Java toString()

Clone

深拷贝 .clone()

Java clone() / C++ 拷贝构造

Copy

隐式按位复制

C++ 平凡拷贝

PartialEq / Eq

== 比较

Java equals() / C++ operator==

PartialOrd / Ord

< > 比较

Java Comparable / C++ operator<

Hash

哈希值

Java hashCode()

Default

默认值

C++ 默认构造 / Java 默认值

Serialize / Deserialize

JSON 序列化(serde)

Java Jackson


5.2 枚举(Enum)#

5.2.1 基本枚举#

Rust 的枚举远比 C/C++ 和 Java 的枚举强大——每个变体(variant)可以携带不同类型的数据:

// 简单枚举(类似 C/C++ enum)
enum Direction {
    North,
    South,
    East,
    West,
}

// 携带数据的枚举(这是 Rust 的杀手特性!)
enum Message {
    Quit,                        // 无数据(类似单元结构体)
    Echo(String),                // 携带一个 String(类似元组结构体)
    Move { x: i32, y: i32 },    // 携带命名字段(类似结构体)
    Color(u8, u8, u8),           // 携带多个值(类似元组结构体)
}

与 C++/Java 枚举的对比:

┌──────────────────────────────────────────────────────────────┐
│  C/C++ enum            Java enum            Rust enum        │
│  ──────────            ─────────            ─────────        │
│  整数常量集合           可以有字段和方法     变体可携带不同数据 │
│  不能携带数据           所有实例结构相同     每个变体结构不同   │
│  类型不安全(隐式转int)  类型安全             类型安全+穷尽匹配 │
│  无模式匹配             switch(有限)        match(穷尽)      │
│                                             ≈ C++ std::variant│
│                                              + 模式匹配       │
└──────────────────────────────────────────────────────────────┘

5.2.2 为枚举实现方法#

impl Message {
    fn process(&self) {
        match self {
            Message::Quit => println!("退出"),
            Message::Echo(text) => println!("回显: {}", text),
            Message::Move { x, y } => println!("移动到 ({}, {})", x, y),
            Message::Color(r, g, b) => println!("颜色: #{:02x}{:02x}{:02x}", r, g, b),
        }
    }
}

fn main() {
    let messages = vec![
        Message::Echo(String::from("hello")),
        Message::Move { x: 10, y: 20 },
        Message::Color(255, 128, 0),
        Message::Quit,
    ];

    for msg in &messages {
        msg.process();
    }
}

5.2.3 Option 与 Result:最重要的两个枚举#

Rust 没有 null,取而代之的是标准库中的两个枚举:

// 标准库定义(简化)
enum Option<T> {
    Some(T),    // 有值
    None,       // 无值
}

enum Result<T, E> {
    Ok(T),      // 成功
    Err(E),     // 失败
}
fn find_user(id: u64) -> Option<String> {
    if id == 1 {
        Some(String::from("Walter"))
    } else {
        None
    }
}

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("除数不能为零"))
    } else {
        Ok(a / b)
    }
}

fn main() {
    // Option 的使用
    match find_user(1) {
        Some(name) => println!("找到用户: {}", name),
        None => println!("用户不存在"),
    }

    // if let 简写
    if let Some(name) = find_user(2) {
        println!("找到: {}", name);
    } else {
        println!("未找到");
    }

    // Result 的使用
    match divide(10.0, 3.0) {
        Ok(result) => println!("结果: {:.2}", result),
        Err(e) => println!("错误: {}", e),
    }

    // unwrap_or 提供默认值
    let result = divide(10.0, 0.0).unwrap_or(0.0);
    println!("安全结果: {}", result);
}

为什么 Option 比 null 好?

┌────────────────────────────────────────────────────────┐
│  Java null                      Rust Option<T>          │
│  ─────────                      ────────────────        │
│  任何引用类型都可能是 null        必须显式处理 None       │
│  NullPointerException(运行时)   编译期强制检查           │
│  忘记检查 → 崩溃                 忘记处理 → 编译不通过   │
│  Tony Hoare: "十亿美元的错误"    零成本抽象,无运行时开销 │
└────────────────────────────────────────────────────────┘

5.2.4 枚举在 Tauri 中的应用#

Tauri 大量使用枚举来表示各种状态和消息:

use serde::{Deserialize, Serialize};

// 前后端通信的命令响应
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
enum ApiResponse {
    Success { message: String },
    Error { code: u32, detail: String },
    Loading,
}

// Tauri 命令返回枚举
#[tauri::command]
fn api_call(endpoint: String) -> ApiResponse {
    if endpoint.is_empty() {
        ApiResponse::Error {
            code: 400,
            detail: String::from("Endpoint cannot be empty"),
        }
    } else {
        ApiResponse::Success {
            message: format!("Called {}", endpoint),
        }
    }
}

5.3 模块系统(Module System)#

Rust 的模块系统由四个核心概念组成:

┌─────────────────────────────────────────────────────────┐
│                  Rust 模块系统四要素                      │
├──────────┬──────────────────────────────────────────────┤
│ Crate    │ 编译单元,一个 crate 生成一个库或可执行文件    │
│ Module   │ 代码组织单元,控制作用域和可见性               │
│ Path     │ 引用模块中条目的路径(绝对/相对)              │
│ use      │ 将路径引入当前作用域的快捷方式                 │
└──────────┴──────────────────────────────────────────────┘

5.3.1 Crate 与 Crate Root#

┌─────────────────────────────────────────────────┐
│  二进制 Crate (Binary Crate)                     │
│  ─ Crate Root: src/main.rs                       │
│  ─ 生成可执行文件                                 │
│                                                   │
│  库 Crate (Library Crate)                         │
│  ─ Crate Root: src/lib.rs                         │
│  ─ 生成 .rlib,供其他 crate 依赖                  │
│                                                   │
│  一个 Package 可以同时包含:                       │
│  ─ 最多 1 个库 crate                              │
│  ─ 多个二进制 crate(src/bin/*.rs)               │
└─────────────────────────────────────────────────┘

与 C++/Java 的对比:

概念

Rust

C++

Java

编译单元

crate

翻译单元(.cpp)

.java 文件

代码组织

mod

namespace

package

可见性

pub

public/private

public/private/protected/package

依赖管理

Cargo.toml

CMakeLists.txt

pom.xml / build.gradle

导入

use

#include / using

import

5.3.2 定义模块#

方式一:在同一文件中定义(内联模块)

// src/main.rs
mod audio {
    pub struct Track {
        pub title: String,
        artist: String,  // 私有字段
    }

    impl Track {
        pub fn new(title: &str, artist: &str) -> Self {
            Self {
                title: title.to_string(),
                artist: artist.to_string(),
            }
        }

        pub fn display(&self) {
            println!("{} - {}", self.title, self.artist);
        }
    }

    pub mod playlist {
        use super::Track;  // 引用父模块中的 Track

        pub fn create_default() -> Vec<Track> {
            vec![
                Track::new("Song A", "Artist 1"),
                Track::new("Song B", "Artist 2"),
            ]
        }
    }
}

fn main() {
    let tracks = audio::playlist::create_default();
    for track in &tracks {
        track.display();
    }
}

方式二:拆分到独立文件(推荐用于大型项目)

src/
├── main.rs          # crate root
├── audio.rs         # mod audio 的内容
└── audio/
    └── playlist.rs  # mod audio::playlist 的内容
// src/main.rs
mod audio;  // 告诉编译器去找 src/audio.rs 或 src/audio/mod.rs

fn main() {
    let tracks = audio::playlist::create_default();
    for track in &tracks {
        track.display();
    }
}
// src/audio.rs
pub mod playlist;  // 声明子模块,对应 src/audio/playlist.rs

pub struct Track {
    pub title: String,
    artist: String,
}

impl Track {
    pub fn new(title: &str, artist: &str) -> Self {
        Self {
            title: title.to_string(),
            artist: artist.to_string(),
        }
    }

    pub fn display(&self) {
        println!("{} - {}", self.title, self.artist);
    }
}
// src/audio/playlist.rs
use super::Track;

pub fn create_default() -> Vec<Track> {
    vec![
        Track::new("Song A", "Artist 1"),
        Track::new("Song B", "Artist 2"),
    ]
}

5.3.3 可见性(pub)#

Rust 默认一切私有,需要用 pub 显式公开:

mod outer {
    pub mod inner {
        pub fn public_fn() {}      // 公开
        fn private_fn() {}          // 仅 inner 模块内可见

        pub struct Config {
            pub host: String,       // 公开字段
            port: u16,              // 私有字段(外部无法直接构造)
        }

        impl Config {
            // 必须提供构造函数,因为 port 是私有的
            pub fn new(host: &str, port: u16) -> Self {
                Self {
                    host: host.to_string(),
                    port,
                }
            }

            pub fn port(&self) -> u16 {
                self.port
            }
        }
    }

    // 在 outer 中可以访问 inner 的公开项
    pub fn demo() {
        inner::public_fn();
        // inner::private_fn();  // ❌ 私有
    }
}

pub 的变体:

┌─────────────────┬──────────────────────────────────────┐
│ 语法             │ 含义                                  │
├─────────────────┼──────────────────────────────────────┤
│ (无修饰)         │ 仅当前模块可见                         │
│ pub             │ 完全公开                               │
│ pub(crate)      │ 当前 crate 内可见(类似 Java package)  │
│ pub(super)      │ 父模块可见                             │
│ pub(in path)    │ 指定路径内可见                          │
└─────────────────┴──────────────────────────────────────┘

5.3.4 use 关键字与路径#

// 绝对路径(从 crate root 开始)
use crate::audio::playlist::create_default;

// 相对路径
use self::audio::Track;
use super::some_parent_item;  // 引用父模块

// 嵌套路径(减少重复)
use std::collections::{HashMap, HashSet, BTreeMap};

// 通配符导入(谨慎使用)
use std::io::prelude::*;

// 重命名(解决命名冲突)
use std::fmt::Result as FmtResult;
use std::io::Result as IoResult;

// 重新导出(pub use)
pub use crate::audio::Track;  // 让外部用户直接通过当前模块访问 Track

5.3.5 实战:Tauri 项目的典型模块结构#

一个中型 Tauri 项目的推荐结构:

src-tauri/
├── Cargo.toml
├── tauri.conf.json
└── src/
    ├── main.rs              # 入口,注册命令和插件
    ├── lib.rs               # 库入口(可选)
    ├── commands/             # Tauri 命令模块
    │   ├── mod.rs            # pub mod user; pub mod file; ...
    │   ├── user.rs           # 用户相关命令
    │   └── file.rs           # 文件相关命令
    ├── models/               # 数据模型
    │   ├── mod.rs
    │   └── user.rs
    ├── services/             # 业务逻辑
    │   ├── mod.rs
    │   └── auth.rs
    ├── state.rs              # 应用状态
    └── error.rs              # 统一错误类型
// src/main.rs
mod commands;
mod models;
mod services;
mod state;
mod error;

fn main() {
    tauri::Builder::default()
        .manage(state::AppState::new())
        .invoke_handler(tauri::generate_handler![
            commands::user::get_profile,
            commands::user::update_profile,
            commands::file::read_file,
            commands::file::write_file,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
// src/commands/mod.rs
pub mod user;
pub mod file;
// src/commands/user.rs
use crate::models::user::UserProfile;
use crate::state::AppState;
use crate::error::AppError;

#[tauri::command]
pub fn get_profile(state: tauri::State<AppState>) -> Result<UserProfile, AppError> {
    state.get_current_user()
}

#[tauri::command]
pub fn update_profile(
    state: tauri::State<AppState>,
    name: String,
    email: String,
) -> Result<UserProfile, AppError> {
    state.update_user(name, email)
}
// src/models/user.rs
use serde::{Serialize, Deserialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserProfile {
    pub id: u64,
    pub name: String,
    pub email: String,
}
// src/error.rs
use serde::Serialize;

#[derive(Debug, Serialize)]
pub enum AppError {
    NotFound(String),
    Unauthorized,
    Internal(String),
}

// 让 AppError 可以作为 Tauri 命令的返回错误
impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            AppError::NotFound(msg) => write!(f, "Not found: {}", msg),
            AppError::Unauthorized => write!(f, "Unauthorized"),
            AppError::Internal(msg) => write!(f, "Internal error: {}", msg),
        }
    }
}

5.4 综合实战:设计一个配置管理模块#

让我们综合运用本章知识,设计一个在 Tauri 应用中管理配置的模块:

// src/config.rs
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// 应用主题
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Theme {
    Light,
    Dark,
    System,
}

impl Default for Theme {
    fn default() -> Self {
        Theme::System
    }
}

/// 语言设置
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Language {
    English,
    Chinese,
    Japanese,
}

impl Default for Language {
    fn default() -> Self {
        Language::English
    }
}

/// 窗口配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WindowConfig {
    pub width: u32,
    pub height: u32,
    pub fullscreen: bool,
}

impl Default for WindowConfig {
    fn default() -> Self {
        Self {
            width: 1024,
            height: 768,
            fullscreen: false,
        }
    }
}

/// 应用配置(顶层)
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AppConfig {
    pub theme: Theme,
    pub language: Language,
    pub window: WindowConfig,
    pub data_dir: Option<PathBuf>,
}

impl AppConfig {
    /// 从文件加载配置
    pub fn load(path: &std::path::Path) -> Result<Self, ConfigError> {
        let content = std::fs::read_to_string(path)
            .map_err(|e| ConfigError::IoError(e.to_string()))?;
        serde_json::from_str(&content)
            .map_err(|e| ConfigError::ParseError(e.to_string()))
    }

    /// 保存配置到文件
    pub fn save(&self, path: &std::path::Path) -> Result<(), ConfigError> {
        let content = serde_json::to_string_pretty(self)
            .map_err(|e| ConfigError::SerializeError(e.to_string()))?;
        std::fs::write(path, content)
            .map_err(|e| ConfigError::IoError(e.to_string()))
    }

    /// 合并部分更新
    pub fn merge(&mut self, patch: ConfigPatch) {
        if let Some(theme) = patch.theme {
            self.theme = theme;
        }
        if let Some(language) = patch.language {
            self.language = language;
        }
        if let Some(window) = patch.window {
            self.window = window;
        }
    }
}

/// 配置的部分更新(所有字段都是 Option)
#[derive(Debug, Deserialize)]
pub struct ConfigPatch {
    pub theme: Option<Theme>,
    pub language: Option<Language>,
    pub window: Option<WindowConfig>,
}

/// 配置错误
#[derive(Debug, Serialize)]
pub enum ConfigError {
    IoError(String),
    ParseError(String),
    SerializeError(String),
}

impl std::fmt::Display for ConfigError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            ConfigError::IoError(e) => write!(f, "IO error: {}", e),
            ConfigError::ParseError(e) => write!(f, "Parse error: {}", e),
            ConfigError::SerializeError(e) => write!(f, "Serialize error: {}", e),
        }
    }
}

这个例子综合展示了:

  • 结构体AppConfigWindowConfigConfigPatch

  • 枚举ThemeLanguageConfigError

  • impl 块:方法、关联函数

  • derive 宏DebugCloneSerializeDeserializeDefault

  • Option:可选字段 data_dir、部分更新 ConfigPatch

  • Result:错误处理

  • 模块:作为独立文件组织


5.5 小结#

┌─────────────────────────────────────────────────────────┐
│              第五章知识地图                                │
│                                                          │
│  结构体 (struct)                                         │
│  ├── 命名字段结构体                                      │
│  ├── 元组结构体                                          │
│  ├── 单元结构体                                          │
│  ├── impl 块(方法 + 关联函数)                           │
│  ├── derive 宏(自动实现 Trait)                          │
│  └── self 的三种形式(&self / &mut self / self)          │
│                                                          │
│  枚举 (enum)                                             │
│  ├── 简单枚举                                            │
│  ├── 携带数据的枚举(Rust 的杀手特性)                    │
│  ├── Option<T>(替代 null)                              │
│  ├── Result<T, E>(错误处理)                             │
│  └── match 穷尽匹配                                      │
│                                                          │
│  模块系统 (mod)                                          │
│  ├── Crate(编译单元)                                   │
│  ├── Module(代码组织)                                   │
│  ├── pub(可见性控制)                                    │
│  ├── use(路径导入)                                      │
│  └── 文件 ↔ 模块映射                                     │
└─────────────────────────────────────────────────────────┘

核心要点回顾:

  1. 结构体 是 Rust 中组织数据的主要方式,通过 impl 块添加方法,没有继承,用 Trait 实现多态。

  2. 枚举 远比 C/Java 的枚举强大,每个变体可以携带不同类型的数据,OptionResult 是最重要的两个枚举。

  3. 模块系统 通过 mod / pub / use 组织代码,默认私有,显式公开。

  4. 在 Tauri 项目中,合理的模块划分(commands / models / services / error)是保持代码整洁的关键。

下一章预告: 第六章将深入 Trait 与泛型——Rust 的多态机制。如果说结构体和枚举是数据的骨架,那 Trait 就是行为的灵魂。