【Rust】Crate 和 Module
Crates
Rust
程序是由 crate
组成的,每个 crate
都是一个完整的的单元:单个库或可执行文件的所有源代码,以及任何相关的测试、示例、工具、配置和其他东西。可以使用 cargo build --verbose
查看项目中使用了哪些 crates
。
通常项目的依赖都是配置在 Cargo.toml
文件中,例如:
1 | [dependencies] |
可以通过 cargo build
,cargo install
或者 cargo add
下载依赖代码。一旦有了源代码,Cargo
就会编译所有的 crate
。它为项目依赖图中的每个 crate
运行一次 rustc
(Rust
编译器)。编译库时,Cargo
使用 --crate-type lib
选项。这告诉 rustc
不要寻找 main()
函数,而是生成一个 .rlib
文件,其中包含可用于创建二进制文件和其他 .rlib
文件的编译代码。例如:
1 | rustc --crate-name num --edition=2018 /Users/fudenglong/.cargo/registry/src/mirrors.ustc.edu.cn-61ef6e0cd06fb9b8/num-0.4.0/src/lib.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --crate-type lib --emit=dep-info,metadata,link -C embed-bitcode=no -C split-debuginfo=unpacked -C debuginfo=2 --cfg 'feature="default"' --cfg 'feature="num-bigint"' --cfg 'feature="std"' -C metadata=b84820de50dc7f78 -C extra-filename=-b84820de50dc7f78 --out-dir /Users/fudenglong/WORKDIR/rust/mandelbrot/target/debug/deps -L dependency=/Users/fudenglong/WORKDIR/rust/mandelbrot/target/debug/deps --extern num_bigint=/Users/fudenglong/WORKDIR/rust/mandelbrot/target/debug/deps/libnum_bigint-bd772250e89d4bb9.rmeta --extern num_complex=/Users/fudenglong/WORKDIR/rust/mandelbrot/target/debug/deps/libnum_complex-d3fd80f953e1ac52.rmeta --extern num_integer=/Users/fudenglong/WORKDIR/rust/mandelbrot/target/debug/deps/libnum_integer-7ff0466209086397.rmeta --extern num_iter=/Users/fudenglong/WORKDIR/rust/mandelbrot/target/debug/deps/libnum_iter-2b149e71dbad2afc.rmeta --extern num_rational=/Users/fudenglong/WORKDIR/rust/mandelbrot/target/debug/deps/libnum_rational-1686ad6eb82c18d4.rmeta --extern num_traits=/Users/fudenglong/WORKDIR/rust/mandelbrot/target/debug/deps/libnum_traits-deaceb32c41a04f1.rmeta --cap-lints allow |
对于每个 rustc
命令,Cargo
都会传递 --extern
选项,给出 crate
将使用的每个库的文件名。这样,当 rustc
看到像 use num::bigint::BigInt;
这样的代码行时,它可以确定 num
是另一个 crate
的名称,并且通过 Cargo
,可以在磁盘上找到已编译的 crate
。Rust
编译器需要访问这些 .rlib
文件,因为它们包含库的编译代码, Rust
会将该代码静态链接到最终的可执行文件中。 .rlib
还包含类型信息,因此 Rust
可以检查我们在代码中使用的库功能是否确实存在,以及我们是否正确使用它们,它还包含 crate
的公共内联函数、泛型和宏的副本等。
如果编译程序时,Cargo
使用 --crate-type bin
,结果将会生成目标平台的二进制可执行文件。
Edition
Rust
具有极强的兼容性保证,在 Rust 1.0
上编译的任何代码都必须在 Rust 1.50
上编译。但有时社区会遇到令人信服的扩展语言的提议,这会导致旧代码不再编译。例如,经过多次讨论,Rust
确定了一种支持异步编程的语法,将标识符 async
和 await
重新用作关键字。但是这种语言更改会破坏任何使用 async
或 await
作为变量名称的现有代码。
为了在不破坏现有代码的情况下发展,Rust
使用 Edition
。Rust 2015
版与 Rust 1.0
兼容。2018
将 async
和 await
更改为关键字,简化了模块系统,并引入了与 2015
不兼容的各种其他语言更改。打开 Cargo.toml
我们会看到如下的 Edition
声明,默认是 2021
:
1 | [package] |
Rust
的编译器承诺始终接受该语言的所有现存版本,并且程序可以自由地混合使用不同版本编写的 crate
,例如,2015
的 crate
依赖于 2018
的 crate
甚至可以,换句话说,一个 crate
的版本只影响其源代码的解释方式,代码编译完成之后,就没有所谓的 Edition
区分了,在学要使用语言的新功能时,我们只需要修改 Edition
即可。
版本不会每年都发布,只有在 Rust
项目决定需要时才会发布,例如,没有 2020
版,将版本设置为 2020
会导致错误。Rust
版本指南 涵盖了每个版本中引入的更改,并提供了版本系统的良好背景,对于新代码,建议总是使用最新的版本,cargo new
默认在最新版本上创建新项目。如果有一个用旧版 Rust
编写的 crate
,cargo fix
命令会帮助你自动将代码升级到新版本。
Profile
配置文件提供了一种更改编译器设置的方法,影响优化和调试符号等内容。Cargo
有 4
个内置配置文件:dev
、release
、test
和 bench
。如果未在命令行上指定配置文件,则会根据正在运行的命令自动选择配置文件。除了内置配置文件外,还可以指定自定义的用户定义配置文件。
可以使用 [profile]
在 Cargo.toml
中更改配置文件设置,在每个命名配置文件中,可以使用如下键/值对更改各个设置:
1 | [profile.dev] |
cargo build
会使用 [profile.dev]
的配置,cargo build --release
会使用 [profile.release]
的配置,cargo test
使用 [profile.test]
中的配置。
更多详细的内容请看 Cargo Profiles。
Module
crate
就是第三方模块,用于项目之间的代码共享,而 Module
是项目内的代码组织。它们充当 Rust
的命名空间、构成 Rust
程序或库的函数、类型、常量等的容器。一个模块如下所示:
1 | mod spores { |
本例中,模块是 Spore
结构和两个函数的集合。pub
关键字使得标记的项公开,因此可以从模块外部访问它。
一个函数被标记为 pub(crate)
,这意味着它在这个 crate
内的任何地方都可用,但不作为外部接口的一部分公开。它不能被其他 crate
使用,也不会出现在这个 crate
的文档中。被标记为 pub
的项目通常称为导出该项目,任何未标记为 pub
的内容都是私有的,只能在定义它的同一模块或任何子模块中使用:
1 | let s = spores::produce_spore(&mut factory); // ok |
模块嵌套
模块可以嵌套,一个模块可能仅仅是子模块的集合:
1 | mod plant_structures { |
如果希望嵌套模块中的项目对其他 crate
可见,必须将到达这个项的所有模块都标记为 pub
。也可以指定 pub(super)
,使项目仅对父模块可见,以及 pub(in <path>)
,使其在特定父模块及其后代中可见。这对于深度嵌套的模块特别有用:
1 | mod plant_structures { |
通过这种方式,我们可以编写一个完整的程序,包含大量代码和整个模块层次结构,以我们想要的任何方式组织起来,而所有这些都在一个源文件中。
模块和文件系统
如果将模块以单个文件形式组织,这种方式工作很痛苦,大型项目中往往需要拆分,将不同的功能的代码以不同的文件区分,使得代码在逻辑上,物理组织上都能很清晰。
单文件模块
之前我们是在 spores
模块中使用括号将模块的内容包裹起来的,现在可以在单个源码文件中开发模块。我们在 main.rs
中声明如下模块:
1 | mod spores; |
然后在 spores.rs
开发该模块的代码:
1 | /// A cell made by an adult fern... |
公开和私有的原则和之前讲的是相同的,Rust
从不单独编译模块,即使它们在单独的文件中:当你构建一个 crate
时,会重新编译它的所有模块。当 Rust
看到一个模块时,例如上面的 mod spores;
时,它会检查 spores.rs
和 spores/mod.rs
是否存在,如果都存在或者都不存在,就会报错。
模块目录(一)
但是当我们的模块中包含子模块时,就不能像 spores.rs
那样单独处理了,就像之前的 plant_structures
模块。如果,我们有下面这样的结构:
1 | fern_sim/ |
在 main.rs
,我们声明 plant_structures
模块,这会让 Rust
加载 plant_structures/mod.rs
:
1 | pub mod plant_structures; |
然后,我们再声明子模块:
1 | pub mod roots; |
这三个模块的内容存储在名为 leaves.rs
、roots.rs
和 stems.rs
的单独文件中,和 plant_structures/mod.rs
同级。
模块目录(二)
我们也可以使用文件和目录一起组成模块,如果之前的 stems
需要包含 xylem
和 phloem
,我们选择保留 plant_structures/stems.rs
,然后再添加一个 stems
目录:
1 | fern_sim/ |
然后,我们可以在 stems.rs
中声明两个新模块:
1 | pub mod xylem; |
所以这里有三种模块组织方式:
- 模块在他们自己的文件中;
- 模块在他们自己的目录中,带有
mod.rs
; - 模块在他们自己的文件中,带有包含子模块的补充目录;
导入
::
操作符被用于访问其他模块的功能。我们可以直接使用其他模块中的功能而不实现导入,例如:
1 | std::mem::swap(&mut s1, &mut s2); |
std
指向标准库的顶层模块,mem
是其中的一个子模块,std::mem::swap
只是其中的一个导出函数。上面的这种使用访问方式有点冗长,另一种方式是将要使用的功能导入当前的模块,使用 use
将 std::mem
引入到当前模块,并且定义新的别 mem
:
1 | use std::mem; |
我们可以写 use std::mem::swap;
导入 swap
函数本身而不是 mem
模块。但最好的方式是:导入类型、trait
和模块(如 std::mem
),然后使用相对路径访问其中的函数、常量和其他成员。例如:
1 | use std::collections::{HashMap, HashSet}; // 同时导入 |
我们也可以使用 as
对导入的名称进行重命名:
1 | use std::io::Result as IOResult; |
子模块不会自动导入父模块的内容,假设我们的 proteins/mod.rs
中内容是这样子的:
1 | pub enum AminoAcid { ... } |
那么模块 proteins/synthesis.rs
中如果不导入 AminoAcid
是不能直接使用的:
1 | pub fn synthesize(seq: &[AminoAcid]) // error: can't find type `AminoAcid` |
而是,每个模块都是以一个空的状态开始的,必须手动导入它使用的任何内容(也不完全是空的,有预导入):
1 | use super::AminoAcid; // explicitly import from parent |
默认情况下,是从当前模块开始导入:
1 | pub mod synthesis; |
self
是当模块的昵称,因此在 proteins/mod.rs
中可以这样写:
1 |
|
也可以简化为:
1 | use AminoAcid::*; |
除了从顶层模块导入,从 self
当前模块导入或者从当前的子模块导入之外,还可以通过 super
或者 crate
关键字进行导入,其中 super
表示父模块,而 crate
表示当前模块的 crate
。
使用相对于 crate
根目录的导入而不是相对于当前路径的导入,有助于在项目中移动代码。因为如果当前模块的路径发生更改,所有导入都不会中断。例如:
1 | use crate::proteins::AminoAcid; // 显示从 crate 的根导入 |
之前看了从父模块导入子模块,但是如果从子模块导入父模块的内容,可以使用 super::
。
如果你有一个模块和外部 crate
同名,在导入的时候就要注意了,否则会引起错误。例如,如果引用了 image
,自己也有个 image
模块:
1 | mod image { |
解决这个问题的方法是使用绝对导入,导入以 ::
开始,例如,如果要导入第三方 image
,可以这样写:
1 | use ::image::Pixels; // the `image` crate's `Pixels` |
引入内部的 image module
可以这样写:
1 | use self::image::Sampler; // the `image` module's `Sampler` |
预导入
之前说每个模块都以空的状态开始,但其实也不完全是空的,因为 Rust
有很多常用的类型,为了简化编程,Rust
会帮我们导入,不用我们显示导入。例如:Vec
和 Result
。
自动导入的内容可以看这里,std::prelude。
别名导出
use
只是将我们使用的内容从其他模块或者 crate
导入并且在本模块起了个没别名,但是它也可以将导入的内容重新导出。例如:
1 | pub use self::leaves::Leaf; |
这意味着 Leaf
和 Root
是 plant_structures
模块中的公共内容,它们也仍然是 self::leaves::Leaf
和 self::roots::Root
的别名。
结构体导出
module
可以包含用户定义的结构体类型,使用 struct
关键字引入,它和它的字段也可以由 pub
声明是否导出。一个简单的结构如下所示:
1 | pub struct Fern { |
结构体的所有字段,甚至私有字段,都可以在声明结构体的模块及其子模块中访问,在模块之外,只能访问导出字段。
静态变量和常量
const
关键字引入了一个常量。 语法和 let
一样,只是它可能被标记为 pub
,并且类型是必需的。此外,大写名称对于常量来说是常规的:
1 | pub const ROOM_TEMPERATURE: f64 = 20.0; // degrees Celsius |
static
关键字引入了一个静态项,和 const
:
1 | pub static ROOM_TEMPERATURE: f64 = 68.0; // degrees Fahrenheit |
常量有点像 C
中的 #define
,值会被编译到使用的地方。静态变量是在程序开始运行之前设置并持续到退出的变量。在代码中使用常量作为 magic number
和字符串,对大量数据或任何需要借用常量值的引用的时候使用静态。
没有 mut
类型常量,静态变量可以标记为 mut
,Rust
无法强制执行其关于对 mut
静态变量的独占访问的规则。因此,它们本质上是非线程安全的,安全代码根本不能使用它们,Rust
不鼓励全局可变状态:
1 | static mut PACKETS_SERVED: usize = 0; |
开发 library 项目
如果想要将项目编译成一个 lib
而不是一个可执行文件,我们只需要三步:
-
将
src/main.rs
重命名成src/lib.js
; -
添加
pub
关键字给src/lib.js
中的导出内容; -
移除
src/main.rs
中的main
函数;
我们不需要更改 Cargo.toml
中的任何内容 默认情况下,cargo build
查看源目录中的文件并确定要构建的内容。当它看到文件 src/lib.rs
时,就知道需要去构建一个 lib
。src/lib.rs
中的代码构成了库的根模块,使用我们的其他 crate
只能访问此根模块的公共项目。
src/bin
目录
我们可以让一个项目是 lib
,也可以让它同时编译成一个可执行程序,只是需要将 main
函数移到目录 sr/bin
中,而且可以有多个 main
函数,例如我的项目结构如下:
1 | ~/WORKDIR/rust/mandelbrot 20:15:24 |
各个文件的内容如下:
1 | use num::bigint::BigInt; |
1 | use mandelbrot::approx_sqrt; |
1 | use mandelbrot::approx_sqrt; |
1 | [package] |
我们可以指定运行不同的 main
函数:
~/WORKDIR/rust/mandelbrot 20:25:13
$ cargo run --bin mandelbrot
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/mandelbrot`
4057691201/1283082416
~/WORKDIR/rust/mandelbrot 20:25:15
$ cargo run --bin mandelbrot_v2
Compiling mandelbrot v0.1.0 (/Users/fudenglong/WORKDIR/rust/mandelbrot)
Finished dev [unoptimized + debuginfo] target(s) in 0.15s
Running `target/debug/mandelbrot_v2`
4057691201/1283082416
属性
Rust
程序中的任何项目都可以用属性进行修饰。属性是 Rust
用于向编译器编写杂项指令和建议的语法。例如,假设您收到以下警告:
libgit2.rs: warning: type `git_revspec` should have a camel case name
such as `GitRevspec`, #[warn(non_camel_case_types)] on by default
但是你选择这个名字是有原因的,你希望屏蔽这个告警,可以通过在类型上添加 #[allow]
完成:
1 |
|
条件编译是使用属性编写的另一个功能,即 #[cfg]
:
1 | // 只有在编译安卓平台的项目时包含这个模块 |
#[cfg]
常用的属性可以看 条件编译。
给函数添加 #[inline]
属性可以建议编译器根据情况决定是否内联函数。
1 |
|
当一个 crate
中定义的函数或方法在另一个 crate
中被调用时,Rust
不会内联它,除非它是泛型的或显式标记为 #[inline]
。Rust
还支持更直接的 #[inline(always)]
,要求在每个调用点内联扩展函数,并支持 #[inline(never)]
,要求永远不要内联函数。
一些属性,如 #[cfg]
和#[allow]
,可以附加到整个模块并应用于其中的所有内容。其他的,如 #[test]
和#[inline]
,必须附加到单个项目,每个属性都是可以指定参数进行定制,这里看完整的属性列表。
要将属性附加到整个 crate
,请将其添加到 main.rs
或 lib.rs
文件的顶部,在任何内容之前,然后写 #!
而不是#
,像这样:
1 |
|
#!
通常只用于文件的开头,用于将属性附加到整个模块或 crate
。某些属性总是使用 #!
语法,因为它们只能应用于整个 crate
。 例如,#![feature]
属性用于打开 Rust
语言和库的不稳定特性,这些特性是实验性的,因此可能存在错误,或者将来可能会被更改或删除。
例如,Rust
为跟踪宏的扩展提供了实验性支持,比如 assert!
,但是由于这种支持是实验性的,你只能通过
- (1) 安装
nightly
版本的Rust
; - (2) 来使用
#![feature(trace_macros)]
明确使用trace_macros
;
1 |
|
单元测试
Rust
内置了一个单元测试框架,任何由 #[test]
标记的普通函数是测试函数:
1 |
|
cargo test
用于运行项目中的所有测试函数:
$ cargo test
Compiling math_test v0.1.0 (file:///.../math_test)
Running target/release/math_test-e31ed91ae51ebf22
running 1 test
test math_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
可以通过 cargo test TESTNAME
指定要运行的测试函数需要包含的关键字,以运行某些测试。
通常使用 asset!
和 asset_eq!
在测试中对结果进行断言,如果断言失败,程序就会 panic
。asset!
和 asset_eq!
没有包含在 release
版本中,如果仅仅是构建 debug
版本,可以使用 debug_assert!
和 debug_assert_eq!
。
为了测试错误情况,可以添加 #[should_panic]
给测试函数,这个例子中我们也使用了 #allow
允许我们无条件 panic
:
1 | /// This test passes only if division by zero causes a panic, |
还可以从测试中返回 Result<(), E>
:
1 | /// if 1024 是有效的数字,这个测试会通过 |
标有#[test]
的函数是会条件编译的。普通的 cargo build
或 cargo build --release
会跳过测试代码。但运行 cargo test
时,Cargo
会构建程序两次:一次以普通方式,一次来运行测试。这意味着单元测试可以与他们测试的代码一起使用,如果需要,可以访问内部实现细节,而且没有运行时成本。但是,它可能会导致一些警告。 例如:
1 | fn roughly_equal(a: f64, b: f64) -> bool { |
在省略了测试代码的构建中,roughly_equal
似乎没有被使用,Rust
会报错:
$ cargo build
Compiling math_test v0.1.0 (file:///.../math_test)
warning: function is never used: `roughly_equal`
--> src/crates_unused_testing_function.rs:7:1
|
7 | / fn roughly_equal(a: f64, b: f64) -> bool {
8 | | (a - b).abs() < 1e-6
9 | | }
| |_^
|
= note: #[warn(dead_code)] on by default
按照约定,当测试代码需要一些代码支持时,需要放进 tests
模块,并且使用 #[cfg(test)]
标记整个模块:
1 | // include this module only when testing |
集成测试
我们可以在 src
目录同级创建一个 tests
目录,用于放我们的集成测试代码,集成测试将我们的库代码当做外部 crate
链接然后进行测试,因此集成测试也只能测试公共 API
:
1 | use fern_sim::Terrarium; |
cargo test
命令会同时运行集成测试和单元测试,如果只想运行 tests/unfurl.rs
中的集成测试代码,可以使用 cargo test --test unfurl
。
文档
cargo doc
命令可以生成 crate
的文档,文档中会包含库中 pub
内容的文档,文档就是添加到它们上面的注释。例如:
1 | /// Simulate the production of a spore by meiosis. |
当 Rust
看到三个斜线开头的注释时,就相当于使用 #[doc]
,上面的例子等价于:
1 |
|
除了 ///
样式的注释之外,还有 //!
,被当做 #![doc]
,经常用于模块或者 crate
,例如 src/lib.rs
可能像下面这样:
1 | //! Simulate the growth of ferns, from the level of |
注释的内容会被当做 Markdown
格式,还可以包含 Html
标记。注释有个特色功能就是可以用 Rust
的模块组织路径去引用 Rust
定义的函数,结构体等。例如:
1 | /// Create and return a [`VascularPath`] which represents the path of |
还可以添加搜索别名,以便使用内置搜索功能更轻松地查找内容。在这个 crate
的文档中搜索 path
或 route
都能找到 VascularPath
:
1 |
|
还可以使用 backticks
在运行文本的中间设置一些代码,在输出中,这些片段将被格式化为固定宽度的字体。可以通过缩进4
个空格来添加更大的代码示例:
1 | /// A block of code in a doc comment: |
也可以使用 Markdown
格式的代码风格,有同样效果:
1 | /// Another snippet, the same code, but written differently: |
还有很多注释类型,可以参考这里 注释。
文档测试
当运行测试的时候,Rust
会检查所有出现在文档中的代码块,编译成一个独立的 crate
,然后和我们的库链接并执行。下面是一个用 cargo new --lib ranges
创建的例子,把下面的代码的放入 ranges/src/lib.rs
中:
1 | use std::ops::Range; |
使用 cargo doc --no-deps --open
应该能看到下面的文档:
如果运行 cargo test
会看到如下的信息:
~/WORKDIR/rust/ranges ⌚ 9:57:08
$ cargo test
Finished test [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/lib.rs (target/debug/deps/ranges-13060b2daf473d43)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests ranges
running 2 tests
test src/lib.rs - overlap (line 5) ... ok
test src/lib.rs - overlap (line 10) ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.63s
如果你将 cargo test --verbose
,你会看到它使用 rustdoc --test
来运行这两个文档中的测试。rustdoc
将每个代码示例存储在一个单独的文件中,添加几行样例代码,以生成两个程序。 例如:
1 | use ranges; |
doc test
背后的想法不是把所有的测试都放在文档中,而是确保文档中的代码能正常编译运行。一个代码示例将包含一些诸如导入类的东西,但是如果想在代码中隐藏,可以以 #
开头:
1 | /// Let the sun shine in and run the simulation for a given |
生成的文档如下图所示,省略了 #
开头的导入:
有时在文档中展示一个完整的示例程序会很有帮助,包括一个 main
函数。 rustdoc
将包含确切字符串 fn main
的任何代码块视为一个完整的程序,并且不会向其中添加任何内容。
如果我们想要实际编译代码块,但又不行让 rust
运行它,可以添加 no_run
标记:
1 | /// Upload all local terrariums to the online gallery. |
但是如果也不想编译,那么使用 ignore
,而不是 no_run
。如果代码块根本不是 Rust
代码,请使用语言的名称,如 c
或 sh
,或 text
(表示纯文本),它将任何它无法识别的注释视为表明代码块不是 Rust
,这会禁用代码高亮和文档测试。
依赖声明
关于依赖声明,详细请看 cargo
依赖声明。
-
image = "0.6.1"
:指定版本号,从crates.io
下载依赖: -
image = { git = "https://github.com/Piston/image.git", rev = "528f19c" }
:指定版本号和原本下载路径; -
image = { path = "vendor/image" }
指定包含依赖源代码的相对目录;
依赖版本号
当我们在 Cargo.toml
文件中写入类似 image = "0.13.0"
的内容时,Cargo
对此的解释相当松散,它使用被认为与 0.13.0
版本兼容的最新版本的 image
。兼容性规格改编自 语义化版本 2.0.0。
-
以
0.0
开头的版本号非常原始,以至于Cargo
从不认为它与任何其他版本兼容; -
以
0.x
开头的版本号(其中x
非零)被认为与0.x
系列中的其他点版本兼容。 我们指定了image
版本0.6.1
,但如果可用,Cargo
将使用0.6.3
; -
一旦项目达到
1.0
,由于新的主版本会破坏兼容性。因此,如果要求版本2.0.1
,Cargo
可能会使用2.17.99
,而不是3.0
;
默认情况下,版本号是灵活的,否则使用哪个版本的问题很快就会变得过分约束。假设一个库 libA
使用 num = "0.1.31"
而另一个库 libB
使用 num = "0.1.29"
。如果版本号需要完全匹配,则没有项目能够同时使用这两个库,允许 Cargo
使用任何兼容版本是一个更实用的默认设置。
尽管如此,不同的项目在依赖关系和版本控制方面有不同的需求。可以使用运算符指定确切的版本或版本范围:
Cargo.toml |
解释 |
---|---|
image = "=0.10.0" |
完全匹配 0.10.0 版本 |
image = ">=1.0.5" |
使用 1.0.5 或者更高的版本,甚至 2.9 |
image = ">1.0.5 <1.1.9" |
使用大于 1.0.5 但小于 1.1.9 的版本 |
image = "<=2.7.10" |
小于 2.7.10 的任何版本 |
* |
任何版本 |
Cargo.lock
Cargo.toml
中的版本号很灵活,但我们不希望 Cargo
每次构建时都将我们升级到最新的库版本。因此,Cargo
有一个内置机制来防止这种情况。第一次构建项目时,Cargo
会输出一个 Cargo.lock
文件,该文件记录了它使用的每个 crate
的确切版本。以后的构建将参考这个文件并继续使用相同的版本。Cargo
仅在你告诉它时才升级到较新的版本,通过手动增加 Cargo.toml
文件中的版本号或运行 cargo update
:
$ cargo update
Updating registry `https://github.com/rust-lang/crates.io-index`
Updating libc v0.2.7 -> v0.2.11
Updating png v0.4.2 -> v0.4.3
cargo update
仅升级到与在 Cargo.toml
中指定的内容兼容的最新版本。如果你指定了 image = "0.6.1"
,并且你想升级到 0.10.0
版本,你必须在 Cargo.toml
中改变它。下次构建时,Cargo
会更新到新版本的镜像库,并将新版本号存储在 Cargo.lock
中。前面的示例显示 Cargo
更新了 crates.io
上托管的两个 crate
,存储在 Git
中的依赖项也会发生类似的情况。假设我们的 Cargo.toml
文件包含以下内容:
image = { git = "https://github.com/Piston/image.git", branch = "master" }
如果 cargo build
发现我们有一个 Cargo.lock
文件,它不会从 Git
存储库中提取新的更改。而是读取 Cargo.lock
并使用与上次相同的版本。但是 cargo update
将从 master
中提取,以便我们的下一个构建使用最新版本。
Cargo.lock
会自动生成,通常不会手动编辑它。如果项目是可执行文件,应该将 Cargo.lock
提交给版本控制。这样,每个人构建的项目都将始终获得相同的版本,Cargo.lock
文件的历史记录将记录你的依赖更新。
如果你的项目是一个普通的 Rust
库,不要提交 Cargo.lock
。因为下游项目也拥有 Cargo.lock
文件,其中包含其整个依赖关系图的版本信息,他们会忽略依赖的库中的 Cargo.lock
文件。
在极少数情况下,项目如果是共享库(即输出是 .dll
、.dylib
或 .so
文件),没有这样的下游项目,应该提交 Cargo.lock
。
Cargo.toml
灵活的版本说明符使我们可以轻松地解决依赖之间的兼容性。 Cargo.lock
保证了构建一致性。
发布到 crates.io
详情请看 发布到 crates.io。
工作区间
详情请看 Cargo 工作空间。