特性
Cargo 的“特性(features)”提供了一种机制来表达条件编译和可选依赖。包在其 Cargo.toml
文件的 [features]
表中定义了一组命名特性,每个特性都可以被启用或禁用。可以通过命令行使用诸如 --features
的标志来启用正在构建的包的特性。依赖项的特性可以在 Cargo.toml
中的依赖项声明中启用。
注意:发布到 crates.io 上的新 crate 或新版本现在最多只能有 300 个特性。例外情况会根据具体情况批准。详情请参阅这篇博文。鼓励通过 crates.io Zulip 频道参与解决方案讨论。
另请参阅特性示例章节,了解特性的一些用法示例。
[features]
部分
特性在 Cargo.toml
文件的 [features]
表中定义。每个特性指定一个数组,其中包含它启用的其他特性或可选依赖项。以下示例展示了特性如何用于一个 2D 图像处理库,其中可以可选地包含对不同图像格式的支持
[features]
# Defines a feature named `webp` that does not enable any other features.
webp = []
定义此特性后,可以使用cfg
表达式在编译时有条件地包含支持所需特性的代码。例如,包的 lib.rs
文件内部可以包含以下内容
#![allow(unused)] fn main() { // This conditionally includes a module which implements WEBP support. #[cfg(feature = "webp")] pub mod webp; }
Cargo 使用 rustc
的--cfg
标志在包中设置特性,代码可以使用cfg
属性或cfg
宏来测试它们是否存在。
特性可以列出它启用的其他特性。例如,ICO 图像格式可以包含 BMP 和 PNG 图像,因此当启用 ICO 特性时,它应该确保也启用了这些其他特性
[features]
bmp = []
png = []
ico = ["bmp", "png"]
webp = []
特性名称可以包含来自Unicode XID 标准(包括大多数字母)的字符,此外还允许以下划线 _
或数字 0
到 9
开头,并且在第一个字符之后还可以包含 -
、+
或 .
。
注意:crates.io 对特性名称的语法施加了额外的限制,它们必须只能是ASCII 字母数字字符或
_
、-
或+
。
default
特性
默认情况下,所有特性都是禁用的,除非明确启用。这可以通过指定 default
特性来改变
[features]
default = ["ico", "webp"]
bmp = []
png = []
ico = ["bmp", "png"]
webp = []
构建包时,会启用 default
特性,进而启用列出的特性。此行为可以通过以下方式更改:
注意:选择默认特性集时要小心。默认特性是一种便利,使用户更容易使用包,而无需仔细选择要为常见用法启用哪些特性,但这也有一些缺点。除非指定了
default-features = false
,否则依赖项会自动启用默认特性。这使得确保不启用默认特性变得困难,特别是对于在依赖关系图中出现多次的依赖项。每个包都必须确保指定了default-features = false
以避免启用它们。另一个问题是,从默认集中移除特性可能会导致SemVer 不兼容的更改,因此您应该确信您将保留这些特性。
可选依赖项
依赖项可以标记为“可选(optional)”,这意味着默认情况下不会编译它们。例如,假设我们的 2D 图像处理库使用一个外部包来处理 GIF 图像。这可以这样表达:
[dependencies]
gif = { version = "0.11.1", optional = true }
默认情况下,这个可选依赖项隐式定义了一个特性,如下所示:
[features]
gif = ["dep:gif"]
这意味着只有当 gif
特性被启用时,才会包含此依赖项。代码中可以使用相同的 cfg(feature = "gif")
语法,并且可以像启用任何特性一样启用此依赖项,例如 --features gif
(参见下面的命令行特性选项)。
在某些情况下,您可能不希望暴露一个与可选依赖项同名的特性。例如,可选依赖项可能是一个内部细节,或者您想将多个可选依赖项组合在一起,或者您只是想使用一个更好的名称。如果在 [features]
表中的任何位置使用 dep:
前缀指定可选依赖项,则会禁用隐式特性。
注意:
dep:
语法仅在 Rust 1.60 及更高版本中可用。早期版本只能使用隐式特性名称。
例如,假设为了支持 AVIF 图像格式,我们的库需要启用另外两个依赖项
[dependencies]
ravif = { version = "0.6.3", optional = true }
rgb = { version = "0.8.25", optional = true }
[features]
avif = ["dep:ravif", "dep:rgb"]
在此示例中,avif
特性将启用列出的两个依赖项。这也避免了创建隐式的 ravif
和 rgb
特性,因为我们不希望用户单独启用它们,因为它们是我们的 crate 的内部细节。
注意:可选地包含依赖项的另一种方法是使用平台特定依赖项。与使用特性不同,这些是根据目标平台来决定的条件。
依赖项特性
可以在依赖项声明中启用依赖项的特性。features
键指示要启用哪些特性
[dependencies]
# Enables the `derive` feature of serde.
serde = { version = "1.0.118", features = ["derive"] }
可以使用 default-features = false
来禁用default
特性
[dependencies]
flate2 = { version = "1.0.3", default-features = false, features = ["zlib"] }
注意:这可能无法确保默认特性被禁用。如果另一个依赖项包含
flate2
但未指定default-features = false
,则会启用默认特性。有关更多详细信息,请参阅下面的特性统一。
依赖项的特性也可以在 [features]
表中启用。语法是 "package-name/feature-name"
。例如:
[dependencies]
jpeg-decoder = { version = "0.1.20", default-features = false }
[features]
# Enables parallel processing support by enabling the "rayon" feature of jpeg-decoder.
parallel = ["jpeg-decoder/rayon"]
"package-name/feature-name"
语法如果 package-name
是一个可选依赖项,也会启用它。这通常不是您想要的。您可以添加一个 ?
,例如 "package-name?/feature-name"
,这只会在其他地方启用了可选依赖项时才启用给定特性。
注意:
?
语法仅在 Rust 1.60 及更高版本中可用。
例如,假设我们为库添加了一些序列化支持,这需要启用一些可选依赖项中的相应特性。可以这样做:
[dependencies]
serde = { version = "1.0.133", optional = true }
rgb = { version = "0.8.25", optional = true }
[features]
serde = ["dep:serde", "rgb?/serde"]
在此示例中,启用 serde
特性将启用 serde 依赖项。它还会为 rgb
依赖项启用 serde
特性,但前提是其他地方已经启用了 rgb
依赖项。
命令行特性选项
可以使用以下命令行标志来控制哪些特性被启用:
-
--features
FEATURES:启用列出的特性。多个特性可以用逗号或空格分隔。如果使用空格,请确保在 shell 中运行 Cargo 时使用引号将所有特性括起来(例如--features "foo bar"
)。如果在工作空间中构建多个包,可以使用package-name/feature-name
语法为特定的工作空间成员指定特性。 -
--all-features
:激活命令行上所有选定包的所有特性。 -
--no-default-features
:不激活选定包的default
特性。
特性统一
特性对于定义它们的包来说是独有的。在一个包上启用某个特性不会在其他包上启用同名特性。
当一个依赖项被多个包使用时,Cargo 在构建它时将使用该依赖项上所有启用特性的并集。这有助于确保只使用该依赖项的一个副本。有关更多详细信息,请参阅解析器文档的特性部分。
例如,我们来看一下winapi
包,它使用了大量特性。如果您的包依赖于包 foo
,该包启用了 winapi
的 “fileapi” 和 “handleapi” 特性,而另一个依赖项 bar
启用了 winapi
的 “std” 和 “winnt” 特性,那么 winapi
将在构建时启用所有这四个特性。
由此产生的一个结果是,特性应该是*可加的(additive)*。也就是说,启用特性不应该禁用功能,并且通常可以安全地启用任何特性组合。特性不应该引入SemVer 不兼容的更改。
例如,如果您想可选地支持no_std
环境,不要使用 no_std
特性。相反,使用一个*启用* std
的 std
特性。例如:
#![allow(unused)] #![no_std] fn main() { #[cfg(feature = "std")] extern crate std; #[cfg(feature = "std")] pub fn function_that_requires_std() { // ... } }
互斥特性
在极少数情况下,特性可能彼此互不兼容。如果可能,应尽量避免这种情况,因为它需要协调依赖关系图中包的所有使用方进行协作,以避免同时启用这些特性。如果无法避免,考虑添加编译错误来检测这种情况。例如:
#[cfg(all(feature = "foo", feature = "bar"))]
compile_error!("feature \"foo\" and feature \"bar\" cannot be enabled at the same time");
与其使用互斥特性,不如考虑其他一些选项:
- 将功能拆分为单独的包。
- 当存在冲突时,选择一个特性而非另一个。
cfg-if
包可以帮助编写更复杂的cfg
表达式。 - 将代码架构设计成允许同时启用特性,并使用运行时选项来控制使用哪一个。例如,使用配置文件、命令行参数或环境变量来选择启用哪种行为。
检查已解析的特性
在复杂的依赖关系图中,有时很难理解不同特性是如何在各种包上被启用的。cargo tree
命令提供了几个选项来帮助检查和可视化哪些特性被启用。可以尝试的一些选项有:
cargo tree -e features
:这将在依赖关系图中显示特性。每个特性都会显示它是由哪个包启用的。cargo tree -f "{p} {f}"
:这是一种更紧凑的视图,显示每个包上启用的特性列表,特性之间用逗号分隔。cargo tree -e features -i foo
:这将反转树,显示特性是如何流入给定的包“foo”的。这很有用,因为查看整个图可能非常大且令人不知所措。当您试图弄清楚特定包上启用了哪些特性以及原因时,请使用此选项。有关如何阅读此输出,请参阅cargo tree
页面底部的示例。
特性解析器版本 2
可以在 Cargo.toml
中使用 resolver
字段指定不同的特性解析器,如下所示:
[package]
name = "my-package"
version = "1.0.0"
resolver = "2"
有关指定解析器版本的更多详细信息,请参阅解析器版本部分。
版本 "2"
解析器在一些不需要统一特性的情况下避免了统一。具体情况在解析器章节中有描述,但简而言之,它在以下情况中避免统一:
- 对于当前未构建的目标架构,平台特定依赖项上启用的特性会被忽略。
- 构建依赖项和 proc-macro 不与普通依赖项共享特性。
- 开发依赖项除非正在构建需要它们的Cargo 目标(如测试或示例),否则不会激活特性。
在某些情况下,避免统一是必要的。例如,如果构建依赖项启用了 std
特性,并且同一个依赖项在 no_std
环境中用作普通依赖项,那么启用 std
将导致构建失败。
然而,一个缺点是这会增加构建时间,因为依赖项会被多次构建(每次使用不同的特性)。使用版本 "2"
解析器时,建议检查多次构建的依赖项,以减少总体构建时间。如果不是*必须*使用不同的特性构建那些重复的包,考虑在依赖项声明的 features
列表中添加特性,这样重复的依赖项最终会具有相同的特性(因此 Cargo 只会构建一次)。您可以使用cargo tree --duplicates
命令检测这些重复的依赖项。它会显示哪些包被多次构建;查找列出相同版本的条目。有关获取已解析特性的更多信息,请参阅检查已解析的特性。对于构建依赖项,如果您使用 --target
标志进行交叉编译,则无需这样做,因为在这种情况下,构建依赖项总是与普通依赖项分开构建的。
解析器版本 2 命令行标志
resolver = "2"
设置也会改变 --features
和 --no-default-features
命令行选项的行为。
使用版本 "1"
时,您只能为您当前工作目录中的包启用特性。例如,在一个包含 foo
和 bar
包的工作空间中,如果您位于包 foo
的目录中,并运行命令 cargo build -p bar --features bar-feat
,这将失败,因为 --features
标志只允许在 foo
上启用特性。
使用 resolver = "2"
时,特性标志允许为命令行中使用 -p
和 --workspace
标志选择的任何包启用特性。例如:
# This command is allowed with resolver = "2", regardless of which directory
# you are in.
cargo build -p foo -p bar --features foo-feat,bar-feat
# This explicit equivalent works with any resolver version:
cargo build -p foo -p bar --features foo/foo-feat,bar/bar-feat
此外,使用 resolver = "1"
时,--no-default-features
标志仅禁用当前目录中包的默认特性。使用版本“2”时,它将禁用所有工作空间成员的默认特性。
构建脚本
构建脚本可以通过检查 CARGO_FEATURE_<name>
环境变量来检测包上启用了哪些特性,其中 <name>
是特性名称转换为大写且 -
转换为 _
的结果。
所需特性
required-features
字段可用于在未启用特定特性时禁用特定的Cargo 目标。有关更多详细信息,请参阅链接的文档。
SemVer 兼容性
启用特性不应该引入 SemVer 不兼容的更改。例如,特性不应该以可能破坏现有用法的方式更改现有 API。有关哪些更改是兼容的更多详细信息,请参阅SemVer 兼容性章节。
添加和移除特性定义以及可选依赖项时应小心谨慎,因为这些有时可能是向后不兼容的更改。更多详细信息可在 SemVer 兼容性章节的Cargo 部分找到。简而言之,遵循以下规则:
- 以下操作通常可以在次要版本中安全进行:
- 添加新特性或可选依赖项。
- 更改依赖项上使用的特性.
- 以下操作通常不应该在次要版本中进行:
有关注意事项和示例,请参阅链接。
特性文档和发现
建议您记录包中可用的特性。这可以通过在 lib.rs
顶部添加文档注释来实现。例如,请参阅 regex crate 源码,它在渲染后可以在 docs.rs 上查看。如果您有其他文档,例如用户指南,考虑将特性文档添加到其中(例如,请参阅 serde.rs)。如果您的项目是二进制项目,考虑在 README 或项目的其他文档中记录特性(例如,请参阅 sccache)。
清晰地记录特性可以设定关于被视为“不稳定”或不应使用的特性的预期。例如,如果存在一个可选依赖项,但您不希望用户明确将该可选依赖项列为特性,则将其从文档列表中排除。
发布在 docs.rs 上的文档可以使用 Cargo.toml
中的元数据来控制构建文档时启用哪些特性。有关更多详细信息,请参阅docs.rs 元数据文档。
注意:Rustdoc 实验性地支持在文档中添加标注,以指示使用某些 API 需要哪些特性。有关更多详细信息,请参阅
doc_cfg
文档。一个示例是syn
文档,您可以在其中看到彩色框,指示使用它需要哪些特性。
发现特性
当特性在库 API 中有文档记录时,您的用户可以更容易地发现哪些特性可用以及它们的作用。如果包的特性文档不容易获得,您可以查看 Cargo.toml
文件,但这有时很难找到。在 crates.io 上的 crate 页面如果提供了源码仓库链接,可以通过它查看。可以使用 cargo vendor
或 cargo-clone-crate 等工具下载源码并进行检查。
特性组合
因为特性是一种条件编译形式,它们需要指数级的配置和测试用例才能实现 100% 覆盖。默认情况下,测试、文档和其他工具(如 Clippy)只会使用默认特性集运行。
我们鼓励您考虑针对不同特性组合的策略和工具 — 每个项目都会根据时间、资源以及覆盖特定场景的成本效益有不同的要求。常见的配置可能是使用或不使用默认特性、特定的特性组合或所有特性组合。