跳转到内容

Rust 模块

到目前为止,所有示例都写在一个 main.rs 文件里。小程序这样没问题,但真正的项目会把代码按逻辑拆成几块。Rust 用来做这件事的工具就是模块系统:mod、pub 和 use。

模块(module)是一个有名字的代码块,用来把相关的代码放在一起:

mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
fn main() {
let sum = math::add(2, 3);
println!("{sum}");
}

输出:

5
▶ 动手试一试

有两点值得注意:调用函数时写的是 math::add(...),把模块名当作路径的一部分;而且函数前面标了 pub,没有它,模块外面就访问不到这个函数。

这一点值得专门说一下,因为很多人会感到意外:模块里的内容默认是私有的。如果把上面例子里的 pub 去掉,代码就编译不过:

mod math {
fn add(a: i32, b: i32) -> i32 { // 没有 `pub`
a + b
}
}
fn main() {
let sum = math::add(2, 3); // ❌ 错误:函数 `add` 是私有的
println!("{sum}");
}

凡是想让模块外部的调用者使用的东西,都必须明确地标上 pub。这是有意为之的设计:模块可以藏起内部的辅助函数,只公开那些打算作为公有 API 的部分。

模块里还可以再放模块,大一点的项目就是这样搭出一棵树状结构的:

mod shapes {
pub mod circle {
pub fn area(radius: f64) -> f64 {
3.14159 * radius * radius
}
}
}
fn main() {
let area = shapes::circle::area(2.0);
println!("{area}");
}
▶ 动手试一试

每次都写 shapes::circle::area(...) 很快就会觉得烦。use 可以把一个路径引入作用域,之后就能直接用它:

mod shapes {
pub mod circle {
pub fn area(radius: f64) -> f64 {
3.14159 * radius * radius
}
}
}
use shapes::circle;
fn main() {
let area = circle::area(2.0); // 比 shapes::circle::area 短
println!("{area}");
}
▶ 动手试一试

其实你已经用过 use 了,就是 HashMap 那一页里的同一个关键字(use std::collections::HashMap;)。

上面的例子都把所有代码放在一个文件里,因为 Rust Playground 只能这样。在你自己电脑上的真实项目里,每个模块通常有自己的文件。假如项目里有一个 math 模块,目录一般是这样的:

src/
├── main.rs
└── math.rs
src/main.rs
mod math; // 告诉 Rust:这个模块的代码在 math.rs 里
fn main() {
let sum = math::add(2, 3);
println!("{sum}");
}
src/math.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}

mod math;(以分号结尾,没有花括号里的内容)告诉 Rust 去同一个目录下的 math.rs 里加载这个模块的内容。项目一旦超出单个文件,你就会用到这种写法。

写一个叫 convert 的模块,里面有两个公有函数:c_to_f(摄氏度转华氏度)和 f_to_c(反过来)。在 main 里打印 100°C 等于 212°F 和 212°F 等于 100°C。

提示:公式是 c * 9.0 / 5.0 + 32.0 和 (f - 32.0) * 5.0 / 9.0。别忘了 pub,调用时要带上模块名,比如 convert::c_to_f(100.0)。

查看答案
mod convert {
pub fn c_to_f(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}
pub fn f_to_c(f: f64) -> f64 {
(f - 32.0) * 5.0 / 9.0
}
}
fn main() {
println!("100°C 等于 {}°F", convert::c_to_f(100.0));
println!("212°F 等于 {}°C", convert::f_to_c(212.0));
}

输出:

100°C 等于 212°F
212°F 等于 100°C
▶ 动手试一试

两个函数都是 pub,所以位于模块外面的 main 可以调用它们。去掉其中一个 pub,编译器就会告诉你那个函数是私有的。

写一个嵌套模块 school::grades,里面有一个公有函数 letter(score: u32) -> char:90 分及以上返回 'A',80 分及以上返回 'B',其他返回 'C'。在 grades 里再写一个私有的辅助函数 is_at_least(score: u32, min: u32) -> bool,供 letter 使用。用 use 把 school::grades 引入作用域,然后打印 95、85 和 70 分对应的等级。

提示:school 里面的 mod grades 和 fn letter 都需要 pub,但 is_at_least 不要加。同一个模块里的代码总是可以调用私有函数。

查看答案
mod school {
pub mod grades {
pub fn letter(score: u32) -> char {
if is_at_least(score, 90) {
'A'
} else if is_at_least(score, 80) {
'B'
} else {
'C'
}
}
// 私有:只有 `grades` 里的代码能调用它
fn is_at_least(score: u32, min: u32) -> bool {
score >= min
}
}
}
use school::grades;
fn main() {
for score in [95, 85, 70] {
println!("{score} -> {}", grades::letter(score));
}
}

输出:

95 -> A
85 -> B
70 -> C
▶ 动手试一试

letter 能调用 is_at_least,因为它们在同一个模块里。如果在 main 里写 grades::is_at_least(95, 90),就会报错 “function is_at_least is private”。这正是模块的用意:对外只露出一个小小的公有 API,辅助函数留给自己用。

  • mod name { ... } 把相关的代码组织成一个有名字的模块。
  • 模块里的一切默认都是私有的,要让模块外部使用,就加上 pub。
  • use 把一个路径引入作用域,省得每次都写完整路径。
  • 在多文件项目里,mod name;(没有花括号里的内容)会从同名文件中加载这个模块的代码。
  • 模块内部的代码总能使用本模块的私有项,所以辅助函数保持私有,只公开调用者需要的部分。

小测验

1. 默认情况下,mod 里的项是公有的还是私有的?

2. 想让 Rust 从 math.rs 加载模块代码,而不是直接写在当前文件里,该怎么写?

得分: 0 / 2