工程:把代码组织起来

模块、crate 与 workspace

它在解决什么

前面两章讲的是「怎么写对一段代码」。从这一章开始换一个层面: 怎么让一堆代码能被别人(和三个月后的你)读懂和改动。

模块系统在 Rust 里有一个和 Java / Go 都不太一样的地方 —— 目录结构不决定模块结构。你得自己用 mod 把文件挂上去。 这一篇讲清楚那套挂法和可见性规则。

三个层级

概念 是什么
crate 一次编译的单位。一个可执行程序是一个 crate,一个库也是
module crate 内部的命名空间树,用 mod 声明
path crate::a::b::c,从根走到某个东西

⚠️ 从 Java 过来要重装的一条:包名不是目录名。 src/foo/bar.rs 里的东西不会自动变成 foo::bar —— 除非有人写了 mod foo; 和 mod bar; 把它挂上去。

文件布局和 mod 的对应关系:

src/
├── main.rs          ← crate 根,里面写 mod shapes;
├── shapes.rs        ← 或者 shapes/mod.rs
└── shapes/
    └── circle.rs    ← shapes.rs 里写 pub mod circle;

📌 现在推荐 shapes.rs + shapes/ 并存这种写法, 老代码里的 shapes/mod.rs 也还能用,两者等价。

可见性:默认私有

不加 pub 就是私有,从模块外面调就是 E0603。

但「私有」的范围比你想的宽:本模块及其子模块都能看见。 所以同一个模块里的 sum() 调私有的 hidden() 完全正常。

🚨 那条不对称

这是模块系统最常被误解的地方:

父模块的私有项  ← 子模块能看见(用 super::)
子模块的私有项  ← 父模块看不见

两个方向都实测过: 子模块用 super::secret() 够到父模块的私有函数,没问题; 父模块去调子模块的私有项,E0603。

⇒ 记法:私有 = 对「我和我的后代」可见。 想明白这一条, 「为什么我在上层写的函数看不见下层那个东西」就不再是个谜。

四档可见性

写法 谁能看见
(不写) 本模块 + 子模块
pub(super) 再加上父模块
pub(crate) 整个 crate 内部,但不对外
pub 所有人,包括依赖你这个库的人

⇒ 判据:写库时,pub 和 pub(crate) 的区别是「这是不是你的 API 承诺」。 写成 pub 就意味着以后改它是破坏性变更。拿不准就先 pub(crate)。

struct 和 enum 的可见性不一样

这一对差异值得单独记:

pub struct 不等于字段也 pub。 字段逐个决定, pub 加在结构体上只是让类型名能被外面提到。 读私有字段是 E0616。

⭐ 这解释了 pub fn new() 这种构造函数为什么在 Rust 里到处都是: 字段私有时,外面根本没法用 Foo { .. } 字面量构造它 —— 你被迫提供一个构造函数,而那正好是加校验的地方。

pub enum 则相反:一旦公开,所有变体和变体里的字段都跟着公开。

这不是随意的:enum 的意义在于「就这几种」, 藏起一种会让下游的 match 永远没法穷尽 —— 而 穷尽性正是 enum 的全部价值。

use 只是起个短名字

它不改变可见性。 删掉那行 use 报的是 E0425(找不到这个名字),不是可见性错误 —— 因为那条路径本来就是公开的,只是写全名太长。

几个常见写法:

use std::collections::{HashMap, BTreeMap};   // 一次引多个
use std::io::Result as IoResult;             // 改名,避免撞名
pub use crate::shapes::Circle;               // re-export:把深处的东西提到浅处

⭐ pub use 是设计库 API 的关键工具:内部可以分十层模块, 对外只暴露一个扁平的入口。标准库自己就大量这么干。

workspace:多个 crate 共享一份锁

项目大了之后拆成多个 crate,根目录放一个 Cargo.toml:

[workspace]
members = ["core", "cli"]
resolver = "3"

好处是共享一个 Cargo.lock 和一个 target/ —— 依赖只编译一次,版本在整个工作区内统一。

⇒ 什么时候拆:当一部分代码需要被独立发布、或者需要独立的依赖集时。 纯粹为了「文件太多」而拆 crate 通常得不偿失 —— 模块已经够用了,而跨 crate 的循环依赖是不允许的。

下一步

代码组织好了,接下来是让它不退化的那件事 —— 见《测试与 cargo test》:Rust 把测试放进了语言本身, #[cfg(test)] 的代码在发布产物里根本不存在。

全部篇目见 Rust 教程首页。

本篇示例

下面每一条都是完整的、能单独编译的程序,由npm run test:rust 在每次构建前用真的 rustc 跑一遍。 「编译不过、报 E0382」这种话在这里是被验证过的断言。 报错原文和对照项的结果由同一道闸门自动回写,会随工具链更新,但不作为断言。

默认私有,`pub` 才露出去

mod outer {
    pub fn visible() -> i32 { 1 }
    fn hidden() -> i32 { 2 }
    pub fn sum() -> i32 { visible() + hidden() }
}

fn main() {
    println!("{}", outer::sum());
}

编译通过 · 输出 "3\n"

对照:从模块外面直接调 `hidden()`
mod outer {
    pub fn visible() -> i32 { 1 }
    fn hidden() -> i32 { 2 }
    pub fn sum() -> i32 { visible() + hidden() }
}

fn main() {
    println!("{}", outer::hidden());
}

编译不过:error[E0603]

`sum` 能调 `hidden` 是因为它们**在同一个模块里** —— 私有的意思是「本模块及其子模块可见」,不是「只有自己可见」。对照项从外面调就是 `E0603`。

🚨 子模块能看见父模块的私有项,反过来不行

mod parent {
    fn secret() -> i32 { 42 }

    pub mod child {
        pub fn peek() -> i32 {
            super::secret()
        }
    }
}

fn main() {
    println!("{}", parent::child::peek());
}

编译通过 · 输出 "42\n"

对照:反过来:父模块去调子模块的私有项
mod parent {
    pub fn try_peek() -> i32 {
        child::inner()
    }

    pub mod child {
        fn inner() -> i32 { 42 }
    }
}

fn main() {
    println!("{}", parent::try_peek());
}

编译不过:error[E0603]

⭐ 这条不对称是 Rust 模块系统最常被误解的地方:**私有 = 对「我和我的后代」可见**。所以子模块可以用 `super::` 够到父模块的私有函数,而父模块够不到子模块的私有项(对照项 `E0603`)。

`pub struct` 不等于字段也 `pub`

mod shapes {
    pub struct Circle {
        pub r: f64,
        name: String,
    }

    impl Circle {
        pub fn new(r: f64) -> Self {
            Circle { r, name: String::from("圆") }
        }

        pub fn name(&self) -> &str {
            &self.name
        }
    }
}

fn main() {
    let c = shapes::Circle::new(2.0);
    println!("{} {}", c.r, c.name());
}

编译通过 · 输出 "2 圆\n"

对照:直接读私有字段 `c.name`
mod shapes {
    pub struct Circle {
        pub r: f64,
        name: String,
    }

    impl Circle {
        pub fn new(r: f64) -> Self {
            Circle { r, name: String::from("圆") }
        }

        pub fn name(&self) -> &str {
            &self.name
        }
    }
}

fn main() {
    let c = shapes::Circle::new(2.0);
    println!("{} {}", c.r, c.name);
}

编译不过:error[E0616]

字段**逐个**决定可见性,`pub` 加在结构体上只是让这个**类型名**能被外面提到。对照项读私有字段报 `E0616`。⇒ 这也是为什么 `pub fn new()` 这种构造函数在 Rust 里到处都是:字段私有时外面根本没法用字面量构造它。

enum 的变体跟着 enum 走,没有单独的可见性

mod types {
    pub enum Status {
        Draft,
        Done,
    }

    pub struct Wrapper {
        field: i32,
    }

    impl Wrapper {
        pub fn new(field: i32) -> Self {
            Wrapper { field }
        }
    }
}

fn main() {
    let s = types::Status::Draft;
    let _w = types::Wrapper::new(1);
    println!("{}", matches!(s, types::Status::Draft));
}

编译通过 · 输出 "true\n"

对照:用字面量构造那个有私有字段的 struct
mod types {
    pub enum Status {
        Draft,
        Done,
    }

    pub struct Wrapper {
        field: i32,
    }

    impl Wrapper {
        pub fn new(field: i32) -> Self {
            Wrapper { field }
        }
    }
}

fn main() {
    let s = types::Status::Draft;
    let _w = types::Wrapper { field: 1 };
    println!("{}", matches!(s, types::Status::Draft));
}

编译不过:error[E0451]

`pub enum` 一旦公开,它的**所有变体和变体里的字段都跟着公开** —— 因为一个不能构造也不能匹配的变体没有意义。struct 恰好相反(对照项 `E0451`)。⇒ 这个差异不是随意的:enum 的意义在于「就这几种」,藏起一种会让 `match` 永远没法穷尽。

`use` 只是起个短名字,不改变可见性

mod math {
    pub mod basic {
        pub fn double(x: i32) -> i32 { x * 2 }
    }
}

use math::basic::double;

fn main() {
    println!("{}", double(21));
}

编译通过 · 输出 "42\n"

对照:删掉那行 `use`
mod math {
    pub mod basic {
        pub fn double(x: i32) -> i32 { x * 2 }
    }
}

fn main() {
    println!("{}", double(21));
}

编译不过:error[E0425]

`use` 做的事只有一件:把一个长路径在当前作用域里起个短名。它**不能**让你访问原本访问不到的东西 —— 那条路径本来就得是可见的。对照项删掉之后报 `E0425`(找不到这个名字),而不是可见性错误。