第七章:模块系统与包管理——组织代码的艺术

第七章:模块系统与包管理——组织代码的艺术

本章导读:当你的代码从几百行增长到几万行,如何组织代码就变得至关重要。Rust 提供了一套清晰的模块系统,帮助你管理代码的可见性、组织文件结构。配合 Cargo 包管理器,你可以轻松地复用代码和分享库。本章将带你深入理解 Package、Crate、Module 之间的关系,以及如何使用 Cargo 管理依赖。


📦 7.1 代码组织的层次结构

Rust 的代码组织有几个层次,理解它们的关系是掌握模块系统的基础。

🏗️ 7.1.1 Package 和 Crate

Package 是一个 Cargo 项目,包含一个 Cargo.toml 文件。一个 Package 可以包含多个 Crate,但必须至少包含一个(库或二进制)。

Crate 是 Rust 编译的基本单位。有两种类型:

  • 二进制 Crate:可以编译成可执行程序,必须有 main 函数
  • 库 Crate:定义可被其他 Crate 使用的功能,没有 main 函数
my_project/
├── Cargo.toml        # Package 配置
├── src/
│   ├── main.rs       # 二进制 Crate(与 Package 同名)
│   ├── lib.rs        # 库 Crate(与 Package 同名)
│   └── bin/
│       └── tool.rs   # 额外的二进制 Crate

命名约定:如果 Package 名为 myproject</code>,那么 <code>src/main.rs</code> 编译成的二进制文件也叫 <code>myprojectsrc/lib.rs 形成的库也以 my_project 为名。

🧩 7.1.2 Module

Module 是 Crate 内部的组织单元,用于分组代码和控制可见性。

// src/lib.rs
mod network {
    pub fn connect() {
        println!("连接网络");
    }
    
    mod server {
        pub fn start() {
            super::connect();
            println!("服务器启动");
        }
    }
}

pub use network::server::start;  // 重导出

🗂️ 7.2 Module 的定义和使用

📝 7.2.1 内联 Module

最简单的方式是在文件中直接定义:

// src/lib.rs
mod english {
    pub fn hello() {
        println!("Hello!");
    }
    
    fn goodbye() {  // 私有函数
        println!("Goodbye!");
    }
}

mod chinese {
    pub fn hello() {
        println!("你好!");
    }
}

pub fn greet(lang: &str) {
    match lang {
        "en" => english::hello(),
        "zh" => chinese::hello(),
        _ => println!("???"),
    }
}

📁 7.2.2 文件 Module

当代码变多时,可以将 Module 放在单独的文件中:

src/
├── lib.rs
└── network.rs       # network module
// src/lib.rs
mod network;  // 声明 module,告诉编译器查找 network.rs

pub fn connect() {
    network::connect();
}
// src/network.rs
pub fn connect() {
    println!("连接网络");
}

📂 7.2.3 目录 Module

对于更复杂的 Module,可以使用目录结构:

src/
├── lib.rs
└── network/
    ├── mod.rs       # network module 入口
    ├── server.rs    # network::server
    └── client.rs    # network::client
// src/lib.rs
mod network;
// src/network/mod.rs
pub mod server;
pub mod client;

pub fn connect() {
    println!("连接网络");
}
// src/network/server.rs
pub fn start() {
    println!("服务器启动");
}

现代风格:Rust 2018 以后,可以省略 mod.rs,直接用 network.rs 作为目录入口:

src/
├── lib.rs
├── network.rs     # network module 入口
└── network/
    ├── server.rs
    └── client.rs

🔐 7.3 可见性控制

Rust 的默认可见性是私有的,需要用 pub 关键字公开。

🚪 7.3.1 基本可见性

mod outer {
    pub fn public_function() {
        println!("公开函数");
        private_function();
    }
    
    fn private_function() {
        println!("私有函数");
    }
    
    pub mod inner {
        pub fn inner_public() {
            println!("内部公开函数");
        }
        
        fn inner_private() {
            println!("内部私有函数");
        }
    }
}

fn main() {
    outer::public_function();      // ✓
    // outer::private_function();  // ✗ 私有
    outer::inner::inner_public();  // ✓
    // outer::inner::inner_private();  // ✗ 私有
}

🎯 7.3.2 细粒度可见性

mod outer {
    pub fn public() {}
    
    // 只对当前 crate 公开
    pub(crate) fn crate_visible() {}
    
    // 只对父 module 公开
    pub(super) fn parent_visible() {}
    
    // 只对特定 module 公开
    pub(in crate::outer::inner) fn specific_visible() {}
    
    pub mod inner {
        // 对所有祖先公开
        pub(super) fn call_parent() {
            super::parent_visible();
            super::crate_visible();  // 同一 crate 内可见
        }
    }
}

📤 7.3.3 结构体和枚举的可见性

mod types {
    pub struct Person {
        pub name: String,      // 公开字段
        age: u32,              // 私有字段
    }
    
    impl Person {
        pub fn new(name: String, age: u32) -> Self {
            Self { name, age }
        }
        
        pub fn age(&self) -> u32 {
            self.age
        }
    }
    
    // 枚举的变体继承枚举的可见性
    pub enum Status {
        Active,    // 因为 Status 是 pub,所以 Active 也是 pub
        Inactive,
    }
}

fn main() {
    let person = types::Person::new(String::from("张三"), 30);
    println!("姓名: {}", person.name);
    // println!("年龄: {}", person.age);  // 错误!私有字段
    println!("年龄: {}", person.age());   // 通过方法访问
}

📥 7.4 use 关键字

use 将路径引入当前作用域,简化代码。

🎯 7.4.1 基本用法

use std::collections::HashMap;
use std::io::Read;
use std::io::Write;

// 合并相同前缀
use std::io::{Read, Write};

// 引入所有公开项(慎用)
use std::collections::*;

// 引入 module 本身
use std::collections;

🔄 7.4.2 重命名和重导出

use std::io::Result as IoResult;  // 重命名

pub use std::collections::HashMap;  // 重导出(公开别人的模块)

🌳 7.4.3 相对路径

mod network {
    pub fn connect() {}
    
    pub mod server {
        pub fn start() {
            super::connect();  // 父级 module
            crate::network::connect();  // 从 crate 根开始
            self::local_fn();  // 当前 module
            
            fn local_fn() {}
        }
    }
}

⚠️ 7.4.4 避免的习惯

// ❌ 不推荐:污染命名空间
use std::collections::*;

// ❌ 不推荐:两个同名类型
use std::collections::HashMap;
use std::collections::hash_map::HashMap;  // 冲突!

// ✓ 推荐:使用完整路径或 as
use std::collections::HashMap;
use std::collections::hash_map::HashMap as StdHashMap;

// ✓ 推荐:函数调用使用完整路径
fn main() {
    std::collections::HashMap::new();
}

📚 7.5 Cargo 包管理

Cargo 是 Rust 的官方包管理器和构建系统。

📋 7.5.1 Cargo.toml 配置

[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <you@example.com>"]
description = "一个很棒的项目"
license = "MIT"
repository = "https://github.com/user/my_project"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
anyhow = "1.0"

[dev-dependencies]
criterion = "0.5"  # 仅在测试和 benchmark 中使用

[build-dependencies]
cc = "1.0"  # 仅在 build.rs 中使用

[features]
default = ["feature1"]
feature1 = []
feature2 = ["dep:some-crate"]

[profile.release]
opt-level = 3
lto = true

📦 7.5.2 添加和管理依赖

# 添加依赖
cargo add serde
cargo add serde --features derive
cargo add tokio --features full

# 添加开发依赖
cargo add --dev criterion

# 移除依赖
cargo rm serde

# 更新依赖
cargo update
cargo update -p serde  # 只更新 serde

# 查看依赖树
cargo tree

🔨 7.5.3 常用命令

# 创建项目
cargo new my_project
cargo new --lib my_library

# 构建
cargo build           # debug 模式
cargo build --release # release 模式

# 检查(更快,不生成二进制文件)
cargo check

# 运行
cargo run
cargo run -- arg1 arg2  # 传递参数

# 测试
cargo test
cargo test test_name    # 运行特定测试

# 文档
cargo doc
cargo doc --open

# 格式化
cargo fmt

# Lint
cargo clippy

# 发布
cargo publish

🗂️ 7.5.4 Workspace

对于包含多个相关 crate 的项目:

my_workspace/
├── Cargo.toml        # workspace 配置
├── crate_a/
│   ├── Cargo.toml
│   └── src/
│       └── lib.rs
├── crate_b/
│   ├── Cargo.toml
│   └── src/
│       └── lib.rs
└── target/           # 共享的编译输出目录
# 根目录 Cargo.toml
[workspace]
members = [
    "crate_a",
    "crate_b",
]

🧪 7.6 实战:构建一个模块化的命令行工具

让我们创建一个模块化的 CLI 工具,展示文件组织和可见性控制:

filetool/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── lib.rs
    ├── commands/
    │   ├── mod.rs
    │   ├── copy.rs
    │   └── list.rs
    └── utils/
        ├── mod.rs
        └── format.rs
# Cargo.toml
[package]
name = "filetool"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4.0", features = ["derive"] }
// src/lib.rs
pub mod commands;
pub mod utils;

pub use commands::{copy_files, list_files};
// src/commands/mod.rs
mod copy;
mod list;

pub use copy::copy_files;
pub use list::list_files;

// 内部共享函数
pub(super) fn validate_path(path: &str) -> std::io::Result<()> {
    std::fs::metadata(path)?;
    Ok(())
}
// src/commands/copy.rs
use super::validate_path;

pub fn copy_files(source: &str, dest: &str) -> std::io::Result<()> {
    validate_path(source)?;
    validate_path(dest)?;
    
    let content = std::fs::read(source)?;
    std::fs::write(dest, content)?;
    
    println!("已复制 {} -> {}", source, dest);
    Ok(())
}
// src/commands/list.rs
use super::validate_path;

pub fn list_files(path: &str) -> std::io::Result<()> {
    validate_path(path)?;
    
    for entry in std::fs::read_dir(path)? {
        let entry = entry?;
        let name = entry.file_name().to_string_lossy().to_string();
        let is_dir = entry.file_type()?.is_dir();
        
        println!("{} {}", if is_dir { "📁" } else { "📄" }, name);
    }
    
    Ok(())
}
// src/utils/mod.rs
pub mod format;

pub fn print_separator() {
    println!("{}", "=".repeat(40));
}
// src/utils/format.rs
pub fn format_size(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;
    const GB: u64 = MB * 1024;
    
    if bytes >= GB {
        format!("{:.2} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.2} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.2} KB", bytes as f64 / KB as f64)
    } else {
        format!("{} B", bytes)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_format_size() {
        assert_eq!(format_size(500), "500 B");
        assert_eq!(format_size(1024), "1.00 KB");
        assert_eq!(format_size(1048576), "1.00 MB");
    }
}
// src/main.rs
use clap::Parser;
use filetool::{copy_files, list_files, utils};

#[derive(Parser)]
#[command(name = "filetool")]
#[command(about = "一个简单的文件工具", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(clap::Subcommand)]
enum Commands {
    /// 列出目录内容
    List {
        /// 目录路径
        path: String,
    },
    /// 复制文件
    Copy {
        /// 源文件
        source: String,
        /// 目标文件
        dest: String,
    },
}

fn main() {
    let cli = Cli::parse();
    
    utils::print_separator();
    
    match cli.command {
        Commands::List { path } => {
            if let Err(e) = list_files(&path) {
                eprintln!("错误: {}", e);
            }
        }
        Commands::Copy { source, dest } => {
            if let Err(e) = copy_files(&source, &dest) {
                eprintln!("错误: {}", e);
            }
        }
    }
    
    utils::print_separator();
}

📝 本章小结

本章我们学习了 Rust 的模块系统和 Cargo 包管理:

  • Package 是 Cargo 项目,包含 Cargo.toml
  • Crate 是编译的基本单位,分为二进制和库
  • Module 组织代码和控制可见性
  • <code>pub</code> 控制可见性,pub(crate)pub(super) 提供细粒度控制
  • <code>use</code> 引入路径,pub use 重导出
  • Cargo 管理依赖、构建、测试

关键要点:

  • 默认私有,需要时才公开
  • 使用目录结构组织大型 module
  • 避免过度使用 use *
  • 使用 workspace 管理多 crate 项目

在下一章,我们将学习迭代器和闭包——Rust 函数式编程的精髓。


动手实验

  1. 创建一个 workspace,包含一个库 crate 和一个二进制 crate,让二进制 crate 使用库 crate 的功能。
  2. 为本章的 filetool 添加一个新命令 info,显示文件的大小和修改时间。
  3. 使用 pub(crate) 限制某些函数只在 crate 内可见,尝试从外部访问它们,观察编译错误。
← 返回目录