这篇文章将介绍Rust如何进行包管理。
前言:
对于市面上的教材或者教学视频,在包管理这一块感觉有点难懂。笔者意在用简单的方式来解释Rust中的包管理。
声明:
本文章基于rust的官方中文文档以及一些其他的书籍。你可以在rust官方中文教程中找到官方的包管理教程。
笔者假定你知道Rust的包管理是使用Cargo的也并不再说什么是包管理。
目录
创建一个package
基本的配置文件
构建和运行
使用cargo check
使用cargo build
使用cargo run
Package和Crate
编写一个库Create
Crate在哪里?
使用use将名称引入作用域
Create中的对外权限和pub关键字
mod
使用as为名称提供一个别名
使用pub use重导出名称
将模块拆分为不同的文件
Path
相对路径和绝对路径
super相对路径
总结
杂项讨论
Cargo.lock
如果读者使用过C++的cmake工具链,你会可能会感觉cmake的语法很混乱,干脆只用最简单的一些部分。在Rust中,得益于Cargo包管理工具,你可以很优雅、快速、便捷的运行测试程序和对程序进行重构和模块的管理。
现在我们要构建一个程序,来模拟一个学校。
使用cargo new命令来创建一个项目
cargo new school
其实叫项目是不准确的,我们看一下cargo --help对 new的解释
new Create a new cargo package
其实使用new是创建一个package,就是包。如果读者学过java,别怀疑,就是那个package。
你可以看见终端中显示如下:
Created binary (application) `school` package
显示这是一个二进制应用名叫‘school’的包
随后我们可以在当前文件夹下看见一个名为school的文件夹
jan@7X:~/Code$ ls | grep school
school
进入school这个package,我们可以看见如下两个文件
jan@7X:~/Code$ cd school/; ls
Cargo.toml src
其中src为存放源代码的地方, Cargo.toml为这个package的配置文件,使用.toml格式。
TOML (Tom's Obvious, Minimal Language) 格式,Cargo 使用此格式的配置文件对package进行配置
查看Cargo.toml你会看到如下的内容
cat Cargo.toml
[package]
name = "school"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
其中[package]是一个表块(section)标题,表明下面的语句用来配置一个包(package)。随着我们在这个文件增加更多的信息,还将增加其他表块。
表块英文直译为节或者部分,笔者将起理解为这个语境下的表块理解为配置文件中的一小段,一小节,读者们也可以试着理解。
接下来的三行设置了 Cargo 编译程序所需的配置:项目的名称、版本,以及使用的 Rust 大版本号。
name 即这个package的名称
version 表明这个包当前的版本
edition 表明使用的Rust版本,就像C++中是98、11、20一样
随后是[dependencies],也是一个表块,表明了此包的依赖包有哪些。
你可以使用如下命令对此package进行构建或者运行
cargo build 构建当前的package
cargo run 构建并运行当前的package
cargo check 检查当前的package是否可,不生成二进制文件
jan@7X:~/Code/school$ cargo check
Checking school v0.1.0 (/home/jan/Code/school)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
jan@7X:~/Code/school$ ls
Cargo.lock Cargo.toml src target
你可以看见多出一个名为target的文件夹,和一个后缀名为.lock的文件。
其中target是存放生成的目标对象的地方,此出的目标对象是指一些生成的二进制文件 ,或者cargo为我们生成的文档等。
进入target可以看见有如下两个文件
jan@7X:~/Code/school/target$ ls
CACHEDIR.TAG debug
进入debug
jan@7X:~/Code/school/target/debug$ ls
build deps examples incremental
cargo把编译好的二进制文件放进debug中,不过我们只是使用了check命令,此时debug下还没有二进制文件。
jan@7X:~/Code/school$ cargo build
Compiling school v0.1.0 (/home/jan/Code/school)
Finished dev [unoptimized + debuginfo] target(s) in 0.41s
随后我们可以看见debug中生成了二进制文件
jan@7X:~/Code/school/target/debug$ ls
build deps examples incremental school school.d
运行
jan@7X:~/Code/school/target/debug$ ./school
Hello, world!
成功显示hello world(cargo 默认会在src的main.rs 中写入一行打印hello world的代码)
首先对main.rs进行简单的改动,让其打印Hello, Cargo
jan@7X:~/Code/school$ cargo run
Compiling school v0.1.0 (/home/jan/Code/school)
Finished dev [unoptimized + debuginfo] target(s) in 0.18s
Running `target/debug/school`
Hello, world!
Hello, Cargo
我们看见cargo将我们的源文件重新编译了一遍然后运行
crate中文翻译为箱,再rust的文档中,没有将其翻译为中文的,保留其为英文。
正如crate的翻译,crate充当的角色就像是把某些东西放在一个箱子里,用的时候再取出来一样。
crate 是一个二进制项或者库。是一个树型的结构。其中二进制项就类似于编译器编译出来的二进制目标文件。库就是一个源代码的集合,就像rust标准库一样。
包(package) 是提供一系列功能的一个或者多个 crate。一个包会包含有一个 Cargo.toml 文件,阐述如何去构建这些 crate。
包中所包含的内容由几条规则来确立。一个包中至多 只能 包含一个库 crate(library crate);包中可以包含任意多个二进制 crate(binary crate);包中至少包含一个 crate,无论是库的还是二进制的。
暂时,我们只需要了解什么是package和creat即可,以及知道一个package中可以包含多个crate。
我们在src中新建lib.rs文件,我们要在里面编写我们需要的代码。
src/lib.rs
fn sun_rise() {
println!("太阳升起,孩子们该上学了");
}
fn sun_down() {
println!("太阳落下,孩子们该睡觉了");
}
现在我们想要在主函数中调用这两个函数,如何使用?
我们尝试直接使用:
src/main.rs
fn work_1() {
sun_rise();
sun_down();
}
我们会得到以下的错误
error[E0425]: cannot find function `sun_rise` in this scope
--> src/main.rs:6:5
|
6 | sun_rise();
| ^^^^^^^^ not found in this scope
error[E0425]: cannot find function `sun_down` in this scope
--> src/main.rs:7:5
|
7 | sun_down();
| ^^^^^^^^ not found in this scope
For more information about this error, try `rustc --explain E0425`.
error: could not compile `school` due to 2 previous errors
我们可以看见编译器发出没有在作用域内找到函数。
那我们应当如何使lib.rs中的函数在main.rs中可见呢?
到现在为止,我们根本没有发现源代码中出现Crate的字样,那么lib.rs对应的Create在哪里?
我们知道Create是一个树形结构。其实 Cargo 遵循的一个约定:src/main.rs 就是一个与包同名的二进制 crate 的 crate 根。同样的,Cargo 知道如果包目录中包含 src/lib.rs,则包带有与其同名的库 crate,且 src/lib.rs 是 crate 根。crate 根文件将由 Cargo 传递给 rustc
来实际构建库或者二进制项目。
我们来看这句话
同样的,Cargo 知道如果包目录中包含 src/lib.rs,则包带有与其同名的库 crate,且 src/lib.rs 是 crate 根。
就是说,如果cargo发现src中有lib.rs这个文件,则会自动创建一个名为与包同名的crate。
所以所,lib所在的crate就是package的名称。
use是Rust中的一个关键字,其作用就像C++中的using或者Java中的import一样。同理的,我们也可以将Rust中的Crate想象成C++中的一个namespace。
于是我们这样改写代码
use school::sun_rise;
use school::sun_down;
fn work_1() {
sun_rise();
sun_down();
}
尝试运行,我们会得到两个错误
error[E0603]: function `sun_rise` is private
--> src/main.rs:5:13
|
5 | use school::sun_rise;
| ^^^^^^^^ private function
|
note: the function `sun_rise` is defined here
--> /home/jan/Code/school/src/lib.rs:1:1
|
1 | fn sun_rise() {
| ^^^^^^^^^^^^^
error[E0603]: function `sun_down` is private
--> src/main.rs:6:13
|
6 | use school::sun_down;
| ^^^^^^^^ private function
|
note: the function `sun_down` is defined here
--> /home/jan/Code/school/src/lib.rs:5:1
|
5 | fn sun_down() {
| ^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `school` due to 2 previous errors
编译器说这两个函数是私有的,所以不能对外提供使用。
就像就像面向对象中的封装一样,对于一个Crate,在其中定义的名称,对外是不可见的,也就是private的,我们可以使用pub关键字来将其变为public的,也就是对外可见的。就像这样
src/lib.rs
pub fn sun_rise() {
println!("太阳升起,孩子们该上学了");
}
pub fn sun_down() {
println!("太阳落下,孩子们该睡觉了");
}
src/main.rs
fn main() {
work_1();
}
use school::sun_rise;
use school::sun_down;
fn work_1() {
sun_rise();
sun_down();
}
这回我们再来运行以下程序
jan@7X:~/Code/school$ cargo run
Compiling school v0.1.0 (/home/jan/Code/school)
Finished dev [unoptimized + debuginfo] target(s) in 0.18s
Running `target/debug/school`
太阳升起,孩子们该上学了
太阳落下,孩子们该睡觉了
成功运行
现在我们要为学校增添教学楼,食堂,宿舍等建筑,我们可以一股脑的将有关的函数放进lib.rs中,但这并不方便代码的维护和阅读。好在Rust中提供了类似C++的namespace关键字mod。我们将要实现的功能拆分成不同的模块(注意模块也是默认对外不可见的,并且即使模块对外可见,模块中的名称也是默认对外不可见的)。
我们可以这样做
src/lib.rs
//食堂模块
pub mod canteen {
pub enum Time {
MORRING,
NOON,
NIGHT,
}
pub fn cook(time: Time) {
match time {
Time::MORRING => println!("做早饭"),
Time::NOON => println!("做午饭"),
Time::NIGHT => println!("做晚饭"),
}
}
}
如果当两个作用域内的名称重叠时,我们可以使用as关键字为名称声明别名
use std::fmt::Result;
use std::io::Result;
这样会发生二义性,我们可以这样做来消除
use std::fmt::Result as FmtResult;
use std::io::Result as IOResult;
当我们导出一个mod中的名称的时候,在导入的地方此名称对外依旧是不可见的。
由于canteen模块下的Time枚举类比较通用,所以我们可以让其直接放在root下。于是我们在src/lib.rs下添加这一行。
use canteen::Time;
src/mian.rs
use school;
fn main() {
let morring = school::Time::MORRING;
}
运行
Checking school v0.1.0 (/home/jan/Code/school)
error[E0603]: enum `Time` is private
--> src/main.rs:4:27
|
4 | let morring = school::Time::MORRING;
| ^^^^ private enum
|
note: the enum `Time` is defined here
--> /home/jan/Code/school/src/lib.rs:26:5
|
26 | use canteen::Time;
| ^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `school` due to previous error
我们可以看见依旧显示Time是private的,我们可以在use前面加上一个pub来解决这个问题,表示这个名称在导入的地方对外是pub的。
src/lib.rs
pub use canteen::Time;
现在我们再来为学校添加宿舍模块,我们可以将其单独写进一个文件里。
src/dormitory.rs
pub fn play_weak_up_bell() {
println!("叮铃铃");
}
pub fn ligths_out() {
println!("宿舍集体熄灯");
}
src/main.rs
mod dormitory;
use school;
fn main() {
work_3();
}
fn work_3() {
school::sun_rise();
dormitory::play_weak_up_bell();
school::sun_down();
dormitory::ligths_out();
}
在 mod 后使用分号,而不是代码块,这将告诉 Rust 在另一个与模块同名的文件中加载模块的内容。
运行
jan@7X:~/Code/school$ cargo run
Compiling school v0.1.0 (/home/jan/Code/school)
Finished dev [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/school`
太阳升起,孩子们该上学了
叮铃铃
太阳落下,孩子们该睡觉了
宿舍集体熄灯
注意 mod XXX;这种写法只会寻找当前层级下的文件。
现在让我们为宿舍添加一个宿管子mod吧,同样的,我们可以如法炮制的。这首先需要我们创建一个和父mod名称相同的文件夹。
然后我们在dormitory这个文件夹下书写对应的源码
src/dormitory/dm_unit.rs
pub fn check() {
println!("检查宿舍");
}
在dormitory.rs中只需要pub重导入这个模块即可。
src/dormitory.rs
pub mod dm_unit; //pub重导入下面会说,这里的疑问可以放下
pub fn play_weak_up_bell() {
println!("叮铃铃");
}
pub fn ligths_out() {
println!("宿舍集体熄灯");
}
src/main.rs
fn main() {
work_3();
}
use school::sun_rise;
use school::sun_down;
#[allow(unused)]
fn work_1() {
sun_rise();
sun_down();
}
use school::canteen;
#[allow(unused)]
fn work_2() {
canteen::cook(canteen::Time::MORRING);
}
mod dormitory;
use dormitory::dm_unit;
use school;
fn work_3() {
school::sun_rise();
dormitory::play_weak_up_bell();
school::sun_down();
work_4();
dormitory::ligths_out();
}
fn work_4() {
dm_unit::check();
}
运行
jan@7X:~/Code/school$ cargo run
Compiling school v0.1.0 (/home/jan/Code/school)
Finished dev [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/school`
太阳升起,孩子们该上学了
叮铃铃
太阳落下,孩子们该睡觉了
检查宿舍
宿舍集体熄灯
这些操作下来,我们的src文件夹的树形结构应该是像这样
jan@7X:~/Code/school/src$ tree
.
├── dormitory
│ └── dm_unit.rs
├── dormitory.rs
├── lib.rs
└── main.rs
path就是我们常说的路径 ,比如说这就是Time的一个路径
canteen::Time
路径又分为两种表示方式
self
、super
或当前模块的标识符开头。 crate
开头。导入Time使用绝对路径重写
src/lib.rs
pub use crate::canteen::Time;
Rust中路径的相对路径和绝对路径和我们熟悉的关于文件相对路径和绝对路径并无二样。
学校的食堂已经运作起来了,但是我们需要成立一个保洁部门,在每天晚上的时候进行食堂的清扫。在canteen下,新建一个mod clear_unit
src/lib.rs::canteen
//保洁部模块
mod clear_unit {
fn clear(time: Time) {
match time {
Time::NIGHT => println!("打扫卫生"),
_ => return,
}
}
}
check我们会发现没有通过
jan@7X:~/Code/school$ cargo check
Checking school v0.1.0 (/home/jan/Code/school)
error[E0433]: failed to resolve: use of undeclared type `Time`
--> src/lib.rs:29:17
|
29 | Time::NIGHT => println!("打扫卫生"),
| ^^^^ use of undeclared type `Time`
error[E0412]: cannot find type `Time` in this scope
--> src/lib.rs:27:24
|
27 | fn clear(time: Time) {
| ^^^^ not found in this scope
|
help: consider importing this enum
|
27 | use crate::Time;
|
Some errors have detailed explanations: E0412, E0433.
For more information about an error, try `rustc --explain E0412`.
error: could not compile `school` due to 2 previous errors
编译器有说不能再作用域内找到相应的名称。
rust中子模块并不能使用父模块的名称,换句话说,rust不像C++一样,嵌套的mod(C++的namespace)父mod中的名称对于子mod来说并不可见。使用super关键字来解决这个问题。
superji代表了上一级mod的名称。
修改之前的代码如下
src/lib.rs::canteen
//保洁部模块
#[allow(unused)]
mod clear_unit {
use super::Time;
fn clear(time: Time) {
match time {
Time::NIGHT => println!("打扫卫生"),
_ => return,
}
}
}
这时候再使用我们就可以发现通过了check
jan@7X:~/Code/school$ cargo check
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
声明:以下内容删减修改自rust官方文档的原文
当你编写大型程序时,组织你的代码显得尤为重要,因为你想在脑海中通晓整个程序,那几乎是不可能完成的。通过对相关功能进行分组和划分不同功能的代码,你可以清楚在哪里可以找到实现了特定功能的代码,以及在哪里可以改变一个功能的工作方式。
伴随着项目的增长,你可以通过将代码分解为多个模块和多个文件来组织代码。一个包可以包含多个二进制 crate 项和一个可选的 crate 库。伴随着包的增长,你可以将包中的部分代码提取出来,做成独立的 crate,这些 crate 则作为外部依赖项。本文将会涵盖所有这些概念。
除了对功能进行分组以外,封装实现细节可以使你更高级地重用代码:你实现了一个操作后,其他的代码可以通过该代码的公共接口来进行调用,而不需要知道它是如何实现的。你在编写代码时可以定义哪些部分是其他代码可以使用的公共部分,以及哪些部分是你有权更改实现细节的私有部分。这是另一种减少你在脑海中记住项目内容数量的方法。
这里有一个需要说明的概念 “作用域(scope)”:代码所在的嵌套上下文有一组定义为 “in scope” 的名称。当阅读、编写和编译代码时,开发者和编译器需要知道特定位置的特定名称是否引用了变量、函数、结构体、枚举、模块、常量或者其他有意义的项。你可以创建作用域,以及改变哪些名称在作用域内还是作用域外。同一个作用域内不能拥有两个相同名称的项;可以使用一些工具来解决名称冲突。
Rust 有许多功能可以让你管理代码的组织,包括哪些内容可以被公开,哪些内容作为私有部分,以及程序每个作用域中的名字。这些功能。这有时被称为 “模块系统(the module system)”,包括:
1)关于Cargo更加高级的使用方法,会有另外一篇文章提到。
2)有一些语法作者这里可以没有显示的说明,不过也应当好理解
3)读者们应当去看官方的文档而非笔者的文章,笔者的文章仅供笔者的学习和包管理的入门,如果想要了解更多有关包管理的知识,请查阅使用包、Crate 和模块管理不断增长的项目 - Rust 程序设计语言 中文版 (rustwiki.org)
4)如果你发了此文章的纰漏或对文章有什么建议可以评论区或者私信联系作者
5)后序的对文章一些补充,会在杂项里说明
Cargo 有一个机制来确保任何人在任何时候重新构建代码,都会产生相同的结果:Cargo 只会使用你指定的依赖版本,除非你又手动指定了别的。例如,如果下周 rand
crate 的 0.8.4
版本出来了,它修复了一个重要的 bug,同时也含有一个会破坏代码运行的缺陷。为了处理这个问题,Rust 在你第一次运行 cargo build
时建立了 Cargo.lock 文件,我们现在可以在 guessing_game 目录找到它。
当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并写入 Cargo.lock 文件。当将来构建项目时,Cargo 会发现 Cargo.lock 已存在并使用其中指定的版本,而不是再次计算所有的版本。这使得你拥有了一个自动化的可重现的构建。换句话说,项目会持续使用 0.8.3
直到你显式地升级,多亏有了 Cargo.lock 文件。
换句话说,cargo在第一次构建的生成lock文件,之后的构建会从lock里查看版本,而不会再使用.toml中的版本即使你修改了Cargo.toml中creat指定的版本,除非你显示的升级。