Rust 的 Cargo 应该算是众多包管理器当中非常好用的一个。如果接触过前端开发,对 npm/yarn 应该是不陌生的,Go 语言也有 go tool。这些包管理器用来下载正确的依赖库、编译和链接文件,还有管理项目等功能。
C++ 没有一个专用的包管理。C/C++ 一般用 GNU make 来构建项目。GNU make 是一个与语言无关的构建工具。GNU make 非常原始,既没有提供头文件的查找的功能,必须手动指明目录,也无法自动地进行依赖的下载。
幸运的是,Rust 的包管理器 Cargo 解决了这些问题,是一个非常好用的包管理工具。
任何 Rust 项目都有一个根模块。如果创建的是一个库,根模块就是 lib.rs 文件。如果是一个可执行的应用,那么根模块就是有 main
函数的文件。
最简单的创建模块的方法就是 mod{}
。
// mod_within.rs
mod food {
struct Cake;
struct Smoothie;
struct Pizza;
}
fn main() {
let eatable = Cake;
}
如果这个时候进行编译,会发现报错了。
Rust 真的这点上很赞,不单单是报错,连参考的解决方案都写好了。我们按照编译器提示的那样,增加一行代码。
// mod_within.rs
mod food {
struct Cake;
struct Smoothie;
struct Pizza;
}
use food::Cake;
fn main() {
let eatable = Cake;
}
现在编译器说,Cake
是私有的。现在再修改一下代码,在 Cake
的声明前加上 pub
:
// mod_within.rs
mod food {
pub struct Cake;
struct Smoothie;
struct Pizza;
}
use food::Cake;
fn main() {
let eatable = Cake;
}
现在可以编译了。可以发现总共就两步,一步公开,一步引入。没有公开 pub
,默认是私有的,外部是不能调用的。
模块也可以是一个文件,把它放在与 mian.rs 同一文件夹下,然后在 main.rs 中引入该模块。我们创建一个文件夹,顺便创建两个文件。
控制台命令如下(用鼠标完成下面的工作也是一个不错的选择):
mkdir modules_demo && cd modules_demo
touch foo.rs && touch main.rs
tree .
.
├── foo.rs
└── main.rs
接下来,在创建好的 foo.rs 中提供一个结构体,并实现一个方法。注意一下,pub
的使用。
// modules_demo/foo.rs
pub struct Bar;
impl Bar {
pub fn init() {
println!("Bar type initialized");
}
}
接下来,在 main.rs
中使用这个模块。
// modules_demo/main.rs
mod foo;
use crate::foo::Bar;
fn main() {
let _bar = Bar::init();
}
我们定义模块,foo
,然后通过 mod foo
引入。之后,使用 crate::foo::Bar
。注意到这个前缀 crate
。
绝对导入:
相对导入:
crate 可以理解为项目,是一个完整的编译单元;crate 内部则使用 mod,这个类似C++的语言的命名空间 namespace。
还可以创建文件夹来表示一个模块。这种方式下还可以继续创建子文件夹或文件实现层次关系。我们在上述基础上创建一个文件夹 foo,文件夹结构如下:
modules_demo
├── foo
│ └── bar.rs
├── foo.rs
└── main.rs
在 bar.rs 中加入下面的代码:
// foo/bar.rs
pub struct Bar;
impl Bar {
pub fn hello() {
println!("Hello from Bar!");
}
}
这里声明了一个单元结构体(unit struct)联系到了 hello
上。我们会在 main.rs 中使用这个API。接下来,将 foo.rs 修改一下:
// modules_demo/foo.rs
mod bar;
pub use self::bar::Bar;
pub fn do_foo() {
println!("Hi from foo!");
}
我们使用了 mod
声明了模块 bar
。接下来,从模块 bar 中重新导出 Bar
。重新导出适合导入隐藏在嵌套子模块中的项。可以看到,pub use
指明重新导出的 Bar
并不是在这里实现的。pub use
就是把其它地方的元素当作模块的直接成员公开出去。有了这个机制,就可以轻松做好接口和实现的分离。
最后,在 main.rs 中使用:
// modules_demo/main.rs
mod foo;
use foo::Bar;
fn main() {
foo::do_foo();
Bar::hello();
}
// Hi from foo!
// Hello from Bar!
项目越来越大的时候,通常的做法就是把代码重构成更小,更容易管理的模块或者库。完善的文档,构建方式,依赖管理也会变得非常的重要。Cargo 就是用来处理这些事情的,https://crates.io 托管着注册的 Rust 库,你可以找到一些有用的第三方库来加速开发进程。
通常来说,crate 可以来自于本地的文件夹、Git仓库(比如Github)、或者 crates.io。Cargo 对这些来源都给予了支持。
Cargo 默认会创建一个二进制项目。加上 --lib
则会生成一个库(library)项目。接下来,创建一个 imgtool 来看看 crate 的结构。
cargo new imgtool
可以看到,Cargo 创建了1个文件夹,总共两个文件。与此同时,Cargo 其实还创建了 git 仓库,还有一个隐藏文件 .gitignore,里面已经写着 /target,会自动把生成的二进制文件过滤掉,不进入git仓库。
目前主流的VCS(Version Control System,版本控制系统)主要就是git,但是有用户使用的是 hg(mercurial),还有比如pijul(用Rust写的)、fossil。要改变版本控制系统,只需要传入 --vcs
加上相应的工具即可。
main.rs
中已经写好了打印 Hello, world! 的样例;接下来,看看 Cargo.toml:
[package]
name = "imgtool"
version = "0.1.0"
authors = ["testuser "]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
基本的作者和项目信息,下面还有一个依赖。edition 主要是2015和2018,创建项目的时候可以通过--edition
指定。不过,现在创建项目肯定是要使用2018啦,所以默认就行了。顺便提一下,Toml 配置文件并不是 Rust 独有的,是由 Tom Preston-Werner 创建的标准。
Cargo 管理依赖主要是两个文件,一个是就是刚刚看到的 Cargo.toml。如果添加依赖后,还会有一个 Cargo.lock。Cargo.toml 确定了需要那些依赖包,由于依赖包可能会随着版本的更新而变得不兼容。所以,需要使用 Cargo.lock 明确地锁定版本,防止日后构造的时候在依赖包版本上出问题。
依赖包的更新可以使用 cargo update
命令;如果只是想更新某一个包,可以用 cargo update -p
指定包的名称。
Cargo 遵循语义版本规则,版本号由三个部分构成 主版本号.次版本号.修订号:
cargo build
会创建可执行文件在 target/debug/hello_cargo
中。debug 的版本会加入一些用于调试的工具,并不是性能最好的。实际发布的时候,应该使用 --release
参数构建发布(release)版本。
也可以使用 cargo run
在一个命令中同时编译并运行生成的可执行文件。当然,也可以使用 cargo run --release
运行发布版本。
Cargo 还提供一个叫 cargo check
命令,可以快速检查代码并确保可以编译,但不产生可执行文件。(通常 cargo check
比 cargo build
快得多,因为它省略了可执行文件生成的步骤)
测试是软件开发中非常重要的一环,值得另外写一篇来讲,这里只是简单的提一下。新建一个库的项目,作为库项目,并不打包成可执行文件,可以发现原本的 main.rs 被 lib.rs 取代了。lib.rs 里面已经写好了一个测试。
我们尝试使用 TDD (Test Driven Development,测试驱动开发)的开发理念来开发一个初级版本的指数函数。简单来讲,先写测试,然后运行测试(必定失败),再写代码通过这个测试,接着继续写一个测试,运行测试失败,再写代码…构成一个测试—代码—测试 的闭环。通过不断地编写测试,实现一种增量的开发。
// src/lib.rs
pub fn pow(base: i64, exponent: usize) -> i64 {
unimplemented!();
}
#[cfg(test)]
mod tests {
use super::pow;
#[test]
fn minus_two_raised_three_is_minus_eight() {
assert_eq!(pow(-2, 3), -8);
}
}
这里我们完全没有编写函数功能,但写了第一个测试,如果函数参数是 -2和3,期望结果是-8。现在运行测试,显然失败。
接下来,修复这个测试:
// src/lib.rs
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 实践中建议增加 examples 文件夹,里面可以放一个或多个含 main
函数的文件,用来展示用法。因此新建一个examples目录,并添加basic.rs 文件:
use myexponent::pow;
fn main() {
println!("8 raised to 2 is {}", pow(8, 2));
}
可以通过 cargo run --example basic
运行。
项目再大的时候,可能就要考虑把公共部分抽离出来作为独立的 Crate 去管理这个复杂的应用。工作区(workspace)的概念就是你可以把 crate 放在文件夹,然后共享 Cargo.toml。
新建一个文件夹,
mkdir workspace_demo
cd workspace_demo && touch cargo.toml
之后在 cargo.toml 中写入:
# workspace_demo/Cargo.toml
[workspace]
members = ["my_crate", "app"]
members
就是在 workspace 之下的 crate 列表。接下来使用 cargo new app
和 cargo new my_crate --lib
创建两个 crate。然后在 my_crate 中添加一个方法:
// workspace_demo/my_crate/src/lib.rs
pub fn greet() {
println!("Hi from my_crate");
}
接着在 app 里使用这个方法:
// workspace_demo/app/src/main.rs
fn main() {
my_crate::greet();
}
我们需要让 Cargo 知道 my_crate 依赖。my_crate 作为本地的crate,需要特别在 Cargo.toml 指定依赖路径:
# workspace_demo/app/Cargo.toml
# ...
[dependencies]
my_crate = { path = "../my_crate" }
接下来,就可以在 workspace_demo 下运行这个项目。
cargo check
适合快速检查;可以用 cargo install cargo-watch
,只要代码改变,自动运行 cargo check。静态检查通过一些实践可以让代码保持高质量。可以使用 rustup component add clippy
添加 clippy。
比如在前面 myexponent 的 src/lib.rs中,添加两行可以进行优化的代码:
// src/lib.rs
pub fn pow(base: i64, exponent: usize) -> i64 {
Dummy code for clippy demo
let x = true;
if x == true {}
/
let mut res = 1;
if exponent == 0 {
return 1;
}
for _ in 0..exponent {
res *= base as i64;
}
res
}
// . ..
使用 cargo clippy 进行检查:
可以看到,这里人家让你简化成 x
就行了。
通过这篇,我们已经简略地看到了 Cargo 如何初始化、构建、运行和测试代码。 Cargo 还是非常好用,Visual Studio Code 里安装上 RLS 插件,写起来的感觉很不错。我第一次的时候就觉得这套工具设计得比golang人性化一点。