一、项目
(一)项目是什么
项目也叫包,英文是package,其实就是个文件夹。
包名就是目录名。
一个包必须有一个Cargo.toml文件。
项目可以嵌套。
(二)如何创建项目
使用cargo new命令创建包。
(三)项目目录结构
项目目录结构如下
.
├── Cargo.lock
├── Cargo.toml
├── src/
│ ├── lib.rs
│ ├── main.rs
│ └── bin/
│ ├── named-executable.rs
│ ├── another-executable.rs
│ └── multi-file-executable/
│ ├── main.rs
│ └── some_module.rs
├── benches/
│ ├── large-input.rs
│ └── multi-file-bench/
│ ├── main.rs
│ └── bench_module.rs
├── examples/
│ ├── simple.rs
│ └── multi-file-example/
│ ├── main.rs
│ └── ex_module.rs
└── tests/
├── some-integration-tests.rs
└── multi-file-test/
├── main.rs
└── test_module.rs
这是使用Cargo创建的包目录结构,解释如下:
- Cargo.toml和Cargo.lock保存在包根目录下
- 源代码放在src目录下
- 默认库箱的根是src/lib.rs
- 默认二进制箱的根是src/main.rs
- 其它二进制箱的根放在src/bin/ 目录下
- 基准测试benchmark放在benches目录下
- 示例代码放在examples目录下
- 集成测试代码放在tests目录下
(四)配置项目
在Cargo.toml文件中的[package]下配置项目。
像下面这样:
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"
[package],是包片段,表明下面的语句用来配置包。
接下来的三行是包的名称、包的版本、使用的Rust版本。
二、箱
(一)箱是什么
箱,英文是crate。
箱就是要构建的目标文件,所以也可以叫target,构建对象。
箱的根,英文是crate root,它是一个源文件,Rust编译器以它为起始点构建crate。每个箱都必须有一个根。
(二)箱的分类
箱根据生成目的分为
1、库(Library)
库箱用于生成一个库。
一个项目最多只能有一个库箱。
默认库箱的根是src/lib.rs,默认库箱的名称跟项目名是一致的。
2、二进制(Binaries)
二进制箱可以生成可执行文件。
默认二进制箱的根是src/main.rs,默认二进制箱的名称跟项目名也是相同的。
除了默认二进制箱,一个项目还可以包含多个其他二进制箱,这些箱的根放在src/bin/ 目录下。
我们可以使用cargo run --bin <bin-name>
来运行指定的二进制箱。
3、示例(Examples)
示例箱在examples目录中。示例编译后的文件会存储在target/debug/examples目录下。
使用cargo build --example <example-name>
构建。
使用cargo run --example <example-name>
命令构建并运行。
使用cargo install --example <example-name>
来将示例编译出的可执行文件安装到默认的目录中。
cargo test命令默认会编译示例,以防止示例因为长久没运行,导致严重过期。
4、测试(Tests)
测试位于tests目录中。
使用cargo test命令编译运行。
5、基准性能(Benches)
基准性能位于benches目录下。
可以通过cargo bench命令来编译运行。
(三)包与箱的关系
一个包至少包含一个库箱或二进制箱,这些箱中最多一个库箱,但可以有任意数量的二进制箱。
示例、测试、基准性能则是可有可无的。
只有包含库箱的包,才能发布为依赖包;当添加依赖时,添加的也必然是包含库箱的包。也就是说,依赖包与库箱是一一对应的,所以可以把依赖包和库箱当成一回事。
如果使用cargo new proj创建包,它就包含一个默认二进制箱。src目录下会默认生成一个main.rs源文件,这是默认二进制箱的根。默认二进制箱的名字与包名相同。
如果使用cargo new --lib proj命令创建包,它就包含一个默认库箱。src目录下会默认生成一个lib.rs源文件,这是默认库箱的根。默认库箱的名字与包名相同。
如果把源文件放在src/bin目录下,一个包可以拥有多个二进制箱:src/bin下的每个文件都会编译成一个独立的二进制箱。src/bin目录下的箱的名字与包名不同,而与源文件的名字相同。
(四)配置箱
在 Cargo.toml中的 [lib]、[[bin]]、[[example]]、[[test]] 、[[bench]] 下配置箱。
大家可能会疑惑 [lib] 和 [[bin]] 的写法为何不一致,因为[[]]这种写法表示它是个数组,可以有多个,而[]写法表示只能有一个。
由于它们的配置内容都是相似的,因此我们以 [lib] 为例来说明相应的配置项:
[lib]
name = "foo" # 箱的名称
path = "src/lib.rs" # 箱的根
test = true # 能否被测试,默认是true
doctest = true # 文档测试是否开启,默认是true
bench = true # 基准测试是否开启
doc = true # 文档功能是否开启
plugin = false # 是否可以用于编译器插件(deprecated)
proc-macro = false # 是否是过程宏类型
harness = true # 是否使用libtest harness
edition = "2015" # 使用的Rust版本
crate-type = ["lib"] # 箱类型
required-features = [] # 构建所需的Features
1、name
对于库和默认二进制( src/main.rs ),默认名称是项目的名称( package.name)。
对于其它箱,默认是目录或文件名。
除了 [lib] 外,name字段对于其他箱都是必须的。
2、proc-macro
该字段的使用方式在过程宏章节有详细的介绍。
3、edition
使用的Rust 版本。
如果没有设置,则默认使用 [package] 中配置的package.edition,通常来说,这个字段不应该单独设置,只有在一些特殊场景中才可能用到:例如将一个大型项目逐步升级。
4、crate-type
该字段定义了箱类型,主要表示这个箱是可执行的,还是不可执行的。
它是一个数组,因此为同一个对象指定多个箱类型。
需要注意的是,只有库和示例可以修改箱类型,因为其他的二进制、测试、基准性能只能是bin这个箱类型。
可用的选项包括bin、lib、rlib、dylib、cdylib、staticlib、proc-macro
5、required-features
所需的 features 列表。
该字段只对 [[bin]]、 [[bench]]、 [[test]] 、[[example]] 有效,对于 [lib] 没有任何效果。
[features]
postgres = []
sqlite = []
tools = []
[[bin]]
name = "my-pg-tool"
required-features = ["postgres", "tools"]
例子
以下是一种自定义配置:
[lib]
crate-type = ["cdylib"]
bench = false
[[bin]]
name = "cool-tool"
test = false
bench = false
[[bin]]
name = "frobnicator"
required-features = ["frobnicate"]
[[example]]
name = "foo"
crate-type = ["staticlib"]
三、模块
模块,英文叫module。
rust模块就是命名空间。
(一)声明模块
使用mod关键字来声明模块。
mod hello {
pub fn say_hello() {
println!("hello world");
}
}
模块内的项默认为private,外部不可见,如果要外部可见需要加pub。
模块可以嵌套,形成模块树(module tree)。
mod nation {
mod government {
fn govern() {}
}
mod congress {
fn legislate() {}
}
mod court {
fn judicial() {}
}
}
每个箱都是一个模块树。箱的根之所以叫做箱的根,是因为箱的根源文件为模块树创建了一个名为 crate 的根模块。
模块树如下所示:
crate
└──nation
├──government
│ └──govern
├──congress
│ └──legislate
└──court
└──judicial
(二)使用模块
1.模块的路径
要在模块树中找到一个项,就要使用路径,就像在文件系统使用路径一样。为了调用一个函数,我们需要知道它的路径。
路径有两种形式:
- 绝对路径(absolute path),是以箱的根模块开头的全路径。引用外部箱代码时,是以外部箱名开头的绝对路径;引用当前箱代码时,不是以箱名,而是以根模块名crate开头。
- 相对路径(relative path),是从当前所在模块开始,以self、super开头的路径。
路径以双冒号(::)为分割符。
例如:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
self::front_of_house::hosting::add_to_waitlist();
}
(1)使用以 super 开头的相对路径
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
(2)使用外部箱
必须先添加依赖包,依赖包就是外部箱所在的包。
在Cargo.toml中的[dependencies]下添加外部箱所在的包。
比如,
[dependencies]
rand = "0.8.5"
Cargo要从 crates.io 下载 rand 和其依赖。
这样就可以使用绝对路径使用外部箱了。
例子:
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng().gen_range(1..101);
}
(3)std
std也是外部箱。因为std随Rust语言一同分发,无需修改 Cargo.toml 来引入 std。
比如,
let mut guess = String::new();
std::io::stdin().read_line(&mut guess).expect("failed readline");
2.use语句
无论是使用绝对路径还是相对路径都不方便,我们可以使用 use 关键字创建一个短路径。
(1)use关键字将标识符引入当前模块下
实例
mod nation {
pub mod government {
pub fn govern() {}
}
}
use crate::nation::government::govern;
fn main() {
govern();
}
use关键字把govern标识符导入到了当前的模块下,可以直接使用。
(2)可以使用use as为标识符添加别名:
实例
mod nation {
pub mod government {
pub fn govern() {}
}
pub fn govern() {}
}
use crate::nation::government::govern;
use crate::nation::govern as nation_govern;
fn main() {
nation_govern();
govern();
}
这里有两个govern函数,一个是nation下的,一个是government下的,我们用as将nation下的取别名nation_govern。这样两个govern函数就可以同时使用了。
(3)use关键字可以与pub关键字配合使用:
实例
mod nation {
pub mod government {
pub fn govern() {}
}
pub use government::govern;
}
fn main() {
nation::govern();
}
(4)使用大括号引入相同模块的多个子模块,可以显著减少 use 语句的数量
比如,
use std::{cmp::Ordering, io};
(5)使用通配符*引入所有子模块
例子
use std::collections::*;
将 std::collections 中所有公有项引入当前模块
(三)将模块分割进不同文件
到目前为止,都是在一个文件中定义多个模块。当模块变得更大时,你可能想要将它们移动到单独的文件中,从而使代码更容易阅读。
例如,
文件名: src/lib.rs
mod front_of_house; //声明front_of_house模块,其内容将位于src/front_of_house.rs中
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
文件名: src/front_of_house.rs
pub mod hosting {
pub fn add_to_waitlist() {}
}
在src/front_of_house.rs中定义front_of_house模块。
在mod front_of_house后使用分号,而不是代码块,表示在文件中定义模块。Rust会在与模块同名的文件中查找模块的代码。
继续重构我们例子,将hosting模块也提取到其自己的文件中。
文件名: src/front_of_house.rs
pub mod hosting;
创建一个src/front_of_house目录,在src/front_of_house/hosting.rs文件中定义hosting模块:
文件名: src/front_of_house/hosting.rs
pub fn add_to_waitlist() {}
四、工作空间
工作空间,英文是workspace。
随着项目的深入,库箱持续增大,而你希望将其拆分成多个库箱。但是因为一个项目最多只能包含一个库箱,所以你必须将一个项目拆分成多个项目。此时,Cargo提供了一个叫 工作空间(workspace)的功能。
工作空间是一个目录,其中包含一系列项目,它们共享同样的Cargo.lock和输出目录。
我们以一个例子说明工作空间的用法:
创建一个工作空间,工作空间中有一个二进制项目和两个库项目。二进制会依赖另两个项目。
一个库会提供add_one方法,另一个库会提供add_two方法。
(一)创建一个工作空间。
1、新建工作空间目录add
$ mkdir add
$ cd add
2、在add目录中,创建Cargo.toml文件。
这个Cargo.toml文件配置了整个工作空间。它不会包含 [package],而是包含 [workspace]。
为工作空间增加成员,如下会加入二进制项目adder:
文件名: Cargo.toml
[workspace]
members = [
"adder",
]
(二)在工作空间中创建项目adder
1、在add目录运行cargo new新建adder项目
$ cargo new adder
到此为止,可以运行cargo build来构建工作空间。
add目录中应该看起来像这样:
├──Cargo.lock
├──Cargo.toml
├──adder
│ ├──Cargo.toml
│ └──src
│ └──main.rs
└──target
工作空间目录有一个target目录;adder并没有自己的target目录。即使进入adder目录运行cargo build,构建结果也位于add/target而不是add/adder/target。
2、修改工作空间的Cargo.toml,添加add_one成员:
文件名: Cargo.toml
[workspace]
members = [
"adder",
"add-one",
]
(三)在工作空间中创建项目add_one
1、在add目录运行cargo new --lib新建add_one项目
$ cargo new add_one --lib
现在add目录应该有如下目录和文件:
├──Cargo.lock
├──Cargo.toml
├──add_one
│ ├──Cargo.toml
│ └──src
│ └──lib.rs
├──adder
│ ├──Cargo.toml
│ └──src
│ └──main.rs
└──target
2、在add_one/src/lib.rs文件中,增加一个add_one函数:
文件名: add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
(四)为adder项目添加依赖add_one
1、在adder/Cargo.toml文件中增加add_one依赖:
文件名: adder/Cargo.toml
[dependencies]
add_one = { path = "../add_one" }
2、在adder中使用库函数add_one。
打开adder/src/main.rs在顶部增加一行use add_one。
接着修改main函数来调用add_one函数。
文件名: adder/src/main.rs
use add_one;
fn main() {
let num = 10;
println!("Hello, world! {} plus one is {}!", num, add_one::add_one(num));
}
3、在add目录中运行cargo build来构建工作空间
$ cargo build
4、通过cargo run -p adder,运行adder
$ cargo run -p adder
这会运行adder/src/main.rs中的代码,其依赖add_one
(五)在工作空间中依赖外部箱
工作空间只在根目录有一个Cargo.lock,而不是在每一个项目目录都有Cargo.lock。这确保了所有的项目都使用相同版本的依赖。如果在Cargo.toml和add_one/Cargo.toml中都增加rand依赖,则Cargo会将其都解析为同一版本并记录到唯一的Cargo.lock中。
1、在add_one/Cargo.toml中的 [dependencies] 部分增加rand:
文件名: add_one/Cargo.toml
[dependencies]
rand = "0.5.5"
现在就可以在add_one/src/lib.rs中增加use rand; 了,
2、在add目录运行cargo build构建整个工作空间,就会引入并编译rand:
$ cargo build
任何依赖rand的项目,都要单独在他们的Cargo.toml中添加rand。例如,如果在adder/src/main.rs中增加use rand;,会得到一个错误,为了修复错误,就要修改adder/Cargo.toml,添加rand依赖。
构建adder会将rand加入到Cargo.lock中adder的依赖列表中,但是这并不会下载rand的额外副本。Cargo确保了工作空间任何项目都使用相同版本的rand。
(六)向 crates.io发布工作空间中的项目
工作空间中的每一个项目需要单独发布。cargo publish命令并没有 --all或者 -p参数,所以必须进入每一个项目的目录并运行cargo publish来发布工作空间中的每一个项目。
现在向工作空间增加add-two来练习!
五、总结
组织结构图如下