Rust 基础入门(四)(更新中)
本文为阅读《Rust圣经》的第一篇笔记,内容包括《Rust基础入门》最后4章节(11~14),建议先看完本网站文章《Rust基础入门(一)/(二)/(三)》等,如果想查看原书请搜索“Rust圣经”或点击此链接:[Rust语言圣经(Rust Course)](https://course.rs/about-book.html)
包和模块
Rust 的组织管理
- 项目(Package):一个 Cargo 提供的 feature ,可以用来构建、测试和分享包
- 工作空间(WorkSpace):对于大型项目,可以进一步将多个包联合在一起,组织成工作空间
- 包(Crate):一个由多个模块组成的树形结构,可以作为三方库进行开发,也可以生成可执行文件进行运行
- 模块(Module):可以一个文件多个模块,也可以一个文件一个模块,模块可以被认为是真实项目中的代码组织单元
包和项目
包 Crate
对于 Rust 而言,包是一个独立的可编译单元,它编译后会生成一个可执行文件或者一个库。一个包会将相关联的功能打包在一起,使得该功能可以很方便的在多个项目中分享。同一个包中不能有同名的类型,但是在不同包中就可以。
项目 Package
由于 Package 就是一个项目,因此它包含有独立的 Cargo.toml 文件,以及因为功能性被组织在一起的一个或多个包。一个 Package 只能包含一个库(library)类型的包,但是可以包含多个二进制可执行类型的包。
二进制 Package
1
2
3
4
5
6
7$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs使用
cargo run可以运行该项目,输出Hello, world!。库 Package
1
2
3
4
5
6
7$ cargo new my-lib --lib
Created library `my-lib` package
$ ls my-lib
Cargo.toml
src
$ ls my-lib/src
lib.rs这里使用
cargo run不能运行my-lib,因为库类型的 Package 只能作为三方库被其它项目引用,而不能独立运行,只有之前的二进制 Package 才可以运行。
理解:项目(Package)是包含 Cargo.toml的工程目录,包(Crate)是项目内部的编译单元(每个 src/main.rs或 src/lib.rs定义一个包)。
一个真实项目中典型的 Package,会包含多个二进制包,这些包文件被放在 src/bin 目录下,每一个文件都是独立的二进制包,同时也会包含一个库包,该包只能存在一个 src/lib.rs:
1 | . |
- 唯一库包:src/lib.rs
- 默认二进制包:src/main.rs,编译后生成的可执行文件与 Package 同名
- 其余二进制包:src/bin/main1.rs 和 src/bin/main2.rs,它们会分别生成一个文件同名的二进制可执行文件
- 集成测试文件:tests 目录下
- 基准性能测试 benchmark 文件:benches 目录下
- 项目示例:examples 目录
模块 Module
模块的使用
使用模块可以将包中的代码按照功能性进行重组,最终实现更好的可读性及易用性。
使用 cargo new –lib restaurant 创建一个小餐馆,注意,这里创建的是一个库类型的 Package,然后将以下代码放入 src/lib.rs 中:
1 | // 餐厅前厅,用于吃饭 |
以上的代码创建了三个模块,有几点需要注意的:
- 使用 mod 关键字来创建新模块,后面紧跟着模块名称
- 模块可以嵌套,这里嵌套的原因是招待客人和服务都发生在前厅,因此我们的代码模拟了真实场景
- 模块中可以定义各种 Rust 类型,例如函数、结构体、枚举、特征等
- 所有模块均定义在同一个文件中
模块树
src/main.rs 和 src/lib.rs 被称为包根(crate root),因为这两个文件的内容形成了一个模块 crate,该模块位于包的树形结构(由模块组成的树形结构)的根部:
1 | crate |
这颗树展示了模块之间彼此的嵌套关系,因此被称为模块树。其中 crate 包根是 src/lib.rs 文件,包根文件中的三个模块分别形成了模块树的剩余部分。该树结点间的父/子结点关系被称为父/子模块
用路径引用模块
想要调用一个函数,就需要知道它的路径,在 Rust 中,这种路径有两种形式:
- 绝对路径,从包根开始,路径名以包名或者 crate 作为开头
- 相对路径,从当前模块开始,以 self,super 或当前模块的标识符作为开头
举个例子:
1 | mod front_of_house { |
- 绝对路径引用:因为 eat_at_restaurant 和 add_to_waitlist 都定义在一个包中,因此在绝对路径引用时,可以直接以 crate 开头,然后逐层引用,每一层之间使用
::分隔 - 相对路径引用:因为 eat_at_restaurant 和 front_of_house 都处于包根 crate 中,因此相对路径可以使用 front_of_house 作为开头
代码可见性
Rust 的模块可见性规则总结如下:
- 默认私有原则:所有模块、函数、结构体、枚举、常量等默认都是私有的
pub关键字:用于控制项对外的可见性- 可见性规则: 父模块无法访问子模块的私有项 子模块可以访问祖先模块的所有项 模块可见性不等于其内部项的可见性
- 层级控制:需要对外暴露的每一层级都需要显式添加
pub - 路径访问:通过绝对路径(
crate::)或相对路径访问时,路径上的每个部分都必须可见 - 模块对外可见后,其内部的函数、结构体等还需要单独标记
pub才能被外部访问。
结构体和枚举的可见性
结构体和枚举的可见性:
- 将结构体设置为 pub,但它的所有字段依然是私有的
- 将枚举设置为 pub,它的所有字段也将对外可见
使用 use 及受限可见性
使用 use
在 Rust 中,可以使用 use 关键字把路径提前引入到当前作用域中,随后的调用就可以省略该路径,极大地简化了代码。
模块与文件分离
按照之前格式,代码应该如下:
1 | mod front_of_house { |
为了将模块放入一个单独文件便于管理,可以进行如下修改:
把 front_of_house 前厅分离出来,放入一个单独的文件中 src/front_of_house.rs:
1 | pub mod hosting { |
然后,将以下代码留在 src/lib.rs 中:
1 | mod front_of_house; |
mod front_of_house;告诉 Rust 从另一个和模块 front_of_house 同名的文件中加载该模块的内容- 使用绝对路径的方式来引用 hosting 模块:
crate::front_of_house::hosting; - 模块 front_of_house 的定义还是在 src/lib.rs 中,只不过模块的具体内容被移动到了 src/front_of_house.rs 文件中。
上述代码会报错,因为如果需要将文件夹作为一个模块,我们需要进行显示指定暴露哪些子模块。
以下为两种修改方法:
第一种方法:创建 mod.rs 文件
文件结构:
1 | src/ |
文件内容:
- src/lib.rs:
1 | mod front_of_house; |
- src/front_of_house/mod.rs:
1 | pub mod hosting; |
- src/front_of_house/hosting.rs:
1 | pub fn add_to_waitlist() {} |
第二种方法:创建同名的 rs 文件
文件结构:
1 | src/ |
文件内容:
- src/lib.rs:
1 | mod front_of_house; |
- src/front_of_house.rs:
1 | pub mod hosting; |
- src/front_of_house/hosting.rs:
1 | pub fn add_to_waitlist() {} |
- 第一种方法(传统方法): 在
front_of_house目录中创建mod.rs文件 这个文件声明了该模块包含的子模块 适用于所有 Rust 版本 - 第二种方法(新风格): 在与
front_of_house目录同级的位置创建front_of_house.rs文件 这个文件声明了该模块包含的子模块 从 Rust 1.30 开始支持 优势:避免项目中出现大量同名的mod.rs文件,提高代码导航的便利性
无论使用哪种方法,编译都能成功,程序可以正常运行。
基本引入方式
绝对路径引入模块
1 | mod front_of_house { |
相对路径引入模块
1 | mod front_of_house { |
引入函数的方式
1 | use std::collections::HashMap; |
其实严格来说,对于引用方式并没有需要遵守的惯例,建议:优先使用最细粒度(引入函数、结构体等)的引用方式,如果引起了某种麻烦(例如前面两种情况),再使用引入模块的方式。
避免同名引用
- 使用父模块的方式来调用
1 | use std::fmt; |
- 使用 as 关键字
1 | use std::fmt::Result; |
引入项再导出
(感觉这个挺奇怪的www,可能还不太理解吧)
当外部的模块项 A 被引入到当前模块中时,它的可见性自动被设置为私有的,如果你希望允许其它外部代码引用我们的模块项 A,那么可以对它进行再导出:
1 | mod front_of_house { |
如上,使用 pub use 即可实现。这里 use 代表引入 hosting 模块到当前作用域,pub 表示将该引入的内容再度设置为可见。
当你希望将内部的实现细节隐藏起来或者按照某个目的组织代码时,可以使用 pub use 再导出,例如统一使用一个模块来提供对外的 API,那该模块就可以引入其它模块中的 API,然后进行再导出,最终对于用户来说,所有的 API 都是由一个模块统一提供的。
使用第三方包
1 | use rand::Rng; |
Rust 社区已经为我们贡献了大量高质量的第三方包,你可以在 crates.io 或者 lib.rs 中检索和使用,从目前来说查找包更推荐 lib.rs,搜索功能更强大,内容展示也更加合理,但是下载依赖包还是得用crates.io。
使用 {} 简化引入方式
1 | use std::collections::HashMap; |
1 | use std::io; |
- use self::xxx,表示加载当前模块中的 xxx。此时 self 可省略
- use xxx::{self, yyy},表示,加载当前路径下模块 xxx 本身,以及模块 xxx 下的 yyy
使用 * 引入模块下的所有项
1 | use std::collections::*; |
以上这种方式来引入 std::collections 模块下的所有公共项,这些公共项自然包含了 HashMap,HashSet 等想手动引入的集合类型。
当使用 * 来引入的时候要格外小心,因为你很难知道到底哪些被引入到了当前作用域中,有哪些会和你自己程序中的名称相冲突:
1 | use std::collections::*; |
以上代码中,std::collections::HashMap 被 * 引入到当前作用域,但是由于存在另一个同名的结构体,因此 HashMap::new 根本不存在,因为对于编译器来说,本地同名类型的优先级更高。
受限可见性
pub- 完全公开
1 | pub fn public_function() {} // 任何地方都可访问 |
pub(crate)- 当前包内可见
1 | pub(crate) fn crate_only() {} // 只在当前 crate 内可见 |
pub(super)- 父模块可见
1 | mod parent { |
pub(self)- 当前模块可见
1 | mod my_module { |
pub(in <path>)- 指定路径可见
1 | mod a { |






