工程:把代码组织起来
模块、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`(找不到这个名字),而不是可见性错误。