Rust学习笔记之非常好用的包管理器Cargo

包管理器

Rust 的 Cargo 应该算是众多包管理器当中非常好用的一个。如果接触过前端开发,对 npm/yarn 应该是不陌生的,Go 语言也有 go tool。这些包管理器用来下载正确的依赖库、编译和链接文件,还有管理项目等功能。

C++ 没有一个专用的包管理。C/C++ 一般用 GNU make 来构建项目。GNU make 是一个与语言无关的构建工具。GNU make 非常原始,既没有提供头文件的查找的功能,必须手动指明目录,也无法自动地进行依赖的下载。

幸运的是,Rust 的包管理器 Cargo 解决了这些问题,是一个非常好用的包管理工具。

模块(Modules)

任何 Rust 项目都有一个根模块。如果创建的是一个库,根模块就是 lib.rs 文件。如果是一个可执行的应用,那么根模块就是有 main 函数的文件。

嵌套模块

最简单的创建模块的方法就是 mod{}

// mod_within.rs

mod food {
  struct Cake;
  struct Smoothie;
  struct Pizza;
}

fn main() {
  let eatable = Cake;
}

如果这个时候进行编译,会发现报错了。
Rust学习笔记之非常好用的包管理器Cargo_第1张图片
Rust 真的这点上很赞,不单单是报错,连参考的解决方案都写好了。我们按照编译器提示的那样,增加一行代码。

// mod_within.rs

mod food {
  struct Cake;
  struct Smoothie;
  struct Pizza;
}

use food::Cake;

fn main() {
  let eatable = Cake;
}

再编译一下,结果发现…
Rust学习笔记之非常好用的包管理器Cargo_第2张图片

现在编译器说,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 。在前面这个代码里,根模块就是 main.rs。

相对导入:

  • self:从当前的模块中相对引入。大多数时候被用来再引入父模块中的子模块。
  • super:可以从父模块中引入对应的项。比如说测试模块可能就要引入父模块。

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 和 crate

项目越来越大的时候,通常的做法就是把代码重构成更小,更容易管理的模块或者库。完善的文档,构建方式,依赖管理也会变得非常的重要。Cargo 就是用来处理这些事情的,https://crates.io 托管着注册的 Rust 库,你可以找到一些有用的第三方库来加速开发进程。

通常来说,crate 可以来自于本地的文件夹、Git仓库(比如Github)、或者 crates.io。Cargo 对这些来源都给予了支持。

创建新项目

Cargo 默认会创建一个二进制项目。加上 --lib 则会生成一个库(library)项目。接下来,创建一个 imgtool 来看看 crate 的结构。

cargo new imgtool

Rust学习笔记之非常好用的包管理器Cargo_第3张图片

可以看到,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 遵循语义版本规则,版本号由三个部分构成 主版本号.次版本号.修订号:

  • 主版本号:有不兼容的API的修改;
  • 次版本号:向下兼容的功能性新增;
  • 修订号:向下兼容的问题修正;

构建

cargo build

会创建可执行文件在 target/debug/hello_cargo中。debug 的版本会加入一些用于调试的工具,并不是性能最好的。实际发布的时候,应该使用 --release 参数构建发布(release)版本。

也可以使用 cargo run 在一个命令中同时编译并运行生成的可执行文件。当然,也可以使用 cargo run --release 运行发布版本。

Rust学习笔记之非常好用的包管理器Cargo_第4张图片

Cargo 还提供一个叫 cargo check 命令,可以快速检查代码并确保可以编译,但不产生可执行文件。(通常 cargo checkcargo build 快得多,因为它省略了可执行文件生成的步骤)

测试

测试是软件开发中非常重要的一环,值得另外写一篇来讲,这里只是简单的提一下。新建一个库的项目,作为库项目,并不打包成可执行文件,可以发现原本的 main.rs 被 lib.rs 取代了。lib.rs 里面已经写好了一个测试。
Rust学习笔记之非常好用的包管理器Cargo_第5张图片

使用 cargo test 将运行这个这个测试。
Rust学习笔记之非常好用的包管理器Cargo_第6张图片

我们尝试使用 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 运行。
Rust学习笔记之非常好用的包管理器Cargo_第7张图片

工作区

项目再大的时候,可能就要考虑把公共部分抽离出来作为独立的 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 appcargo 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 下运行这个项目。

Rust学习笔记之非常好用的包管理器Cargo_第8张图片

Cargo 工具和命令

  • cargo-watch:前面提到了 cargo check 适合快速检查;可以用 cargo install cargo-watch,只要代码改变,自动运行 cargo check。
  • cargo-edit:自动化地添加依赖到 Cargo.toml。
  • cargo-deb:便于创建 Debian Linux 下 deb 包
  • cargo-outdated:显示过时的包

clippy

静态检查通过一些实践可以让代码保持高质量。可以使用 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 进行检查:
Rust学习笔记之非常好用的包管理器Cargo_第9张图片
可以看到,这里人家让你简化成 x 就行了。

小结

通过这篇,我们已经简略地看到了 Cargo 如何初始化、构建、运行和测试代码。 Cargo 还是非常好用,Visual Studio Code 里安装上 RLS 插件,写起来的感觉很不错。我第一次的时候就觉得这套工具设计得比golang人性化一点。

你可能感兴趣的:(Rust,rust,开发语言,后端)