掘金 后端 ( ) • 2024-04-27 09:33

本教程环境

系统:MacOS

Rust 版本:1.77.2

crate

Rust 程序由 crate 组成。在 Rust 中,crate 是最小的编译单元。 crate 可以分为:

  • 二进制 crate:可编译为一个可执行文件,是可以运行的程序。二进制的 crate 包含一个 main 函数作为程序的入口。
  • 库 crate:可编译为 Rust 代码库,其目的是允许其他程序通过外部引用来使用其中的功能。库 crate 不包含 main 函数。

可使用 cargo build --verbose 可以看到构建过程中 Cargo 正在运行的命令。

在使用 Cargo 时候,可以在 Cargo.toml 文件的 dependencies 中指定引入项目的库 crate 的版本。运行 cargo build 时,Cargo 会从 crates.io 中下载这些 crate 的指定版本的源代码。它会读取这些 crate 的 Cargo.toml 文件,并下载它们的依赖项,并递归的进行。所有这些依赖关系的集合,会告诉 Cargo 关于要构建什么 crate 以及按什么顺序构建,这叫做该 crate 的依赖图。

此时,有了源代码,Cargo 就会编译所有的 crate。它会为项目依赖图中的每个 crate 都运行一次 rustc。 在编译库的时候,会使用 --crate-type lib 选项。会告诉 rustc 不用寻找 main 函数,而是生成一个 .rlib 文件,其中包含已编译代码,可用于创建二进制文件和其他的 .rlib 文件。 在编译二进制程序时,Cargo 会使用 --crate-type bin,结果是目标平台的二进制可执行文件。

对于每一个 rustc 命令,Cargo 都会传入 --extern 选型,给出 crate 将使用的每个库的文件名。这样,当 rustc 看到一行代码(例如 use image::png::PNGEncoder)时,就可以确定 image 是一个 crate 的名称。Cargo 知道在哪里可以找到磁盘上已编译的 crate。Rust 编译器需要访问这些 .rlib 文件,因为它们包含库的已编译代码。Rust 会将代码静态链接到最终的可执行文件中。.rlib 包含一些类型信息,这样 Rust 就可检查我们在代码中使用的库特性是否确实存在于 crate 中,以及我们是否正确使用了它们。.rlib 文件中还包含此 crate 的公共内联函数、泛型和宏这三者的副本,在 Rust 知道如何使用它们之前,这些特性无法完全编译为机器码。

cargo build 支持很多选项。最常用的是 cargo build --release,会生成优化过的程序。

版本

Cargo.toml 文件中 [package] 部分会使用 edition 来指定 Rust 的版本。

edition = "2021"

Rust 承诺编译器始终接受该语言的所有现存版本,并且程序可以自由混用以不同版本编写的 crate。2015 版的 crate 甚至可以依赖 2021 版的 crate。crate 的版本只影响其源代码的解释方式,编译代码时,版本的差异已然消失。当想在自己的代码中使用新的语言特性时,只需要改版本就可以了。 如果有一个旧版本的 Rust 编写的 crate,则 cargo fix 能帮助将代码升级到新版本。

模块

模块是关于项目内代码组织的。扮演着 Rust 命名空间的角色,是构成 Rust 程序或库的函数、类型、常量等的容器。 模块是一组语法项的集合,这些语法项具有命名的特性。需要使用 mod 关键词,默认为私有的。 可以使用 pub 关键字使某个语法项声明为公共的。

mod spores {
    use cells::{Cell, Gene};
    pub struct Spore {
       // ...
    }

    pub fn produce_spore(factory: &mut Sporanium) -> Spore {
        // ...
    }

    pub(crate) fn genes(spore: &Spore) -> Vec<Gene> {
        // ...
    }

    fn recombine(parent: &mut Cell) {
        // ...
    }
}

pub(crate) 表示在这个 crate 中的任何地方都能使用,但不会作为外部接口的一部分公开,不会被其他 crate 使用。 未标记为 pub 的内容都是私有的,只能在定义它的模块及其任意子模块中使用。

嵌套模块

模块可以嵌套。如果希望嵌套模块中的语法项对其他 crate 可见,需要将它和它所在的模块标记为 pubpub(super) 表示语法项只对其父模块可见。 pub(in <path>) 让语法项在特定的父模块及其后代中可见。这对于深度嵌套的模块有用。

mod plant_structures {
	pub mod roots {
		pub mod products {
			pub(in crate::plant_structures::roots) struct Cytokinin {
				// ...
			}
		}
		use products::Cytokinin; // 正确: 在 roots 模块可见
	}
	use roots::products::Cytokinin; // 错误: 只在 roots 模块中可见
}
use plant_structures::roots::procucts::Cytokinin; // 错误

单独文件中的模块

模块还可以这样写:

mod spores;

然后将 spores 模块的主体代码,也就是花括号里的,保存到一个单独的名为 spores.rs 的文件中。 模块可以有自己的目录。当 Rust 看到 mod spores; 时,会同时检查 spores.rsspores/mod.rs,如果这两个文件都存在或都不存在,就会报错。如果 spores 模块还有子模块,那么考虑使用 spores/mod.rs 的方式。

fern_sim/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── spores.rs
    └── plant_structures/
        ├── mod.rs
        ├── leaves.rs
        ├── roots.rs
        └── stems.rs”

路径与导入

:: 运算符用于访问模块中的各项特性。项目中任何位置的代码都可通过写出其路径来引用:

if s1 > s2 {
	std::mem::swap(&mut s1, &mut s2);
}

或者使用 use 将这些特性导入到当前模块中:

use std::mem;
if s1 > s2 {
	mem::swap(&mut s1, &mut s2);
}

可一次性导入多个名称:

use std::collections::{HashMap, HashSet};
use std::fs::{self, File};
use std::io::prelude::*; // 导入所有语法项目

可使用 as 在导入的时候重新命名。

use std::io::Result as IOResult;

模块不能自动访问其父模块的内容。例如 proteins 模块的 proteins/mod.rs文件有如下代码:

// proteins/mod.rs
pub enum AminoAcid { ... }
pub mod synthesis;

那么 synthesis 子模块中不能直接使用 AminoAcid 类型。需要使用 super 显式导入。 默认情况下,路径是相对于当前模块的。self 也是当前模块的同义词。 关键字 supercrate 在路径中有着特殊的含义:super 指父模块,crate 指当前模块所在的 crate。使用 crate 根路径而不是当前模块的路径可以更容易地在项目中移动代码,因为如果当前模块的路径发生了变化,不会破坏任何导入。 子模块可使用 use super::* 访问父模块中的私有语法项。 Rust 中有一种特殊路径,称为绝对路径,该路径以 :: 开头。

标准库预导入

标准库 std 会自动链接到每个项目。意味着可以使用 use std::whatever,或者就按名称引用 std 中的语法项,例如 std::mem::swap()。 还有一些特别便捷的名称(Vec 和 Result)会包含在标准库预导入中并自动导入。Rust 的行为就好像每个模块都用以下导入语句开头一样:

use std::prelude::v1::*;

公开结构体字段

结构体声明为 pub 时,其字段还是私有的。要暴露其字段的话,其字段也需要声明为 pub

pub struct Fern {
	pub roots: RootSet,
	pub stems: StemSet
}

创建一个库项目

使用 cargo new --lib <项目名>。 创建的项目没有 src/main.rs 文件,而是创建一个 src/lib.rs 文件。不需要要更改 Cargo.toml 中的任何内容。此时会让 Cargo 保持默认行为。默认设定下,cargo build 会查看源目录中的文件并根据文件名确定要构建的内容。当它发现存在文件 src/lib.rs 时,就知道要构建一个库。 src/lib.rs 中的代码构成了库的根模块。其他使用这个库的 crate 只能访问这个根模块的公共语法项。

src/bin 目录

也可将程序和库放在一个 crate 中。Cargo 本身就是用这样的方式编写的。它的大部分代码在一个 Rust 库中。使用的 cargo 命令行程序只是一个很薄的包装程序,它会调用库来完成所有繁重的工作。库和命令行程序都位于同一个源代码存储库中。 可以将需要运行的程序放置在 src/bin 目录下。例如取名为 efern.rs,里面包含了 main 函数用来运行程序。 当执行 cargo build 时,会编译库和可执行程序。 运行的时候执行 cargo run --bin efern 即可。 如果程序比较大,可以在 src/bin 下创建目录,然后这个目录里定义 main.rs 文件作为可执行程序的入口文件。 src/bin 适合小型的项目使用。 如果定义了一个库,然后在一个大型项目中使用这个库。那么可以将这个库放置在完全独立的目录中,然后在 Cargo.toml 中将 fern_sim 列为依赖项。

属性

属性是 Rust 的通用语法,用于向编译器提供各种指令和建议。 如果收到了 #[warn(non_camel_case_type)] 警告。可以通过在此类型上添加#[allow(non_camel_case_type)] 属性来禁用这条警告。

#[allow(non_camel_case_types)]
pub struct git_revspec {
	...
}

可使用 #[cfg] 的属性编写条件编译。

// Android 时候才构建项目中包含此模块
#[cfg(target_os = "android)]
mod mobile;

下面是常用的 #[cfg] 选项: 截屏2024-01-29 17.47.01.png 可以使用 #[inline] 属性对函数的内联展开进行微观管理。通常会把这种优化留给编译器。 Rust 默认 #[inline(always)] 要求函数在每个调用点内联展开。#[inline(never)] 要求函数永不内联。 #[cfg]#[allow] 可附着在整个模块上对其中的所有内容生效。#[test]#[inline] 则必须附着到耽搁语法项。 要将属性附着到整个 crate,需要将其添加到 **main.rs****lib.rs** 文件顶部。并写成 **#!**

#![allow(non_camel_case_types)]

#![feature] 属性可启用 Rust 语言和库中的不稳定特性。

测试和文档

Rust 中内置了一个简单的单元测试框架。测试是使用了 #[test] 属性的普通函数。 cargo test 会运行项目中的所有测试。 无论你的 crate 是可执行文件还是库,都可以通过将参数传给 Cargo 来运行特定测试:cargo test match 会运行名称中包含 match 的所有测试。 测试经常使用 assert!assert_eq! 两个 Rust 标准库中的宏。它们两个会包含在发布构建中。如果仅仅在调试中使用断言,可以使用 debug_assert!debug_assert_eq!。 使用 #[should_panic] 属性来测试各种出错情况。

#[test]
#[allow(unconditional_painc, unused_must_use)]
#[should_panic(expected="divide by zero")]
fn test_divide_by_zero_error() {
	1 / 0;
}

可以从测试中返回 Result<(), E>。只要错误类型实现了 Debug 特型,就可以简单的使用 ? 抛弃 Ok 变体以返回 Result

use std::num::ParseIntError;

// 如果 1024 是一个有效的数值,那么本测试就会通过
#[test]
fn explicit_radix() -> Result<(), ParseIntError> {
	i32::from_str_radix("1024", 10)?;
	Ok(())
}

标记为 #[test] 的函数是有条件编译的。cargo buildcargo build --release 会跳过测试代码。但是当运行 cargo test 时,Cargo 会分开两次来构建你的程序:一次是以普通的方式,一次带着你的测试和已启用的测试工具。意味着单元测试可以与它们所测试的代码一起使用,按需访问内部实现细节,而且没有运行期成本。但是,可能会导致一些警告。 所以,如果测试变的庞大需要支撑性代码时,应该将它们放在 tests 模块中,并使用 #[cfg] 属性声明整个模块仅用于测试:

#[cfg(test)]
mod tests {
	fn roughly_equal(a: f64, b: f64) -> bool {
        (a - b).abs() < 1e-6
    }

    #[test]
    fn trig_works() {
        use std::f64::consts::PI;
        assert!(roughly_equal(PI.sin(), 0.0));
    }
}

Rust 测试工具默认会使用多个线程同时运行好几个测试。要禁用这个功能,运行单个测试 cargo test testname 或运行 cargo test --test-threads 1。 通常测试工具只会显示失败测试的输出。如果要展示成功测试的输出,需要运行 cargo test -- --nocapture

集成测试

集成测试就是使用 .rs 文件,位于 src 目录同层级的 tests 目录中。 cargo test 会运行单元测试和集成测试。要运行某个特定文件(如 tests/unfurl.rs)中的集成测试,请使用 cargo test --test unfurl 命令。

文档

cargo doc 会创建 HTML 文档:

cargo doc --no-deps --open
  • --no-deps 只会为本 crate 生成文档,不会为依赖的所有 crate 生成文档。
  • --open 选项会要求 Cargo 随后在浏览器中打开此文档。

/// 表示文档型注释。它和 #[doc] 属性一致。 //!#![doc] 通常是模块或 crate 上使用。 可以添加别名以便使用内置搜索功能更轻松的查找内容。

#[doc(alias = "route")]
pub struct VascularPath {
	// ...
}

在此 crate 文档中搜索 pathroute 都能找到 VasularPath。 可以在文档中包含外部文件。例如,存储库的 README.md 文件中包含与准备用作 crate 的顶层文档相同的文本,那么可以将下面的这句话放在 lib.rsmain.rs 的顶部。

#![doc = incluce_str!("../README.md")]

文档测试

在 Rust 库 crate 中运行测试时,Rust 会检查文档中出现的所有代码是否真能如预期般工作。Rust 会获取文档型注释中出现的每个代码块,然后将其编译为单独的可执行包,再与你的库链接在一起,最后运行。 要告诉 Rust 编译你的示例,但是不实际运行它,使用 no_run 注释。

/// 将本地玻璃栽培箱的所有照片上传到在线画廊
///
/// ```no_run
/// let mut session = fern_sim::connect();
/// session.upload_all();
/// ```
pub fn upload_all(&mut self) {
    ...
}

指定依赖项

指定版本号。

image = "0.6.1"

如果想要使用没有发布在 crates.io 上的依赖项。指定 Git 存储库 URL 和修订号。

image = { git = "https://github.com/Piston/image.git", rev = "528f19c" }

或者指定一个包含 crate 源代码的目录(本地):

image = { path = "vendor/image" }

版本

如果 Cargo.toml 文件中写的是 image = "0.13.0" 时,Cargo 会使用与版本 0.13.0 兼容的最新版本的 image。 兼容性规则:

  • 以 0.0 开头的版本号非常原始,Cargo 永远不会假定它能与任其他版本兼容。
  • 以 0.X 开头,X 不为零,可认为与 0.X 系列的版本兼容,例如指定版本为 0.6.1,则可能使用 0.6.3.
  • 一旦项目达到了 1.0,出现了新的主版本号会破坏兼容性。因此,如果指定了版本 2.0.1,那么 Cargo 可能会使用 2.17.99,但不会使用 3.0。

可以使用一些运算符来指定确切的版本或版本范围。

  • image = "=0.10.0",使用确切的 0.10.0 版本。
  • image = ">=1.0.5",任何大于 1.0.5 的版本,甚至是大于 1.0 的版本。
  • imagge = ">1.0.5 <1.1.9",限定了版本的范围。
  • image = "<=2.7.10",使用小于 2.7.10 的版本。

有时候会使用 * 通配符,它会告诉 Cargo 任何版本都可以。

Cargo.lock

为了项目依赖的稳定性,不希望每次构建时都将依赖库升级到最新的版本。Cargo 内置一种机制来防止发生这种情况。第一次构建时候,会输出一个 Cargo.lock 文件,会记录它使用的每个 crate 的确切版本。以后的构建都将参考此文件并继续使用相同的版本。仅当要求 Cargo 升级时才会升级到最新版本,方法是手动修改 Cargo.toml 文件中的版本号或运行 cargo update。 但是 cargo update 只能升级到兼容的最新版本。如果有大版本的更改,需要自己手动修改。

将 crate 发布到 crates.io

首先打包 crate。

$ cargo package

它会创建一个 .crate 文件,其中包含了所有库的源文件。可以使用 cargo package --list 来查看包含了哪些文件。 之后登录 crates.io 并获取密钥。 使用 cargo login *** 登录。 最后使用 cargo publish 进行发布。

工作空间

随着项目不断成长,最终会写出很多 crate。它们并存于同一个源代码存储库中: 截屏2024-02-06 16.26.22.png Cargo 的工作方式是,每个 crate 都有自己的构建目录 target,其中包含该 crate 的所有依赖项的单独构建。这些构建目录是完全独立的。即使两个 crate 具有共同的依赖项,它们也不能共享任何已编译的代码。这有点浪费。 可以使用 Cargo 工作空间来节省编译时间和磁盘空间。Cargo 工作空间是一组 crate,它们共享着公共构建目录和 Cargo.lock 文件。需要在根目录中创建一个 Cargo.toml 文件,并将下面的代码放入其中:

[workspace]
members = ["fern_sim", "fern_img", "fern_video"]

fern_sim 这些包含了你的 crate 的子目录名。这些子目录中所有残存的 Cargo.lock 文件和 target 目录都需要删除。 完成这些操作后,任何 crate 中的 cargo build 都会自动在根目录下创建和共享构建目录。 cargo build --workspace 会构建当前工作空间中的所有 crate。cargo testcargo doc 也能接受 --workspace 选项。

参考链接:

🌟 🙏🙏感谢您的阅读,如果对您有帮助,欢迎关注、点赞 🌟🌟