第五章 结构体、枚举与模块系统#
"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被移动到user2,user1.username失效。但u64和bool实现了Copy,所以user1.sign_in_count和user1.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 |
作用 |
类比 |
|---|---|---|
|
|
Java |
|
深拷贝 |
Java |
|
隐式按位复制 |
C++ 平凡拷贝 |
|
|
Java |
|
|
Java |
|
哈希值 |
Java |
|
默认值 |
C++ 默认构造 / Java 默认值 |
|
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),
}
}
}
这个例子综合展示了:
结构体:
AppConfig、WindowConfig、ConfigPatch枚举:
Theme、Language、ConfigErrorimpl 块:方法、关联函数
derive 宏:
Debug、Clone、Serialize、Deserialize、DefaultOption:可选字段
data_dir、部分更新ConfigPatchResult:错误处理
模块:作为独立文件组织
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(路径导入) │
│ └── 文件 ↔ 模块映射 │
└─────────────────────────────────────────────────────────┘
核心要点回顾:
结构体 是 Rust 中组织数据的主要方式,通过
impl块添加方法,没有继承,用 Trait 实现多态。枚举 远比 C/Java 的枚举强大,每个变体可以携带不同类型的数据,
Option和Result是最重要的两个枚举。模块系统 通过
mod/pub/use组织代码,默认私有,显式公开。在 Tauri 项目中,合理的模块划分(commands / models / services / error)是保持代码整洁的关键。
下一章预告: 第六章将深入 Trait 与泛型——Rust 的多态机制。如果说结构体和枚举是数据的骨架,那 Trait 就是行为的灵魂。