Rust 模块和文件不是一回事:一次讲清 `mod`、`use`、`pub use`

内容概览

  1. 为什么 Rust 模块系统容易让人困惑

    很多人会下意识把"模块"和"文件"画等号,但 Rust 里这两件事不是一回事。

  2. 先理解 crate:Cargo 认识的是入口文件,不是所有 .rs 文件
    src/main.rssrc/lib.rs 是默认入口,其他文件需要通过 mod 接入模块树。

  3. mod 是声明模块,use 是把名字引入作用域
    mod 决定代码是否进入 crate;use 只是让路径更短。

  4. 模块可以写在同一个文件里,也可以拆到不同文件里

    文件只是模块代码的存放方式,不是模块本身。

  5. math.rsmath/mod.rs 的关系

    两种写法在模块路径上等价,只是文件布局不同。

  6. 子模块、私有模块与 re-export

    用私有子模块组织实现,用 pub use 暴露真正想给外部使用的 API。

  7. crate::super::self:: 怎么理解

    可以把它们类比成路径里的根目录、上级目录和当前目录。

  8. 兄弟模块之间如何访问

    兄弟模块不能"直接互相看见",通常要通过父模块路径访问。

  9. prelude 模式:大型 crate 如何整理常用 API

    不一定把所有东西都暴露到根目录,可以通过 prelude 精心导出常用符号。

  10. 结语:Rust 模块系统的核心心智模型

    先想模块树,再想文件树;先理解可见性,再决定目录结构。

本文是对Rust modules vs files的整理与翻译


Rust 模块和文件不是一回事:一次讲清 modusepub use

Rust 初学者经常会遇到一个看起来很小、但实际很烦的问题:

我明明新建了一个 math.rs 文件,为什么 main.rs 里不能直接用?

比如项目结构是这样:

text 复制代码
src/
  main.rs
  math.rs

math.rs 里写:

rust 复制代码
pub fn add(x: i32, y: i32) -> i32 {
    x + y
}

然后 main.rs 里写:

rust 复制代码
fn main() {
    let result = math::add(1, 2);
    println!("{result}");
}

很多人会以为这应该能跑。毕竟在不少语言里,一个文件就是一个模块,文件存在了,编译器就会知道它。

但 Rust 不这样。

Rust 的模块系统最容易误解的地方就在这里:模块不是文件,文件也不会自动变成模块

文件只是代码的存放位置。真正决定模块结构的是 mod


一、先从 crate 说起

在 Rust 里,一个 crate 可以粗略理解成一个项目或一个编译单元。它通常有一个 Cargo.toml,里面声明包名、版本、edition、依赖等信息。

一个二进制 crate 默认入口是:

text 复制代码
src/main.rs

一个库 crate 默认入口是:

text 复制代码
src/lib.rs

也就是说,Cargo 默认会认识 main.rslib.rs

但它不会自动扫描 src 下面所有 .rs 文件,然后全部编译进去。

这点非常关键。

如果你随手新建一个文件:

text 复制代码
src/math.rs

但没有任何地方通过 mod math; 引用它,那么这个文件就不属于当前 crate 的模块树。编译器甚至不会解析它。

所以:

text 复制代码
文件存在 != 模块存在

Rust 要求你明确告诉编译器:这个模块是 crate 的一部分。


二、moduse 到底分别干什么

这是 Rust 模块系统里最重要的一组区别。

mod:声明一个模块

rust 复制代码
mod math;

这句话的意思是:

请在当前模块下面声明一个名为 math 的子模块。它的源码通常在下面两个位置之一:

text 复制代码
math.rs
math/mod.rs

如果是在 src/main.rs 里写 mod math;,编译器会去找:

text 复制代码
src/math.rs

或者:

text 复制代码
src/math/mod.rs

use:把路径引入当前作用域

rust 复制代码
use math::add;

这句话的意思是:

math::add 这个名字引入当前作用域,这样后面可以直接写 add()

它只是简化路径,不会让编译器加载新文件。

换句话说:

rust 复制代码
use math::add;

不能替代:

rust 复制代码
mod math;

这两个东西经常一起出现,但职责完全不同。

可以这样记:

text 复制代码
mod 决定"有没有这个模块"
use 决定"怎么更方便地叫它"

三、模块不一定要放在单独文件里

Rust 的模块可以直接写在当前文件中。

比如 src/main.rs

rust 复制代码
mod math {
    pub fn add(x: i32, y: i32) -> i32 {
        x + y
    }
}

fn main() {
    let result = math::add(1, 2);
    println!("1 + 2 = {result}");
}

这里 math 是一个模块,但它没有单独文件。

所以模块首先是语言层面的命名空间,不是文件系统概念。

它可以用来:

  • 组织相关函数、类型和常量
  • 控制可见性
  • 避免命名冲突
  • 对外隐藏内部实现

文件只是承载模块代码的一种方式。


四、把模块拆到单独文件

现在我们想把 math 模块拆出来。

项目结构:

text 复制代码
src/
  main.rs
  math.rs

src/math.rs

rust 复制代码
pub fn add(x: i32, y: i32) -> i32 {
    x + y
}

src/main.rs 必须写:

rust 复制代码
mod math;

fn main() {
    let result = math::add(1, 2);
    println!("1 + 2 = {result}");
}

注意 math.rs 里面不需要再包一层:

rust 复制代码
mod math {
    // ...
}

这是很多新手会犯的错误。

因为 mod math; 已经告诉编译器:math.rs 里的内容属于 math 模块。

所以正确心智模型是:

rust 复制代码
mod math;

大概等价于告诉编译器:

rust 复制代码
mod math {
    // 这里放 src/math.rs 的内容
}

只是实际项目中不要用 include! 这样手动拼文件,正常写 mod math; 就好。


五、math.rsmath/mod.rs 是等价布局

如果模块比较小,可以这样:

text 复制代码
src/
  main.rs
  math.rs

如果模块变复杂,可以这样:

text 复制代码
src/
  main.rs
  math/
    mod.rs

此时 src/main.rs 仍然是:

rust 复制代码
mod math;

fn main() {
    let result = math::add(1, 2);
    println!("{result}");
}

src/math/mod.rs

rust 复制代码
pub fn add(x: i32, y: i32) -> i32 {
    x + y
}

这两种布局对模块路径来说是等价的:

text 复制代码
src/math.rs
src/math/mod.rs

都表示:

text 复制代码
crate::math

可以简单理解:

text 复制代码
math.rs       适合小模块
math/mod.rs   适合模块下还要继续拆子模块

补充一点:现代 Rust 也常见另一种目录布局:

text 复制代码
src/
  math.rs
  math/
    add.rs
    sub.rs

也就是说,math.rs 可以作为 math 模块的入口,同时 math/ 目录放它的子模块。很多新项目更偏好这种方式,因为可以减少大量 mod.rs 文件。


六、子模块怎么拆

假设我们想让 math 下面有两个子模块:

text 复制代码
src/
  main.rs
  math/
    mod.rs
    add.rs
    sub.rs

src/main.rs

rust 复制代码
mod math;

use math::{add, sub};

fn main() {
    let result = add(1, 2);
    println!("1 + 2 = {result}");
}

src/math/add.rs

rust 复制代码
pub fn add(x: i32, y: i32) -> i32 {
    x + y
}

src/math/sub.rs

rust 复制代码
pub fn sub(x: i32, y: i32) -> i32 {
    x - y
}

现在关键在 src/math/mod.rs

如果只写:

rust 复制代码
mod add;
mod sub;

这表示 math 模块知道了两个子模块,但这两个子模块默认是私有的。

外部不能直接写:

rust 复制代码
use math::{add, sub};

因为 math::add 此时指的是一个私有模块,不是一个公开函数。

那是不是改成这样就行?

rust 复制代码
pub mod add;
pub mod sub;

这确实能让外部访问模块,但路径会变成:

rust 复制代码
math::add::add(1, 2)
math::sub::sub(1, 2)

这通常不是我们想要的 API。

我们真正想暴露的是函数,而不是内部拆分出来的模块。

更好的写法是:

rust 复制代码
mod add;
mod sub;

pub use add::add;
pub use sub::sub;

这样:

  • add 模块是私有实现细节
  • sub 模块是私有实现细节
  • math::add 是公开函数
  • math::sub 是公开函数

从调用方看,API 很干净:

rust 复制代码
use math::{add, sub};

fn main() {
    println!("{}", add(1, 2));
    println!("{}", sub(3, 1));
}

这就是 pub use 的价值:重导出 API,同时隐藏内部文件结构


七、pub modpub use 的区别

这两个写法都能让外部访问东西,但暴露的层级不一样。

pub mod

rust 复制代码
pub mod add;

表示把整个 add 模块公开出去。

调用方可能需要写:

rust 复制代码
math::add::add(1, 2)

这等于把你的目录结构暴露成公共 API。

pub use

rust 复制代码
mod add;

pub use add::add;

表示模块仍然私有,但把里面的 add 函数重新导出。

调用方写:

rust 复制代码
math::add(1, 2)

这更像是在设计 API,而不是把文件系统直接摊给用户看。

实际项目里,pub use 很常见。它可以让内部实现自由拆文件,而不影响外部调用路径。


八、crate::super::self:: 怎么理解

Rust 的模块路径里常见三个前缀:

rust 复制代码
crate::
super::
self::

可以把它们类比成文件路径:

text 复制代码
crate::  类似根目录 /
super::  类似上级目录 ../
self::   类似当前目录 ./

假设结构是:

text 复制代码
crate
└── math
    ├── add
    └── sub

math/add.rs 里,想访问 math 模块里的常量:

src/math/mod.rs

rust 复制代码
mod add;
mod sub;

const DEBUG: bool = true;

src/math/add.rs

rust 复制代码
pub fn add(x: i32, y: i32) -> i32 {
    if super::DEBUG {
        println!("add({x}, {y})");
    }

    x + y
}

这里 super::DEBUG 表示访问父模块 math 里的 DEBUG

即使 DEBUG 不是 pub,子模块也可以访问父模块中的私有项。Rust 的可见性不是简单地等同于文件路径,而是基于模块树。

如果你在任意地方想从 crate 根部开始写路径,可以用:

rust 复制代码
crate::math::add

这比相对路径更稳定,适合跨层级引用。


九、兄弟模块不能直接互相访问

假设:

text 复制代码
math
├── add
└── sub

你在 sub.rs 里不能直接写:

rust 复制代码
add::add(x, -y)

因为 addsub 是兄弟模块,不共享彼此的作用域。

正确方式是通过父模块:

rust 复制代码
pub fn sub(x: i32, y: i32) -> i32 {
    super::add::add(x, -y)
}

如果父模块做了 re-export:

src/math/mod.rs

rust 复制代码
mod add;
mod sub;

pub use add::add;
pub use sub::sub;

那么 sub.rs 里也可以写:

rust 复制代码
pub fn sub(x: i32, y: i32) -> i32 {
    super::add(x, -y)
}

这条规则很重要:兄弟模块之间不是天然互通的,要通过共同的父模块路径访问


十、use 不影响编译范围

很多人会问:

如果我写:

rust 复制代码
use math::add;

是不是只会编译 add,不会编译 sub

不是。

use 只影响名字是否进入当前作用域,不影响编译器解析哪些文件。

真正决定文件是否进入 crate 的是 mod 关系。

比如:

rust 复制代码
mod add;
mod sub;

pub use add::add;
pub use sub::sub;

只要 mod sub; 存在,编译器就会解析、类型检查、借用检查 sub.rs

即使你当前没有 use sub,它仍然是 crate 的一部分。

如果某个函数完全没人用,编译器可能会警告:

text 复制代码
warning: function is never used

最终二进制是否包含这段代码,则由优化和 dead code elimination 处理。

但"是否进入编译器检查范围"由 mod 决定,不由 use 决定。


十一、为什么会有 unused import 和 dead code 警告

如果你写:

rust 复制代码
use math::{add, sub};

但只用了 add

rust 复制代码
fn main() {
    let result = add(1, 2);
    println!("{result}");
}

编译器会警告 sub 是 unused import。

如果你连 sub 都没有对外导出,或者当前 crate 内没有使用它,编译器还可能警告 dead code。

这两个警告不是挑刺,而是在提醒你:

  • 作用域里引入了没用的名字
  • crate 里有暂时没有实际用途的代码

一般不要急着用:

rust 复制代码
#[allow(unused)]

去压警告。

更好的做法通常是:

  • 真的不用就删掉
  • 未来再需要时从版本控制里找回来
  • 如果是库对外 API,就从 lib.rs 或模块入口明确导出

十二、prelude 模式:不要把所有东西都塞到根目录

随着 crate 变大,模块层级会越来越复杂。

如果你把所有常用类型和 trait 都直接 re-export 到 crate 根目录,根命名空间会变得很拥挤。

很多 Rust crate 会提供一个 prelude 模块:

rust 复制代码
use some_crate::prelude::*;

prelude 通常不是"所有东西",而是一组精心挑选的常用符号。

它的好处是:

  • 用户可以快速导入常用 API
  • crate 根目录不会太乱
  • 可以避免一些名字冲突
  • 对外 API 更有层次

比如你自己写库时,也可以这样组织:

rust 复制代码
pub mod client;
pub mod error;
pub mod request;

pub mod prelude {
    pub use crate::client::Client;
    pub use crate::error::Error;
    pub use crate::request::Request;
}

使用者可以选择:

rust 复制代码
use my_crate::client::Client;

也可以选择:

rust 复制代码
use my_crate::prelude::*;

这是一种 API 设计习惯,不是语法魔法。


十三、一个实用的模块组织模板

对于一个稍微有点规模的 Rust 项目,可以参考这样的结构:

text 复制代码
src/
  lib.rs
  client.rs
  error.rs
  config.rs
  transport/
    mod.rs
    http.rs
    websocket.rs

src/lib.rs

rust 复制代码
pub mod client;
pub mod config;
pub mod error;

mod transport;

pub use client::Client;
pub use config::Config;
pub use error::Error;

src/transport/mod.rs

rust 复制代码
mod http;
mod websocket;

pub(crate) use http::HttpTransport;
pub(crate) use websocket::WebSocketTransport;

这里有几个设计点:

rust 复制代码
pub mod client;

表示 client 是公开模块,用户可以访问 my_crate::client::...

rust 复制代码
mod transport;

表示 transport 是内部实现,不对外暴露。

rust 复制代码
pub use client::Client;

表示把常用类型提升到 crate 根部,让用户可以写:

rust 复制代码
use my_crate::Client;
rust 复制代码
pub(crate) use ...

表示只在当前 crate 内公开,不暴露给外部依赖。

这类组织方式比"文件怎么摆就怎么暴露"更可控。


十四、常见误区总结

误区一:新建文件就能用

不行。必须有 mod 把它接入模块树。

rust 复制代码
mod math;

误区二:use 会加载文件

不会。use 只把已有路径引入作用域。

误区三:pub fn 就一定能从外部访问

不一定。

函数所在模块也必须是可访问的,或者函数被更上层 pub use 重新导出。

误区四:子模块文件里还要再写一层 mod xxx {}

通常不需要。

如果 main.rs 写了 mod math;,那么 math.rs 的内容已经在 math 模块里。

误区五:兄弟模块可以直接访问彼此

不能。要通过 super::crate:: 路径访问。

误区六:pub mod 是最好的公开方式

不一定。
pub mod 会公开模块结构。很多时候用私有 modpub use,API 更干净。


结语:先画模块树,再摆文件

Rust 模块系统一开始看起来麻烦,是因为它没有把"文件系统"和"命名空间"强行绑定在一起。

这其实是好事。

你可以按照实现需要拆文件,也可以按照 API 设计重新导出符号。

内部文件结构可以很细,外部 API 可以很简洁。

理解 Rust 模块系统,最重要的是记住这几句话:

text 复制代码
crate 是编译单元。
main.rs / lib.rs 是默认入口。
mod 把模块接入模块树。
use 只是缩短路径。
pub 控制可见性。
pub use 用来重新设计对外 API。
文件结构不等于 API 结构。

当你不再问"这个文件为什么不能用",而是开始问"这个模块有没有被接入模块树""这个路径在当前作用域里是否可见",Rust 的模块系统就会清晰很多。

最终,你会发现 Rust 的模块系统并不是在为难你。

它只是要求你明确区分两件事:

代码放在哪里,以及代码以什么名字暴露出去。

相关推荐
绯雾sama2 小时前
易扣AI (Go + CloudWeGo) 企业级AI智能体项目教程 第2章:后端项目用户模块搭建
后端
fliter2 小时前
半小时读懂 Rust:从语法符号到所有权思维
后端
fliter2 小时前
深入 Rust enum 的内存世界
后端
_Evan_Yao2 小时前
从“全量发布”到“小步快跑”:灰度发布的简单实践与学习路径
java·后端·学习
石小石Orz2 小时前
给Claude增加状态栏显示:claude-hud保姆级教程
前端·人工智能·后端
折哥的程序人生 · 物流技术专研3 小时前
《Java 100 天进阶之路》第21篇:Java Object类
java·开发语言·后端·面试·哈希算法
喵个咪3 小时前
Kratos + WebRTC 实战:实现浏览器 P2P 音视频通话与实时数据通信
后端·微服务·webrtc
Gopher_HBo3 小时前
GoFrameMap转换详解
后端
小江的记录本3 小时前
【MySQL】《MySQL日志面试背诵版+思维导图》(核心考点 + MySQL 8.0最新优化)
java·数据库·后端·python·sql·mysql·面试