Rust 的模块是怎么组织的
作者:@古明地盆
喜欢这篇文章的话,就点个关注吧,或者关注一下我的公众号也可以,会持续分享高质量Python文章,以及其它相关内容。:点击查看公众号
楔子
本次来聊一聊 Rust 的模块导入,到目前为止,我们只有一个 main.rs 文件,可以看一下当前的工程目录:
我们的项目叫 RustProj,里面的 src 目录负责存放源代码。在当前命令行中输入 cargo run 并回车的时候,就会执行 src/main.rs 里的 main 函数。但随着功能越来越复杂,我们不可能把代码都写在一个文件中,应该按照功能划分成不同的模块。那么下面就来看看 Rust 的模块是怎么组织的。
手动定义一个模块
在 Rust 中,模块可以来自于文件,也可以单独定义,举个例子:
// 通过 mod 关键字即可定义一个模块
// mod 后面跟模块名,以及模块内容
mod a {
// rust 模块也有一个可见性的问题,所有条目默认都是私有的
// 像函数、方法、struct、enum、模块、常量等等,都是私有的
// 想变成公有,需要在开头加上 pub 关键字
// 只不过结构体特殊,结构体开头加 pub 只表示结构体是公有的
// 但字段还是私有的,如果希望字段也公有,那么还要在每个字段前面加上 pub
pub fn mod_a_f1() {
println!("我是模块 a 下的函数 f1")
}
// 在模块内部还可以再定义模块
pub mod b {
pub fn mod_b_f2() {
println!("我是模块 a/b 下的函数 f2")
}
}
}
fn main() {
a::mod_a_f1();
a::b::mod_b_f2();
/*
我是模块 a 下的函数 f1
我是模块 a/b 下的函数 f2
*/
}
通过模块名可以调用模块内部的条目,访问方式是通过 ::
。但是要注意条目的可见性,默认都是私有的,如果想通过模块名访问,那么一定要在条目的前面加上 pub 关键字。比如里面的模块 b,必须在 mod b 的前面加上 pub,a::b 才能够成立。
那么可能有人好奇了,为啥 mod a 的前面没有 pub呢?原因是模块 a 位于最外层,和调用方处于同一级,因此不需要 pub。
另外在 Rust 里面还有一个 crate 的概念,crate 是 Rust 最小的编译单元,在一个范围内将多个文件里面的功能组合在一起,最终通过编译生成一个二进制文件或库文件。所以 crate 是项目中多个文件的组合,整体形成一棵树,并且其中一个是入口文件、也就是它的根,对于当前来说显然是 main.rs。
如果你对 crate 还感到有些困惑,那么这里给出一个虽然不准确、但是方便记忆的方式,你可以把 main.rs 所在的 src 目录理解为 crate。因此我们还可以这么调用:
fn main() {
crate::a::mod_a_f1();
crate::a::b::mod_b_f2();
/*
我是模块 a 下的函数 f1
我是模块 a/b 下的函数 f2
*/
}
通过 crate 即可从指定文件里面查找指定条目,并且这种方式相当于使用绝对路径进行查找,因此它永远都是成立的。另外可能有人发现了,这里 crate 后面没有指定文件啊,因为 main.rs 是入口文件,如果不指定文件的话,那么查找的默认是 main.rs 里的条目。
然后 mod 内部的条目之间,也可以相互调用,举个例子:
mod a {
pub fn mod_a_f1() {
b::mod_b_f2()
}
// 注意:mod b 不是公有的
mod b {
pub fn mod_b_f2() {
println!("我是模块 a/b 下的函数 f2")
}
}
}
fn main() {
a::mod_a_f1();
/*
我是模块 a/b 下的函数 f2
*/
// a::b::mod_b_f2(); // 不合法
}
我们在函数 mod_a_f1 的内部调用模块 b 的一个函数,显然整个过程不需要解释,但模块 b 不是公有的为啥也能访问呢?很简单,因为函数 mod_a_f1 和模块 b 是在同一级,所以可以直接拿到模块 b,因此模块 b 可以不是公有的,但它内部的函数必须公有。而 main 函数和模块 b 显然就不是同一级了,它们之间有一个屏障,也就是模块 a。但模块 b 在模块 a 里面不是公有的,因此在 main 函数里面无法通过 a::b 的方式获取。
然后上面是在父模块内部调用子模块的条目,因此条目必须公有;但如果是子模块调用父模块的条目,那么条目是否公有就都无所谓了。
mod a {
// 函数是私有的
fn mod_a_f1() {
println!("我是模块 a 下的函数 f1")
}
pub mod b {
pub fn mod_b_f2() {
println!("我是模块 a/b 下的函数 f2");
// 想在这里调用 mod_a_f1 要怎么做呢?
// 首先要找到模块 a,但它和 a 不在同一级
// 因此无法使用 a::mod_a_f1() 的方式
crate::a::mod_a_f1();
// 需要使用绝对路径,从 crate 开始定位
}
pub mod c {
pub fn mod_c_f3() {
println!("我是模块 a/b/c 下的函数 f3");
// 它是一个嵌套在模块 b 里面的模块
// 如果也要调用 mod_a_f1,显然方法和上面相同
// 但除此之外还有一种方式,就是使用 super
super::super::mod_a_f1();
// super 表示获取当前所在模块的上一级模块
// 一个 super 获取到的显然是模块 b
// 两个 super 获取到的就是模块 a 了
}
}
}
}
fn main() {
// 首先 main 函数里的 crate::a::mod_a_f1() 是不合法的;
// 因为 mod_a_f1 在模块 a 的内部是不可见的
// 但 mod_b_f2 里面也是 crate::a::mod_a_f1() 啊,为啥它就合法呢
// 因为子 mod 中的条目如果私有,对于父 mod 是不可见的
// 但父 mod 中的条目无论公有还是私有,子 mod 都是可见的
// 所以模块 a 的 mod_a_f1,对于在 crate 里面的 main 函数来说不可见
// 但对于在模块 b 里面的 mod_b_f2 来说可见
// 因此调用方式是一样的,唯一的区别就是调用位置所导致的可见性问题
a::b::mod_b_f2();
/*
我是模块 a/b 下的函数 f2
我是模块 a 下的函数 f1
*/
a::b::c::mod_c_f3();
/*
我是模块 a/b/c 下的函数 f3
我是模块 a 下的函数 f1
*/
}
还是很好理解的,需要注意的是里面的 super。从父模块找子模块的话,直接一级一级往下找即可,但子模块找父模块则需要从 crate 开始,有时会比较麻烦。为此 Rust 提供了 super,用于定位上一级模块。
然后我们再来看一个好玩的:
mod a {
pub mod b {
pub mod c {
pub fn mod_c_f3() {
println!("我是模块 a/b/c 下的函数 f3");
// super 表示上一级模块
// super::super 显然就是上上一级,也就是模块 a
// 那么 super::super::super 表示啥呢?显然是 crate
// 而通过 crate 即可找到 main 函数和模块 a
super::super::super::main();
}
}
}
}
fn main() {
println!("main 函数被调用");
a::b::c::mod_c_f3();
}
你觉得这段代码执行的时候会发生什么现象呢?我们试一下。
我们看到因为无限递归导致栈溢出了,相信你应该明白模块之间的关系了,多个 rs 文件整体组成一个 crate,基于 crate 可以获取每一个文件的条目。此方法相当于使用绝对路径定位,因此无论在什么情况下它都是可靠的。但如果模块嵌套的比较深,那么通过 crate 一级一级查找就有点麻烦了,比如我们要获取相邻模块(比如上一级)内部的条目,这种情况下 Rust 推荐使用 super。
另外上面使用三个 super 找到了 crate,如果是四个 super 呢?显然会报错,因为 crate 已经是最顶层了。
然后 Rust 还提供了一个 use 关键字,可以将指定的条目引入当前作用域,用于简化模块查找过程。
mod a {
pub mod b {
pub mod c {
pub fn mod_c_f3() {
println!("我是模块 a/b/c 下的函数 f3");
}
}
}
}
fn main() {
// 引入指定模块,这里通过绝对路径
use crate::a::b;
// 然后便可以通过 b 来进行查找
b::c::mod_c_f3(); //我是模块 a/b/c 下的函数 f3
// 引入模块,通过相对路径
use a::b::c;
c::mod_c_f3(); //我是模块 a/b/c 下的函数 f3
// 还可以导入到某一个具体的函数
use a::b::c::mod_c_f3;
mod_c_f3(); //我是模块 a/b/c 下的函数 f3
}
所以当模块层级比较多的时候,我们还可以使用 use 将指定的模块单独导入进来,这样在使用的时候就没必要从最外层开始找了。当然啦,我们在导入的时候还可以起别名。
fn main() {
use a::b::c as cc;
cc::mod_c_f3();
use a::b::c::mod_c_f3 as mod_c_f33333;
mod_c_f33333();
}
结果是一样的,总之在导入模块的时候,可以通过 as 起别名。
基于 .rs 文件定义模块
我们上面是通过 mod 关键字手动定义一个模块,但现在 src 目录下又多了一个 girl.rs 文件,里面内容如下:
然后我们在 main.rs 里面要如何导入它呢?首先这段代码是位于 girl.rs 里面,如果想在 main.rs 里面使用的话,那么条目一定要定义为公有,所以 mod a 的前面一定要有 pub。
下面来看看如何在 main.rs 里面导入它。
// 我们之前在 main.rs 里面也是使用 mod 关键字
// 而现在的代码放在了一个单独的文件里面
// 因此 mod xxx; 表示将 xxx.rs 导入进来成为一个模块
// 而 mod xxx {} 则表示自己手动定义一个名为 xxx 的模块
mod girl;
// 模块有了,我们也可以使用 use 关键字,但注意上面的 mod girl 一定不可以省略
// 我们之前可以直接使用 use,是因为模块是通过 mod 手动定义的
// 相当于模块就在 main.rs 里面,而现在的代码是在 girl.rs 里面
// 需要先通过 mod girl 将其变成一个模块,即 mod girl {girl.rs 里的内容}
// 然后才可以使用 use,不然 Rust 根本不知道 girl 是什么
use girl::a::b::c::Girl;
// 然后 a、b、c 也是模块,它们之间一层一层嵌套
fn main() {
// 可以一层一层导入
let g1 = girl::a::b::c::Girl{
name: String::from("古明地觉"),
age: 16, length: 156
};
// 因为模块 girl 和当前的 main 函数在同一级
// 因此在 main 函数里面可以直接找到模块 girl
// 当然我们也可以使用 crate,也就是基于绝对路径查找
let g2 = crate::girl::a::b::c::Girl{
name: String::from("古明地恋"),
age: 15, length: 155
};
// 上面已经使用 use 直接将 Girl 导入进来了
// 所以也可以直接用
let g3 = Girl{
name: String::from("雾雨魔理沙"),
age: 19, length: 163
};
println!("{:?}", g1);
println!("{:?}", g2);
println!("{:?}", g3);
/*
Girl { name: "古明地觉", age: 16, length: 156 }
Girl { name: "古明地恋", age: 15, length: 155 }
Girl { name: "雾雨魔理沙", age: 19, length: 163 }
*/
}
过程不难理解,核心就是使用 mod 关键字,mod 不仅可以用来定义模块,也可以将一个文件变成模块。
mod xxx; 表示导入 xxx.rs,其中模块名为 xxx。
mod xxx {} 表示手动定义模块 xxx,至于模块的内容则写在大括号里面。
当然不管哪一种,它们得到的都是模块,然后我们在 src 目录里面再定义一个 boy.rs。
pub struct Boy {
pub name: String,
pub age: u8,
}
pub mod a {
// crate 可以看作是 main.rs 所在的 src 目录
// 基于 crate 能获取任何一个文件里的条目,类似使用绝对路径查找一样
// 但注意这里 crate 的后面需要指定文件名
// 如果是 crate::Boy 则表示导入 main.rs 里的 Boy
use crate::boy::Boy;
// impl 前面不需要加 pub,因为外界调用的是里面的方法
// 所以只需要控制方法是否私有即可
impl Boy {
pub fn info(&self) -> String {
return format!("name = {}, age = {}", self.name, self.age);
}
}
// 我们还可以通过 impl crate::boy::Boy {} 定义方法
// 或者通过 impl super::Boy {} 也行,只要能找到即可
// 但很明显这样做代码有些长,特别是当 Boy 要反复被使用时
// 因此建议先通过 use crate::boy::Boy; 导入,然后直接使用 Boy 即可
}
然后在 main.rs 里面导入它:
mod boy;
fn main() {
let b1 = boy::Boy {
name: "sorata".to_string(),
age: 17,
};
// 也可以先通过 use 将 Boy 导入进来,然后直接使用 Boy
use boy::Boy;
let b2 = Boy {
name: "sorata".to_string(),
age: 17,
};
println!("{}", b1.info());
println!("{}", b2.info());
/*
name = sorata, age = 17
name = sorata, age = 17
*/
}
过程不难理解,但需要注意为 Boy 结构体实例实现的 info 方法,它必须是公有的,否则外界无法调用。因为结构体 Boy 的定义和 info 方法的定义,不在同一个模块中,如果两者在同一个模块,那么是否公有就无所谓了。
包的导入
通过 mod 可以加载一个文件,那如果是目录呢?这里我们在 src 目录中新创建一个 people 目录,里面有两个文件:girl.rs 和 boy.rs。
girl.rs 和 boy.rs 里面的内容分别如下:
// boy.rs
pub mod a {
pub mod b {
pub mod c {
# [derive(Debug)]
pub struct Boy {
pub name: String,
pub age: u8,
}
}
}
}
// girl.rs
pub mod a {
pub mod b {
pub mod c {
# [derive(Debug)]
pub struct Girl {
pub name: String,
pub age: u8,
pub length: u8,
}
}
}
}
girl.rs 和 boy.rs 里面各自定义了一个结构体,当然为了更好地理解 Rust 的模块,这里故意将代码变得复杂一些,专门搞了几个模块。然后 main.rs 要如何加载呢?
首先,如果一个目录想要成为一个 Rust 可以导入的包,那么里面必须要有一个 mod.rs 文件。这个 mod.rs 里面有什么,通过包名便可以导入什么。所以 Rust 的 mod.rs 就类似于 Python 里的 __init__.py。
Rust 一个包指向了里面的 mod.rs,所以我们在 people 目录再新建一个 mod.rs,内容如下:
// 导入 boy 和 girl,并且要声明为 pub,否则这两个模块无法被外界使用
// 之前之所以不用 pub,是因为模块直接导入到 main.rs 里面了
pub mod boy;
pub mod girl;
// 也可以使用 use 关键字,将某一个具体的条目引入到当前作用域
// 但还是那句话,使用 use 之前,必须先通过 mod 将模块导入进来
pub use boy::a::b::c::Boy;
由于有了 mod.rs,那么它所在的 people 目录就成为了一个 Rust 可以识别并导入的包。然后通过 people 来使用内部的条目,而这些条目都来自于 mod.rs,我们测试一下。
// 导入 people
mod people;
fn main() {
// 通过 people 一层层往下找
let b1 = people::boy::a::b::c::Boy{
name: String::from("sarata"), age: 17};
// 由于 Boy 在 mod.rs 中已经引入过来了
// 那么通过 people 也可以直接使用
let b2 = people::Boy{
name: String::from("sarata"), age: 17};
println!("{:?}", b1);
println!("{:?}", b2);
/*
Boy { name: "sarata", age: 17 }
Boy { name: "sarata", age: 17 }
*/
// Girl 也是同理
let g1 = people::girl::a::b::c::Girl{
name: String::from("satori"), age: 16, length: 156
};
println!("{:?}", g1);
/*
Girl { name: "satori", age: 16, length: 156 }
*/
// 但是 Girl 在 mod.rs 没有被引入进来,所以我们不能直接通过 people.Girl 来获取
// 需要单独 use,然后通过 Girl 来获取,因此这就要求 girl 在 mod.rs 必须通过 pub mod girl 定义模块
use people::girl::a::b::c::Girl;
}
当然,我们在 mod.rs 里面也可以单独定义一些逻辑,只要是 mod.rs 里面有的,通过包名都可以导入。只不过业务逻辑应该写在其它文件里面,然后在 mod.rs 里面再导过来。
这里再补充一点,因为 mod.rs 和 main.rs 是隔离的,所以 mod.rs 里面导入的条目如果想被 main.rs 导入,那么一定要使用 pub 声明为公有的。比如 pub mod boy,如果没有这个 pub,那么在 main.rs 里面就无法通过 crate::people 导入 boy。但在 mod.rs 里面 pub use 了 boy.rs 里面 Boy 结构体,所以即使 boy 不是公有的,依旧可以通过 crate::people::Boy 来使用 Boy 结构体。
因此如果一个模块里面有很多条目,你不希望全部暴露出去,那么就在 mod.rs 里面以私有的方式导入模块。并同时将那么希望被 main.rs 访问的条目,使用 pub use 单独导出即可。
然后再来看看在包的内部,文件之间要如何互相导入呢。我们在 people 目录下面再定义一个 method.rs,用于为 Boy 和 Girl 实现方法。
use crate::people::boy::a::b::c::Boy;
use crate::people::girl::a::b::c::Girl;
impl Boy {
pub fn info(&self) -> String {
format!("boy's name: {}, boy's age: {}",
self.name, self.age)
}
}
impl Girl {
pub fn info(&self) -> String {
format!(
"girl's name: {}, girl's age: {}, girl's.length: {}",
self.name, self.age, self.length)
}
}
光定义这个文件还不够,我们还要在 mod.rs 里面导入进来,否则该文件就白创建了,因为不可能被执行。
pub mod boy;
pub mod girl;
// 通过引入 method,保证 method.rs 会被执行
// 但没必要定义为公有,因为 main.rs 不需要使用它
// 我们只要保证 method.rs 里面内容会被执行即可
mod method;
然后是 main.rs:
mod people;
fn main() {
let b = people::boy::a::b::c::Boy{
name: String::from("sarata"), age: 17};
let g = people::girl::a::b::c::Girl{
name: String::from("sarata"), age: 17, length: 156};
println!("{}", b.info());
println!("{}", g.info());
/*
boy's name: sarata, boy's age: 17
girl's name: sarata, girl's age: 17, girl's.length: 156
*/
}
以上我们就为两个结构体实现了方法。
那么问题来了,如果导入的条目比较多,难道每个都要写一行吗?比如:
pub use crate::xxx::a;
pub use crate::xxx::b;
pub use crate::xxx::c;
pub use crate::xxx::d;
这么写无疑会有些麻烦,为此我们可以换一种方式:
// self 指的是 xxx 本身
// 如果除了 a、b、c、d 还希望导入 xxx 本身,那么就用 self
pub use crate::xxx::{self, a, b, c, d}
以上就是 Rust 的模块导入,特别是模块条目的可见性问题,需要我们注意。比如某个结构体的字段你不希望被修改,但其它字段可以,那么你就将不想被修改的字段声明为私有(不使用 pub)即可。如果在不被修改的前提下,希望访问是可以的,那么就再单独定义一个方法,返回该字段的值。
如果觉得文章对您有所帮助,可以请囊中羞涩的作者喝杯柠檬水,万分感谢,愿每一个来到这里的人都生活愉快,幸福美满。
微信赞赏
支付宝赞赏
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!