Rust 学习笔记(卷二)
文章目录
Rust 学习笔记(卷二)
八、工程
相比以前的 C++,Rust 提出了包和模块的概念,使工程管理变得更加有组织。下面我们会自顶向下的介绍 Rust 中有关工程的概念。
1. package 和 crate
package 总览
Rust 工程管理中,最大的概念是 package,其次是 crate。Rust 的 crate 可以是一个库,也可以是一个可执行文件,而 package 的作用则是将一个或多个 crate 组织起来。
Package 是使用 Cargo.toml
文件管理的,这是我们所熟悉的,而 crate 则有组织地被放在项目文件夹中,包括:
- 库包。需要存在
src/lib.rs
源文件。包名与项目名相同。一个项目只能有一个库包。 - 可执行文件包。需要存在
src/main.rs
源文件。包名与项目名相同。 - 更多可执行文件包。每个
src/bin/
中的源文件都是一个包,包名与文件名相同。
对比 C++ 来看,package 可以看作 CMake 的 project,而 crate 可以看作 CMake 的 target。
包根(crate root)
前面提到,如果一个 package 包含唯一的库包0,则需要存在 src/lib.rs
文件;对于可执行文件包则需要存在 src/main.rs
。像这种一个源文件代表一个 crate 的,我们称为包根(crate root)。
包根只是一个源文件,那多个源文件该怎么办?方法是借助模块。模块的机制允许我们将包内的代码放在多个源文件中。
2. 模块
初识模块
Rust 中,与模块相关的关键字是 mod
。
// src/orange.rs
pub fn hello() {
println!("Hello, orange!");
}
// src/main.rs
mod orange;
use orange::hello;
fn main() {
hello();
}
C++ 程序 79:module
// orange.cpp
export module orange; // 手动写明,与文件名本身无关。
import std;
namespace orange { // 手动创建命名空间,与模块无关。
export void hello() {
std::cout << "Hello, orange!" << std::endl;
}
}
// 源.cpp
import orange;
using orange::hello;
int main() {
hello();
}
关于 Rust 程序 79 的新东西有:
-
pub
关键字。如果要使用的东西不属于自己的父模块(例如程序 79 中,orange
模块和main
模块是兄弟,orange
模块不是main
模块的父模块),则只能使用公开的模块,用pub
关键字表示。这和 C++ 程序 79 中hello
函数前的export
关键字一样。 -
mod
关键字。其功能是在包根所在目录查找指定名字的源文件,并将源文件中的内容视为位于同名模块中(例如程序 79 中,在包根main.rs
同目录下找到了orange.rs
,将orange.rs
中的内容视为位于模块orange
中)。 -
use
关键字。一旦模块进入视野内(例如程序 79 中通过mod
语句在当前位置声明了一个模块,又如通过配置依赖项已经引入了外部模块),则可以使用use
语句引入模块里的内容。使用use
语句引入内容的可见性将在之后讲解。
与 C++ 要求必须在单独的文件里定义模块不同,Rust 中允许在同一个源文件里定义模块。所以 Rust 的模块更像是 C++ 的模块和命名空间的结合体。
Rust 程序 80:只有包根mod orange {
pub fn hello() {
println!("Hello, orange!");
}
}
use orange::hello;
fn main() {
hello();
}
C++ 程序 80:假装是模块
import std;
namespace orange { // 只有命名空间,没有模块。
void hello() {
std::cout << "Hello, orange!" << std::endl;
}
}
using orange::hello;
int main() {
hello();
}
Rust 程序 80 中,mod
块前没有再加 pub
,这是因为我们在下面使用它时已经能够看到它的完整实现了,所以没有必要再加上 pub
。
Rust 中,为什么不像 C++ 那样必须写 import std;
?因为 Rust 已经帮我们把常用的模块导入,模块名为 std::prelude
。
单个源文件中的嵌套模块
如前所述,Rust 的模块就像 C++ 中模块和命名空间的组合。在单个源文件中,模块就可以嵌套定义,形成树形结构。
Rust 程序 81:模块树// 模块路径为 crate::orange。使用 crate 表示根模块,对应包根。
mod orange {
pub struct Demo {
pub name: String, // 模块外只能访问结构体的 pub 字段。
}
// 模块路径为 crate::orange::details。
mod details {
// 使用 super 表示模块树中的父模块。可省略。
impl super::Demo {
pub fn print(&self) {
println!("{}", self.name);
}
}
}
}
// main 的模块路径为 crate::main。
fn main() {
// 使用 self 表示当前模块。只能使用 pub 的内容。
use self::orange::Demo;
let demo = Demo {
name: String::from("Orange"),
};
demo.print(); // 只需让实现的方法为 pub,无需让 impl 的模块为 pub。
}
C++ 程序 81:命名空间树
import std;
// 命名空间路径为 ::orange。使用 :: 表示全局命名空间。
namespace orange {
struct Demo {
public: // 类外只能访问结构体的 public 成员。
std::string name;
};
// 命名空间路径为 ::orange::details。
namespace details {
// 父命名空间可省略。
void print(const Demo& self) {
std::cout << std::format("{}", self.name) << std::endl;
}
}
}
// main 的命名空间路径为 ::main。
int main() {
using orange::Demo;
const auto demo = Demo{
.name = std::string("Orange"),
};
// C++ 不支持在多个命名空间内实现类的扩展方法。
using namespace orange::details;
print(demo);
}
具有层级结构的源文件形成的嵌套模块
如前所述,创建除包根外的源文件会导致新建一个模块。例如,包根 src/main.rs
对应的模块是 crate
,而与包根位于同一目录的 src/another.rs
对应的模块则是 crate::another
。这是我们在程序 79 中已经学会的。但如果我们希望用一个单独的源文件表示模块 crate::another::yet_another
,该怎么做?
方法是创建文件夹。容易想到,可以在 src/
新建一个名为 another
的文件夹,再在 another
文件夹中新建 yet_another.rs
源文件。
src/
|--main.rs
|--another/
|--yet_another.rs
但这是不够的。Rust 规定,对于 another
这种用文件夹表示的层级子模块,需要在文件夹中新建名为 mod.rs
的源文件,并在其中显式的导入其中的子模块。所以合规的文件结构应该为:
src/
|--main.rs
|--another/
|--mod.rs # 每个子文件夹中都必须有 mod.rs。
|--yet_another.rs
亦或者,在文件夹外新建一个与文件夹同名的源文件。
src/
|--main.rs
|--another.rs # 或者把 mod.rs 换成与模块同名的源文件放在外面。
|--another/
|--yet_another.rs
以上两种方式只能取其一。下面以 mod.rs
为例。mod.rs
的内容应该为:
pub mod yet_another;
而 main.rs
的开头还应该有:
mod another; // 包根 main.rs 不需要导出模块,所以不加 pub。
在完成以上准备工作后,要在 main.rs
中使用 yet_another
中的内容,可以写:
use another::yet_another::*; // ::* 表示导入所有内容。
为什么需要写以上 mod 语句和 use 语句?下面我们总结一下 Rust 从包根开始搜索模块的过程。
- Rust 编译器只知道存在包根(
main.rs
或lib.rs
)。包根对应的模块为根模块crate
。 - 如果包根源文件不使用
mod
语句声明模块,则编译器不知道存在其他模块。 - 使用
mod
语句引入模块yet_another
,编译器发现存在文件夹another
,于是编译src/another/mod.rs
或src/another.rs
,如果同时存在则报错。 - 编译
src/another/mod.rs
时,检查到pub mod yet_another
,编译器发现不存在文件夹src/another/yet_another
,但存在src/another/yet_another.rs
,于是将该源文件作为模块crate::another::yet_another
编译。 -
main.rs
中,已经能看到crate::another
。由于yet_another
是pub
的,所以也能看到模块crate::another::yet_another
。使用use
语句简化其中内容的使用。
一个很现实的问题是,子模块有时希望导出自己导入的内容(例如 another
模块希望导出 another::yet_another
模块),有时又希望不导出导入的内容,仅仅在内部自己使用(例如 crate
模块不会导出 crate::another
模块)。显然我们可以用 pub
关键字控制这一点。不过,上面的例子的导出对象都是模块,如果我们仅仅想要导出一个已经实现的函数该怎么办?可以使用 pub use
语句将当前模块导入的内容再导出。外部使用时,看这个导出的内容就好像是该模块自己实现的一样。
// orange.rs
mod utils {
pub fn print() {
println!("Orange");
}
// 可见,但限制只能在父模块中可见。
pub(super) fn internal_print() {
println!("Orange is handsome.");
}
}
// pub use 语句只能再导出 pub 的内容。
pub use utils::print; // 通过 use 语句实现了导出。
pub fn orange_main() {
print(); // use 语句的效果。
utils::internal_print();
}
// main.rs
mod orange;
fn main() {
orange::orange_main();
orange::print(); // pub use 语句的效果。
}
C++ 程序 82:引入项再导出
// orange.ixx
export module orange;
import std;
namespace orange {
namespace utils {
void print() {
std::cout << "Orange" << std::endl;
}
void internal_print() {
std::cout << "Orange is handsome." << std::endl;
}
}
// export using 语句可以直接导出非 export 的内容。
export using utils::print;
export void orange_main() {
print(); // using 语句的效果。
utils::internal_print();
}
}
// main.cpp
import orange;
int main() {
orange::orange_main();
orange::print(); // export using 语句的效果。
}
小结 use 语句
最后,我们对 use 语句作一些补充,并作一个简单的小结。可以说 Rust 中的 use 语句几乎等价于:
- C++ 中的 using 语句(后接函数等)和 using namespace 语句(后接命名空间)的结合。
- C# 中的 using static 语句(后接函数等)和 using 语句(后接命名空间)的结合。
- Java 中的 import 语句(后接函数或包)。
- ……
但 Rust 与 C++ 更类似,在使用 use 语句前,必须能够看到希望使用的内容。Rust 中使用 mod
语句从包根开始声明自己希望使用的模块,而 C++ 使用 import
语句声明自己希望使用的模块。相比之下,C#、Java 等语言不需要用额外语句导入模块。
很显然,use
语句能够帮助我们省略冗长的包路径,是一个缩写助手。所以 use
语句也能为所使用的内容起别名。
fn main() {
use std::print as my_print;
my_print!("Hello, world!");
use std as my_std;
my_std::path::Path::new("src/main.rs");
}
C++ 程序 83:使用别名
import std;
int main() {
// C++ 不允许用 using 语句为函数或对象起别名。
auto& my_cout = std::cout;
my_cout << "Hello, world!" << std::endl;
// C++ 中使用 namespace 语句为命名空间起别名。
namespace my_std = std;
my_std::filesystem::path("源.cpp");
}
3. 文档
4. 使用第三方包
5. 打包自己的包
九、标准库
十、多线程的并发编程
并发、异步。
十一、“不安全”编程
unsafe 代码块
全局变量与静态变量
理论上,全局变量越少越好,以防形成一盘散沙之势。然而,全局变量通常也是无法避免的,例如在使用单例设计模式时。