Skip to content

Commit

Permalink
Merge pull request #64 from yuqiang-yuan/item26-features
Browse files Browse the repository at this point in the history
item 26 done; update link to translated files
  • Loading branch information
lispking authored Sep 19, 2024
2 parents 93f452f + b97f182 commit fbb31ee
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
- [第 23 条:避免通配符导入](./chapter_4/item23-wildcard.md)
- [第 24 条:重新导出在 API 中所用的依赖项类型](./chapter_4/item24-re-export.md)
- [第 25 条:管理依赖项关系图](./chapter_4/item25-dep-graph.md)
- [第 26 条:警惕特征蔓延](./chapter_4/item26-features.md)
- [工具](./chapter_5.md)
- [第 27 条:为公共接口撰写文档](./chapter_5/item27-document-public-interfaces.md)
- [第 28 条:在合适的时候使用宏](./chapter_5/item28-use-macros-judiciously.md)
Expand Down
6 changes: 3 additions & 3 deletions src/chapter_4/item21-semver.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,10 @@ Cargo 在选择依赖项的版本时,会在符合条件的版本中选择最
[《Google 软件工程》(O'Reilly)]: https://abseil.io/resources/swe-book/html/ch21.html#the_limitations_of_semver
[第 12 条]: ../chapter_2/item12-generics&trait-objects.md
[第 13 条]: ../chapter_2/item13-use-default-impl.md
[第 22 条]: https://www.lurklurk.org/effective-rust/visibility.html
[第 22 条]: ./item22-visibility.md
[第 23 条]: ./item23-wildcard.md
[第 25 条]: https://www.lurklurk.org/effective-rust/dep-graph.html
[第 26 条]: https://www.lurklurk.org/effective-rust/features.html
[第 25 条]: ./item25-dep-graph.md
[第 26 条]: ./item26-features.md
[第 31 条]: ../chapter_5/item31-use-tools.md
[第 32 条]: https://www.lurklurk.org/effective-rust/ci.html
[第 35 条]: ../chapter_6/item35-bindgen.md
Expand Down
2 changes: 1 addition & 1 deletion src/chapter_4/item23-wildcard.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,4 @@ use thing::prelude::*;

最后,如果你不打断遵循这个条款的建议,**考虑将你使用通配符引入的依赖项固定到一个特定的版本上(见 [第 21 条]**,让依赖项不会自动升级次要版本。

[第 21 条]:https://www.lurklurk.org/effective-rust/semver.html
[第 21 条]:./item21-semver.md
2 changes: 1 addition & 1 deletion src/chapter_4/item24-re-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ let choice = dep_lib::pick_number_with(&mut prev_rng, max);
[original]: https://www.lurklurk.org/effective-rust/re-export.html
[RustCrypto crates]: https://docs.rs/signature/1.3.0/signature/index.html#reexports
[Item 21]: ./item21-semver.md
[Item 25]: https://www.lurklurk.org/effective-rust/dep-graph.html
[Item 25]: ./item25-dep-graph.md
[path mechanism]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-path-dependencies
[rand-gen-range-0.7]: https://docs.rs/rand/0.7.3/rand/trait.Rng.html#method.gen_range
[rand-gen-range-0.8]: https://docs.rs/rand/0.8.5/rand/trait.Rng.html#method.gen_range
Expand Down
2 changes: 1 addition & 1 deletion src/chapter_4/item25-dep-graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ dep-graph v0.1.0
[crates.io]: https://crates.io/
[Item 21]: ./item21-semver.md
[Item 24]: ./item24-re-export.md
[Item 26]: https://www.lurklurk.org/effective-rust/features.html
[Item 26]: ./item26-features.md
[Item 31]: ../chapter_5/item31-use-tools.md
[Item 32]: https://www.lurklurk.org/effective-rust/ci.html
[Item 33]: https://www.lurklurk.org/effective-rust/no-std.html
Expand Down
211 changes: 211 additions & 0 deletions src/chapter_4/item26-features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# 第 26 条:警惕特征(`feature`)蔓延

通过使用 Cargo 的 *特征(feature)* 机制,Rust 允许同一套代码有不同的配置,其底层基于条件编译机制。但是,关于 Rust 中的特征,有一些值得关注的点,本章将一一阐述。

## 条件编译

通过使用 [`cfg`][cfg] 或者 [`cfg_attr`][cfg_attr] ,Rust 支持[条件编译][conditional compilation] ,可以让你决定函数、代码行、代码块等内容是否包含在编译后的文件中(相对而言,C/C++ 是基于代码行的预处理器)。这里的“条件”可以是一个像 `test` 这种单纯的名字,也可以是类似 `panic = "abort"` 这种名值对的形式。

注意,名值对的形式下,一个名字的值可以有多个:

```rust
// 构建时设置环境变量 `RUSTFLAGS` 如下:
// '--cfg myname="a" --cfg myname="b"'
#[cfg(myname = "a")]
println!("cfg(myname = 'a') is set");
#[cfg(myname = "b")]
println!("cfg(myname = 'b') is set");
```

```
cfg(myname = 'a') is set
cfg(myname = 'b') is set
```

除了前述这种显式指定 `feature` 值之外,更常用的配置项是由工具链自动引入的构建时的目标环境,包括;目标操作系统([`target_os`][target_os])、 CPU 架构([`target_arch`][target_arch])、指针位宽([`target_pointer_width`][target_pointer_width])、字节序([`target_endian`][target_endian])等,通过构建时的目标平台启用对应的特征来实现代码的可移植性。

另外,标准选项 [`target_has_atomic`][target_has_atomic] 是支持多值的,如果目标平台同时支持 32 位和 64 位架构,则 `[cfg(target_has_atomic = "32")]``[cfg(target_has_atomic = "64")]` 同时生效。(关于原子性的更多信息,请参考 O'Reilly 出版的,Mara Bos 所著的 《[Rust Atomics and Locks][Rust Atomics and Locks]》一书的第二章。)

## 特征

通过使用基于 `cfg` 的名值对机制,[Cargo][Cargo] 包管理器提供了[*特征(features)*][features] 选择能力:在构建 crate 的时候,可以有选择地启用所需的函数或者对应的 crate。Cargo 将确保对于每个 crate,都使用所配置的 `feature` 值来进行编译。

这是 Cargo 的特有功能:对于 Rust 的编译器而言,`feature` 只是另一个可配置选项。

截至本文成稿时间,检测所启用的特征的最可靠方式就是查看 [*Cargo.toml*][Cargo.toml] 文件。举例说明,下面这段内容实际上包含了 *6 个*特征:

```toml
[features]
default = ["featureA"]
featureA = []
featureB = []
# 启用 `featureAB` 表示同时启用了 `featureA` 和 `featureB`.
featureAB = ["featureA", "featureB"]
schema = []

[dependencies]
rand = { version = "^0.8", optional = true }
hex = "^0.4"
```

可是,上面的例子中,`[features]` 小节只有 5 个特征啊!因此这里有一些细节需要注意。

首先,`[features]` 中的 `default` 是一个特殊的特征名字,它表示默认启用的特征。当然了,在构建时可以通过 `--no-default-features` 参数忽略默认特征,或者在 *Cargo.toml* 中这样写:

```toml
[dependencies]
somecrate = { version = "^0.3", default-features = false }
```

无论如何,`default` 仍然是一个可以在代码中正常使用的特征名字:

```rust
#[cfg(feature = "default")]
println!("This crate was built with the \"default\" feature enabled.");
#[cfg(not(feature = "default"))]
println!("This crate was built with the \"default\" feature disabled.");
```

在示例的 *Cargo.toml* 中,另一个不明显的特征隐藏在 `[dependencies]` 小节:依赖项 `rand` crate 被标记为 `optional = true`,这就使得 `rand` 成为一个特征名字 [^1]。当示例的 crate 使用 `--features rand` 编译的时候,`rand` 特征将被激活:

```rust
#[cfg(feature = "rand")]
pub fn pick_a_number() -> u8 {
rand::random::<u8>()
}

#[cfg(not(feature = "rand"))]
pub fn pick_a_number() -> u8 {
4 // chosen by fair dice roll.
}
```

虽然 crate 名字是全局的(由 `crates.io` 管理),而特征名字是本地的,但事实上 *crate 和特征共享命名空间* 。因此,**谨慎选择特征名字**,以避免和可能作为特征存在的 crate 名字冲突。虽然 Cargo 支持通过修改 `package`[重命名所引入的 crate][rename] 来避免潜在的冲突问题,但是提前避免冲突总比重命名来的好。

所以你要检查 *Cargo.toml***依赖的crate 的 `[features]`**,同时,还要检查 **`[dependencies]` 中标注为 `optional` 的 crate** 来确认当前 crate 的全部特征名字。如果要启用依赖项的一个特征,需要在 `[dependencies]` 小节增加 `features` 属性:

```toml
[dependencies]
somecrate = { version = "^0.3", features = ["featureA", "rand" ] }
```

上述的依赖项设置对于 `somecrate` 启用了 `featureA``rand` 两个特征。但是,你在 *Cargo.toml* 中指定的特征并不代表针对这个 crate 只启用了这些特征,因为 Cargo 中有[*特征联合*][feature unification]现象:最终构建时实际启用的特征是构建图中针对此 crate 所启用的所有特征的*并集*。换句话说,在上面的例子中,如果引用的某个依赖项也依赖 `somecrate`,并且启用了 `featureB` 特征,那么最终构建的时候,会同时启用 `featureA``featureB` 以及 `rand` 三个特征,以满足各个 crate 的需求 [^2]。这个规则也适用于 `default` 特征,如果你的 create 通过 `default-features = false` 禁用了默认特征,但是构建图中其他依赖项没有显式关闭默认特征,那么最终构建的时候,`default` 特征仍然是启用的。

特征联合的特点意味着多个特征之间应该是**可累加的**。因此 crate 中包含不兼容的特征并不是一个好主意,毕竟我们没有任何办法阻止使用者同时启用这些相互不兼容的特征。

例如,下面的例子中,crate 向外暴露了一个结构体,它的字段是公开访问的。把公开访问的字段设计成特征依赖的就会显得很糟糕:

<div class="ferris"><img src="../images/ferris/not_desired_behavior.svg" width="75" height="75" /></div>

```rust
/// 结构体的字段是公开访问的,
/// 所以使用者可以通过指定字段初始值来构建结构体的实例。
#[derive(Debug)]
pub struct ExposedStruct {
pub data: Vec<u8>,

/// 仅当 `schema` 特征启用的时候,
/// 才需要的额外数据
#[cfg(feature = "schema")]
pub schema: String,
}
```

那么使用这个 crate 的用户可能会存在一些困惑:当构造结构体的实例时,`schema` 字段是否应该填充对应的值?*为了*解决这个问题,他将不得不在 *Cargo.toml* 中启用对应的特征:

```toml
[features]
# `use-schema` 特征启用了 `somecrate` 中的 `schema` 特征
# (为了清晰起见,这里使用了不同的特征名字,
# 实际开发中,大概率是重用原有的特征名字)
use-schema = ["somecrate/schema"]
```

然后,在代码中依赖这个特征:

<div class="ferris"><img src="../images/ferris/not_desired_behavior.svg" width="75" height="75" /></div>

```rust
let s = somecrate::ExposedStruct {
data: vec![0x82, 0x01, 0x01],

// 仅当启用 `somecrate/schema` 特征时,
// 才填充此字段的值
#[cfg(feature = "use_schema")]
schema: "[int int]",
};
```

但是,这并不能涵盖所有的情况:当这段代码没有激活 `somecrate/schema` 特征,但是所用的其他依赖项启用了这个特征,就会导致错误。问题的关键在于,只有拥有该特征的 crate 能够检测到该特征;对于 crate 的用户来说,无法确定 Cargo 是否启用了 `somecrate/schema`。因此,你应该**避免在结构体中对公共字段进行特征隔离(feature-gating)**

类似的考虑也适用于公开的 trait,尤其是暴露出来给其他代码使用的 trait。假设一个 trait 中包含了特征隔离的方法:

<div class="ferris"><img src="../images/ferris/not_desired_behavior.svg" width="75" height="75" /></div>

```rust
/// 为支持 CBOR 序列化的条目设计的 trait
pub trait AsCbor: Sized {
/// 将条目序列化成 CORB 数据
fn serialize(&self) -> Result<Vec<u8>, Error>;

/// 从 CBOR 数据反序列化一个条目
fn deserialize(data: &[u8]) -> Result<Self, Error>;

/// 返回这个条目对应的模式
#[cfg(feature = "schema")]
fn cddl(&self) -> String;
}
```

在项目中使用这个 trait 的用户同样会面临困惑:到底要不要实现 `cddl(&self)` 方法?外部代码根本没有办法得知是否应该实现 trait 中特征隔离的方法。

所以,结论就是:**避免在公共 trait 中设计特征隔离的方法**。包含默认实现的 trait 方法是个例外(见[第 13 条][Item 13]) —— 前提是,外部代码永远不会改写默认实现。

特征联合也意味着,如果你的 crate 包含了 *N* 个特征 [^3],那么可能的特征组合是 *2<sup>N</sup>* 种。为了避免不必要的问题,应在你的 CI 系统中(见[第 32 条][Item 32])通过完备的测试用例(见[第 30 条][Item 30])来涵盖所有的 *2<sup>N</sup>* 种特征组合。

然而,当需要控制向展开后的依赖图(见[第 25 条][Item 25])暴露的内容时,使用可选的特征还是非常有帮助的,尤其是那些可以在 `no_std` 环境(见[第 33 条][Item 33])中使用的偏底层的 crate,通常会包含 `std` 或者 `alloc` 特征,来方便你在标准环境中使用它们。

## 牢记

- 特征和依赖项共享命名空间
- 选择特征名字的时候,应该慎重考虑,以避免和依赖项名字冲突
- 特征应该是可累加的
- 在公开暴露的结构体或者 trait 上,避免使用特征隔离的字段或方法
- 拥有很多相对独立的特征会导致可能的构建配置组合数量过于庞大

原文[点这里][origin]查看

-----

## 注释

[^1]: 这种默认行为可以通过在 `features` 节的其他地方使用 `"dep:<crate>"` 来禁用。详细信息请参考[文档][dep-crate-doc]
[^2]: `cargo tree --edges features` 命令可以帮助你检测哪个 crate 启用了哪些特征,以及为什么要启用。
[^3]: 一个特征可以强制启用另外的特征,在最上面的例子中,`featureAB` 特征同时启用了 `featureA``featureB`



<!-- 参考链接 -->

[origin]: https://www.lurklurk.org/effective-rust/features.html
[dep-crate-doc]: https://doc.rust-lang.org/cargo/reference/features.html#optional-dependencies
[conditional compilation]: https://doc.rust-lang.org/reference/conditional-compilation.html
[cfg]: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg-attribute
[cfg_attr]: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute
[target_os]: https://doc.rust-lang.org/reference/conditional-compilation.html#target_os
[target_arch]: https://doc.rust-lang.org/reference/conditional-compilation.html#target_arch
[target_pointer_width]: https://doc.rust-lang.org/reference/conditional-compilation.html#target_pointer_width
[target_endian]: https://doc.rust-lang.org/reference/conditional-compilation.html#target_endian
[target_has_atomic]: https://doc.rust-lang.org/reference/conditional-compilation.html#target_has_atomic
[Rust Atomics and Locks]: https://marabos.nl/atomics/
[Cargo]: https://doc.rust-lang.org/cargo/index.html
[features]: https://doc.rust-lang.org/cargo/reference/features.html
[Cargo.toml]: https://doc.rust-lang.org/cargo/reference/manifest.html
[rename]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#renaming-dependencies-in-cargotoml
[feature unification]: https://doc.rust-lang.org/cargo/reference/features.html#feature-unification
[Item 13]: ../chapter_2/item13-use-default-impl.md
[Item 25]: ./item25-dep-graph.md
[Item 30]: ../chapter_5/item30-write-more-than-unit-tests.md
[Item 32]: https://www.lurklurk.org/effective-rust/ci.html
[Item 33]: https://www.lurklurk.org/effective-rust/no-std.html

0 comments on commit fbb31ee

Please sign in to comment.