这是Rust主题下的小小理念之一: 系统程序员也能享受美好. ---- Robert O'Callahan

本章介绍的特型:

  • crate: 项目间代码共享
  • 模块: 项目内代码组织
  • 版本管理
  • 记录和测试Rust代码
  • 消除不必要编译器警告
  • 发布库

8.1 crate (板条箱)

  • Rust程序由crate(板条箱)构成
  • crate是完整且内聚的单元: 包括库, 可执行程序的所有源码, 测试, 工具, 其他杂项
475

crate工作过程:

  • Cargo.toml: 需要的crate及其版本
    • [dependencies]: 项目所依赖的外部库, cargo编译时自动下载这些依赖 (在开源站点crates.io上检索)
    • 依赖图: 如果依赖库下仍有依赖, 构成了crate的依赖图, 而Cargo可以自动处理
    • 编译crate: 下载完成所有依赖后, Cargo会编译所有crate (为每个依赖运行一次rustc)
    • Rust将代码静态链接到最终可执行文件
    • 其他: cargo build --release会忽略断言, 生成优化的程序, 运行更快, 代价是编译时间更长
[package]
name = "actix-gcd"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }

8.1.1 版本

为了避免版本问题 (最常见的: 新增标识符导致旧版程序无法编译), Rust提供了版本控制.

Rust的版本控制只针对语法解释, 2015下的crate仍然可以与2021下的crate联动.

[package]
name = "actix-gcd"
version = "0.1.0"
edition = "2021" #版本控制

8.1.2 创建配置文件

Cargo.toml可以设定不同区段

命令行使用的Cargo.toml区段
cargo build[profile.dev]
cargo build --release[profile.release]
cargo test[profile.test]

8.2 模块

  • 模块功能: 项目内代码组织, 管理命名空间
  • 模块定义: 一组语法项的集合, 且语法项具有命名的特型
    • 语法项定义: 模块, 函数, 结构体, 枚举, 常量等命名实体
    • pub: 令某个语法项为公共项, 可以从模块外部访问
    • pub(crate): 令某个语法项在crate中任意使用(父级, 同级模块也能使用), 但不会作为外部结构公开.
    • 私有: 默认私有, 只能在定义它的模块及其子模块中可见 (重点: 子模块可见)
    • pub(in <path>): 在特定路径下(及其子模块)可见
// 孢子模块
mod spores {
    use cells::{Cell, Gene};

    pub struct Spore { // 孢子结构体
        ...
    }

    // 模拟孢子分裂
    pub fn produce_spore(factory: &mut Sporangium) -> Spore {
        ...
    }

    // 提取基因
    pub(crate) fn genes(spore: &Spore) -> Vec<Gene> {
        ...
    }
}

8.2.1 嵌套模块

特型:

可见度问题:

  • 私有mod可以包含pub mod
  • 但是, 如果从外部访问一个pub mod, 而其父mod私有的话, 无法访问. (必须保证路径所有的mod都是pub mod)

更优解: 拆分源文件, 利用文件树来辅助划分

mod plant {
    pub mod roots { // plant的子模块
        ...
    }

    pub mod stems{ // plant的子模块
        ...
    }
}

8.2.2 单独文件中的模块

文件树

src/
├── main.rs      # 入口文件
└── spores.rs    # `spores` 模块

main.rs

mod spores;  // 声明 `spores` 模块,Rust 会查找 `spores.rs`

fn main() {
    spores::grow();  // 调用 `spores.rs` 中的 `grow` 函数
}

spores.rs: 无需mod关键字来声明自己是一个模块, Rust将spores.rs整体视为mod spores模块

pub fn grow() {
    println!("Spores are growing!");
}

更一般的文件树

表示目录模块的方式:

  • 目录下创mod.rs文件
  • 同级目录创建plant目录和plants.rs文件
fern_sim/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── spores.rs                 spores模块
    └── plant/
        ├── mod.rs                plant目录下的mod.rs表示plant模块
        ├── leaves.rs             子模块
        ├── roots.rs              子模块
        └── stems.rs              子模块

8.2.3 路径与导入

::用于访问模块中的语法项

use std::mem;
use std::collections::{HashMap, HashSet}; // 同时导入两个模块
use std::io::Result as IOResult; // 导入, 同时赋予本地别名

if s1 > s2 {
    std::mem::swap(&mut s1, &mut s2); // 直接访问
    mem::swap(&mut s1, &mut s2); // 导入访问 (导入模块而不是具体的语法项, 通常是更好的选择)
}

父子模块的导入 (super关键字)

父模块proteins/mod.rs

pub enum AminoAcid { ... }
pub mod synthesis; // 声明子模块

子模块proteins/synthesis.rs

use super::AminoAcid; // super表示父模块, self表示自身模块, crate表示当前模块所在的crate (相当于src)

pub fn synthesize(seq: &[AminoAcid]) { // 错误: 即使AminoAcid是pub, 仍然需要导入后才可见
    ...
}

crate与模块同名情况

假设: 存在一个image crate和一个image模块

use image::Pixels; // 存在歧义

// 解决方法

use ::image::Pixels; // 表示image crate
use self::image::Pixels; // 表示image模块

8.2.4 标准库预导入

rust会将常用的标准库内容预导入 (底层自动导入, 比如: Vec和Result都可以直接使用)

use std::prelude::v1::*; // 底层自动包含该use语句

8.2.5 公开use声明

  • use语法的本质: 创建一个别名, 在当前作用域引入某个item, 从而方便访问.
  • pub use: 导入当前模块时, 也可以通过当前路径访问pub use导入的内容

plant.rs

...

pub use self::leaves::Leaf; // 导入到当前模块中

main.rs

use plant;

fn main() {
    plant::take_leaf(); // 因为pub use, plant可以直接使用Leaf中的item
}

8.2.6 公开结构体字段

  • 私有字段: 模块及其子模块可以访问
  • 公共字段: 外部访问

Rust结构体与类区别:

  • Rust结构体: 私有字段同模块共享
  • 类: 私有字段无法共享
  • 区别: 同模块共享可以更加灵活, 同模块下可以定义多个紧密协作的类型, 互相访问私有字段
pub struct Fern {
    pub roots: RootSet,
    pub stems: StemSet,
}

8.2.7 静态变量与常量

  • 静态变量(默认不可变, 可以显式声明mut): 生命周期持续整个进程的变量
    • 使用场景: 常量值引用
    • 其他说明: Rust不鼓励mut静态变量, 因为线程不安全
  • 常量: 类似于C语言中的#define宏定义
    • 使用场景: 魔数, 字符串
pub const ROOM: f64 = 20.0; // 常量: 室内温度
pub static OUTDOOR: f64 = 8.0; // 静态变量: 室外温度

8.3 库创建

  • 编写库中的语法项 (需要外部访问的需要pub)
  • 修改main.rslib.rs
  • 无需修改Cargo.toml文件, Rust自动检查文件名, 如果为lib.rs则编译为库

注意: 示例中结构体(字段), 方法, 函数均为pub, 可以外部访问

// 植物结构体
pub struct Fern {
    pub size: f64,
    pub growth_rate: f64
}

// 植物的方法
impl Fern {
    // 生长方法
    pub fn grow(&mut self) {
        self.size *= 1.0 + self.growth_rate;
    }
}

// 模拟生长函数
pub fn run_simulation(fern: &mut Fern, days: usize) {
    for _ in 0 .. days {
        fern.grow();
    }
}

8.4 src/bin目录

crate中: 程序和库可以同时存在.

创建src/bin/efern.rs文件, 并写入main函数

使用cargo run即可正常编译, 分别编译:

  • 编译库
  • 编译可执行程序
use example::{Fern, run_simulation}; // example由Cargo.toml中的name字段决定

fn main() {
    let mut fern = Fern {
        size: 1.0,
        growth_rate: 0.001
    };

    run_simulation(&mut fern, 1000);
    println!("final fern size: {}", fern.size);
}

甚至可以在bin下创建其他的子模块, Rust自动将bin中的rs文件视为构建的额外程序

fern_sim/
├── Cargo.toml
└── src/
    └── bin/
        ├── efern.rs              模块
        └── draw_fern/
            ├── main.rs           可执行程序
            └── draw.rs           模块

如何导入自创库

[dependencies] 
fern_sim = { path = "../fern_sim" }

8.5 属性

Rust中任何语法项(item)都可以用属性修饰.

属性定义: Rust通用语法, 用于向编译器提供各种指令和建议

#[allow]: 允许禁用警告, 不显示

#[cfg]: 条件编译属性, 根据编译时配置有选择编译代码

#[cfg(...)] 选项当启用时······
test启用测试(使用 cargo testrustc --test 编译)
debug_assertions启用调试断言(通常在非优化构建中)
unix为 Unix(包括 macOS)编译
windows为 Windows 编译
target_pointer_width = "64"针对 64 位平台。另一个可能的值是 "32"
target_arch = "x86_64"特别针对 x86-64。其他值有:"x86", "arm", "aarch64", "powerpc", "powerpc64", "mips"
target_os = "macos"为 macOS 编译。其他值有:"windows", "ios", "android", "linux", "freebsd", "openbsd", "netbsd", "dragonfly"
feature = "robots"启用名为 "robots" 的用户自定义特性(用 cargo build --feature robotsrustc --cfg feature="robots" 编译)。这些特性是在 Cargo.toml[features] 区段中声明的。
not(A)不满足条件 A 时。如果要提供某函数的两种实现,请将其中一个标记为 #[cfg(X)],另一个标记为 #[cfg(not(X))]
all(A, B)同时满足 AB(相当于 &&
any(A, B)只要满足 AB 之一(相当于 `

#[inline]: 指定函数内联 (编译优化技术, 在调用点直接展开函数代码)

  • 属性附加在封闭区域: #!替换#
  • 属性附加在整个crate: #!替换#, 并写在main.rs或者lib.rs顶部

8.6 测试与文档

  • 测试函数定义: 测试函数带有#[test], 使用cargo test运行所有测试
  • 测试内容: 一般使用assert!assert_eq!测试
    • 可以使用debug_assert!debug_assert_eq!尽在调试构建中检查的断言
  • 测试函数的性能: Rust测试工具使用多个线程运行多个测试
#[test]
fn math_works() {
    let s: i32 = 1;
    assert!(s.is_positive());
    assert_eq!(x + 1, 2);
}

8.6.1 集成测试

  • 集成测试: 在src目录旁边创建tests目录, 内部创建rs文件
  • 测试: cargo test
  • 优势: 集成测试编译为独立crate, 模拟外部以crate调用该lib的过程, 更加仿真

8.6.2 文档

# 创建HTML文档
# --no-deps: 仅为fern_sim生成文档, 不会对依赖项生成
# --open: Cargo随后在浏览器中打开此文档
$ cargo doc --no-deps --open
  • 文档根据库中pub特型以及附加的所有文档型注释生成的.
  • 文档注释:
    • #[doc = "注释"]
    • ///, Rust看到三斜杠视为#[doc]属性

8.6.3 文档测试

  • 运行cargo test, Rust自动为文档中出现的代码块生成测试代码(主要是断言), 并进行测试.
  • 如果希望只编译不运行, 请在代码块类型中添加no_run
  • 如果连编译都不希望, 请在代码块类型中添加ignore

8.7 指定依赖项

如果代码没有发布在crates.io上, 可以通过指定Git库URL和修订号

image = { git = "https://github.com/Piston/image.git", rev = "528f19c" }

// 本地crate引用
image = { path = "vendor/image" }

8.7.1 版本

指定版本, Rust解析标准十分宽松: 选择与指定版本兼容的最新版本

image = "0.6.1" # Rust自动选择兼容版本好
image = "=0.10.0" # 指定确定的版本
image = ">=1.0.5" # 使用1.0.5及其更高版本
image = ">1.0.5<1.1.9" # 1.0.5到1.1.9之间的版本
image = "<=2.7.10" # 2.7.10及其更低版本

8.7.2 Cargo.lock

  • Cargo.toml中的版本号是故意保持灵活的, 但是编译时通常希望固定的版本号.
  • Cargo.lock在第一次构建时生成, 用来记录crate的确切版本, 以后的构建都将参考lock使用相同版本.
  • 更新版本
    • 直接修改Cargo.toml
    • cargo update: 只会升级到兼容版本

8.8 将crate发布到crates.io

8.9 工作空间

问题: 如果三个crate使用了重叠的依赖项, 独立编译会导致包含重复依赖导致磁盘浪费

解决方法: 在fernsoft下创建Cargo.toml文件

[workspace]
mambers = ["fern_sim", "fern_img", "fern_video"]

删除所有子crate中的Cargo.locktarget目录

完成后所有的cargo build均会在根目录下创建和使用共享的target目录

fernsoft/
├── .git/...
├── fern_sim/
│   ├── Cargo.toml
│   ├── Cargo.lock
│   ├── src/...
│   └── target/...
├── fern_img/
│   ├── Cargo.toml
│   ├── Cargo.lock
│   ├── src/...
│   └── target/...
└── fern_video/
    ├── Cargo.toml
    ├── Cargo.lock
    ├── src/...
    └── target/...