文章目录
- Rust 学习笔记(卷二)
- 八、工程
- 1. package 和 crate
- package 总览
- 包根(crate root)
- 2. 模块
- 初识模块
- 单个源文件中的嵌套模块
- 具有层级结构的源文件形成的嵌套模块
- 小结 use 语句
- 3. 文档
- 4. 使用第三方包
- 5. 打包自己的包
- 九、标准库
- 十、多线程的并发编程
- 十一、“不安全”编程
- unsafe 代码块
- 全局变量与静态变量
- 内部可变性
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();
}
// 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++ 的模块和命名空间的结合体。
mod orange {pub fn hello() {println!("Hello, orange!");}
}
use orange::hello;
fn main() {hello();
}
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++ 中模块和命名空间的组合。在单个源文件中,模块就可以嵌套定义,形成树形结构。
// 模块路径为 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。
}
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 语句的效果。
}
// 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");
}
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 代码块
全局变量与静态变量
理论上,全局变量越少越好,以防形成一盘散沙之势。然而,全局变量通常也是无法避免的,例如在使用单例设计模式时。