当项目变大时,通常的做法是将代码重构为更小、更易于管理的单元,即模块或程序库。你还需要工具来为项目撰写文档,并说明它应该如何构建,以及相关的依赖库是什么。此外,为了支持开发人员可以与社区共享程序代码库的语言生态系统,采用某类在线注册服务是比较流行的做法。
Cargo
是能够帮助你处理上述所有事情的工具,crates.io
是托管程序库的主要位置。用Rust
编写的程序库被称为crate
、crates.io
托管它们供开发人员使用。通常。crate
有3个来源:本地目录、GitHub
之类的在线Git
代码库,或者像crates.io
这样的托管crate
注册服务。Cargo
支持上述所有来源的软件包。
它显示了一些我们可以使用的常用命令,以及附加标记的参数。我们使用子命令new
创建一个新的Cargo
项目
新建一个Cargo项目
使用 cargo new <name>
命令将会新建一个项目,并将name
用作项目目录名。我们可以在cargo
和任何子命令之间添加help
标签来获得与之有关的更多上下文信息,可以通过运行cargo help new
命令查看子命令new
的帮助文档,如下所示:
默认情况下,运行cargo new
命令会创建一个二进制项目。而创建程序库项目时必须使用—lib参数。让我们执行cargo new imgtool
命令,然后介绍一下它创建的目录结构:
$ tree
.
├── .git
├── .gitignore
├── Cargo.toml
└── src
└── main.rs
Cargo
创建了一些基础文件,Cargo.toml
和 src/main.rs
,其中的函数 main
主要用于输出 “Hello World!”
。对于二进制 crate
(可执行文件),Cargo
创建了一个文件 src/main.rs
;对于程序库 crate
,Cargo
会在 src
目录下创建文件 src/lib.rs
。
Cargo
还可以使用默认值为新项目初始化 Git
版本库,例如阻止将.gitignore
文件签入目 标目录,并在 Cargo.lock
文件中检查二进制 crate
,同时在程序库 crate
中忽略它。使用的默 认版本控制系统(Version Control System,VCS)是 Git
,可以通过将--vcs
标记参数传递给Cargo
(--vcs hg for mercurial
)来更改它。目前 Cargo
支持的版本控制系统包括 Git
、hg
(mercurial
)、pijul
(用 Rust
编写的版本控制系统)和 fossil
。如果我们希望修改默认行为,可以传递--vcs none
来只让 Cargo
在创建项目时不配置任何版本控制系统。
让我们看一下之前创建的 imgtool 项目对应的 Cargo.toml
文件。该文件定义了项目的元数据和依赖项,它也被称为项目的清单文件:
workspace = { members = ["imgtool"] }
[package]
name = "variables"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
这就是新项目最基本的 Cargo.toml
清单文件。它使用 TOML(Tom's Obvious Minimal Language)
配置文件格式,TOML
是由 Tom Preston-Werner
创建的配置文件格式。TOML
让人联想到标准的.ini
文件,但被添加了一种数据类型,这使它成为理想的配置文件格式, 并且比 YAML
或 JSON
格式更简单。我们暂时保留此文件的最少配置信息,并在后续添加 相关的内容。
Cargo 与依赖项
对于依赖其他程序库的项目,软件包管理器必须找到项目中所有直接依赖项和任何间接依赖项,然后编译,并将它们链接到项目。软件包管理器不仅是帮助用户解决依赖性的工具,还应该确保项目可预测和可重复地构建。
Cargo
是通过两个文件来管理 Rust
项目的:Cargo.toml
文件(之前介绍过)由开发人 员使用 semver(如 v1.3.*)编写依赖管理及其所需版本,以及一个名为 Cargo.lock
的锁文 件,它由 Cargo
在构建项目时生成,包含所有直接依赖项和任何间接依赖项的绝对版本(如 1.3.15)。此锁文件确保在二进制项目中能够重复构建。Cargo
通过引用此锁文件来最小化 它必须完成的工作,以便对项目进行任何进一步的更改。因此,建议使二进制项目在其版 本库中包含.lock
文件,而程序库项目是无状态的,不需要包含它。
可以使用 cargo update
命令更新依赖关系,这会更新项目的所有依赖项。为了更新单个 依赖,我们可以使用命令 cargo update -p <crate-name>
。如果希望更新单个软件包的某个版 本,Cargo
会确保更新 Cargo.lock
文件中与该软件包相关的部分,并保持其他软件包的版本不变。
Cargo
遵循语义版本控制系统,其中你的程序库将以major.minor.patch
格式指定。它们的含义如下。
Major
:只有在对项目进行新的重大更改时(包括错误修复)才会添加。Minor
:仅在以后兼容的方式添加新功能时才会添加。Patch
:仅在以后向后兼容的方式修复错误,并且未添加任何功能时才会添加。
:::info
例如:你可能希望在项目中引用序列化库 serde
。在编写本书时,serde
的最新版本是 1.0.85
,你可能只关心主版本号,因此在 Cargo.toml
中指定 serde=”1
“作为依赖关系(这将 转换为 semver
格式的 1.xx
),Cargo
将为你解决并在锁文件中将其修复为 1.0.85
。下次使用 cargo update
命令更新 Cargo.lock
时,此版本可能会升级到 1.xx
匹配的最新版本。如果你对 此并不在意,并且只想要最新版本的 crate
,那么可以使用“*
”指代版本,但这并不是推 荐的做法,因为它会影响构建的可重复性,例如你可能会引入一个与主版本有冲突的变更。 发布项目时使用“*
”声明依赖项版本号的做法也是被禁止的。
:::
为此,我们将了解`cargo`的构建命令,它主要用于编译、链接及构建我们的项目。此命令为你的项目执行以下操作。
- 如果你还没有
Cargo.lock
文件,将为你运行cargo updte
命令进行更新,并根据Cargo.toml
将确切的版本放入锁文件。 - 下载已在
Cargo.lock
中解析的依赖项。 - 构建这些依赖项。
- 构建项目并将其与依赖项链接。
默认情况下,Cargo
会在target/debug/
目录创建项目的调试版本,可以传递 --release
标记参数,在target/release
目录下为正式上线代码创建优化后的构建。调试版本提供了更短的构建时间,缩短了反馈循环,而正式版本的稍慢,因为编译器对源代码运行了更多的优化步骤。在开发过程中,你需要缩短修复-编译-检查的反馈时间。为此,可以使用cargo check
命令缩短编译时间。它基本上跳过了编译器的代码生成部分,只通过前端阶段运行代码,即 编译器的解析和语义分析。另一个命令是 cargo run
,它会执行双重任务。执行 Cargo
构建, 然后运行 target/debug/
目录下的程序。为了构建/运行
正式发布的版本,你可以使用 cargo run --release
命令。在我们的 imgtool/
目录下运行 cargo run
命令后,可得到以下输出结果:
使用Cargo执行测试
Cargo
还支持运行测试和基准评估。Cargo
运行测试。接下来将为一个程序库编写测试。为此, 让我们运行 cargo new myexponent --lib
命令来创建一个程序库:
% cargo new myexponent --lib
Created library `myexponent` package
% cat src/lib.rs
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
一个程序库类似于一个二进制项目,两者不同之处在于,我们得到的不是 src/main.rs
并将其中的 main
函数作为入口点,而是 src/lib.rs
,其中有一个简单的测试函数 it_works
, 并附有#[test]
注释。我们可以使用 cargo test
命令立即运行 it_works
函数,并查看它的结果:
现在,让我们尝试一下 Cargo
的测试驱动开发(Test Driven Development,TDD)。我 们将通过添加一个指数函数(pow
函数)来扩展此程序库,程序库的用户可以使用该函 数计算给定数字的指数。我们将为这个函数编写一个最初不够完善的测试,然后逐步对 它进行优化,直到能够正常运作。这是新的 src/lib.rs 文件,其中包含没有任何实现的 pow
函数:
fn pow(base: i64, exponent: usize) > i64 {
unimplemented!();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn minus_two_raised_three_is_minus_eight() {
assert_eq!(pow(-2, 3), -8);
}
}
现在不必担心细节,我们已经实现了一个 pow
函数,它将 i64
作为基数,将正指数的 类型指定为 usize
,并返回了一个已经转化为指数的数字。在“mod tests
”中,我们有一个 名为 minus_two_raised_three_is_minus_eight
的测试函数,它会执行单个断言。宏 assert_eq!
将会检查传递给它的两个值的相等性。如果左边的参数等于右边的参数,则断言通过;否 则抛出一个错误,编译器会提示测试失败。如果我们执行 cargo test
,pow
函数调用的单元 测试显然是失败的,因为我们有一个 unimplemented!()
宏会被调用:
简而言之,unimplemented!()
只是一个方便的宏,用来标记未完成的代码或者你希望稍 后实现的代码,但是在希望编译器不出现类型错误的情况下无论如何都要编译它。在编译 器内部,这会调用宏 panic!
并伴随提示信息“not yet implemented
”。它可以在你希望实现某 个特征的多种方法的情况下使用。例如,你开始实现某个方法,但是还没有打算完成该实 现的其他方法。在编译时,如果你只是提供一个空的函数体,那么将会得到未提供其他方 法实现的错误提示。对于这些方法,我们可以在其中放置一个unimplemented!()
宏,使其通 过类型检查器的校验从而顺利编译,并在运行时避免这些错误。我们将在后面介绍一些 具有类似功能的、更简便的宏。
现在,让我们快速地实现 pow 函数的一个有缺陷的版本来解决此问题,然后再试一次:
pub fn pow(base: i64, exponent: usize) -> i64 {
let mut res = 1;
if exponent == 0 {
return 1; }
for _ in 0..exponent {
res *= base as i64;
}
res }
运行cargo test
命令之后得到如下输出结果:
使用Cargo运行示例
为了让用户能够快速地使用你开发的软件包,最好提供能够引导用户使用它的代码示 例。Cargo
标准化了这种方式,这意味着你可以在项目根目录中添加一个包含一个或多个.rs
文件的 examples/
目录,其中的 main
函数展示了软件包的用法。
可以使用 cargo run --examples<file_name>
命令运行 examples/
目录下的代码,其中的文 件名不带.rs
扩展名。为了证实这一点,我们为 myexponent
库添加一个 examples/
目录,其 中包含一个名为 basic.rs
的文件:
use myexponent::pow;
fn main() {
println!("8 raised to 2 is {}", pow(8, 2));
}
在 examples/
目录下,我们从 myexponent
库导入了 pow
函数。以下是运行 cargo run --example basic
命令后的输出结果:
Cargo 工作区
随着时间的推移,你的项目可能会变得非常庞大,现在,你需要考虑是否将代码的通 用部分拆分成单独的程序库,以便管理复杂性。Cargo
的工作区(workspace
)可以帮你做 到这一点。工作区的概念是,它们允许你在可以共享相同的 Cargo.lock
文件和公共目录, 或者输出目录的目录下创建本地程序库。为了证明这一点,我将创建一个包含 Cargo 工作 区的新项目。工作区只是一个包含 Cargo.toml
文件的目录。它不包含任何[package
]部分, 但是其中有一个[workspace
]项。让我们新建一个名为 workspace_demo
的新目录,并按照如 下步骤添加一个 Cargo.toml
文件:
% mkdir workspace_demo
% cd workspace_demo && touch Cargo.toml
然后我们将[workspace]
项添加到Cargo.toml
文件中:
vim Cargo.toml
[workspace]
members = ["my_crate", "app"]
在[workspace]
项下,members
属性表示工作区目录中的程序库列表。在 workspace_demo
目录中,我们将创建两个程序库:一个是程序库 my_crate
,一个是调用 my_crate
库的二进 制程序 app
。
为了保持简洁,my_crate
中只包含一个公有的 API
,用于输出一条问候消息:
pub fn greet() {
println!("Hi from my_crate");
}
在我们的 app
程序中有 main
函数,它会调用 my_crate
程序库中的 greet
函数:
fn main() {
my_crate::greet();
}
不过,我们需要让 Cargo
识别 my_crate
中的依赖关系。由于 my_crate
是一个本地程序 库,我们需要在 app
的 Cargo.toml
文件中将其指定为路径依赖,如下所示:
[package]
name = "app"
version = "0.1.0"
authors = ["creativcoder"]
edition = "2021"
[dependencies]
my_crate = { path = "../my_crate" }
现在,当我们运行 cargo build
命令时,二进制文件将在 workspace_demo
目录下的 target
目录中生成。此外,我们可以在 workspace_demo
目录中添加多个本地程序库。现在,如果 我们想要通过 crates.io
添加第三方的依赖项,那么需要将它们添加到所有会调用它们的程 序中。不过,在 Cargo
构建过程中,Cargo
会确保在 Cargo.lock
文件中只有该依赖项的单一 版本。这可以确保不会重新构建或者重复出现第三方依赖项。