第 23 条:避免通配符导入
Rust 的 use
语句可以从另一个 crate 或者模块中引入一个具名项,使得该项可以在当前模块的代码中不加限定符使用。形如 use somecrate::module::*
的通配符导入 wildcard import(或称 glob import)表示那个模块所有的 public 符号都被添加到了本地的命名空间中。
如 第 21 条 中所述,一个外部 crate 可能会向 API 中添加新的条目作为次要版本升级的一部分;这是一种向后兼容的修改。
这两个场景一旦结合到一起就会产生新的忧虑,依赖项的一个非破坏性更改可能会破坏你的代码:如果依赖项添加了一个与你已经使用的名称冲突的新符号,会发生什么情况?
从最简单的角度来看,这倒不是个问题:通配符导入的符号被视为低优先级,所以在你的代码中使用的匹配的名称都会被优先匹配:
#![allow(unused)] fn main() { use bytes::*; // Local `Bytes` type does not clash with `bytes::Bytes`. // 本地定义的 `Bytes` 类型不会跟 `bytes::Bytes` 发生冲突。 struct Bytes(Vec<u8>); }
不幸的是,仍然存在发生冲突的情况。例如,依赖项添加了一个新的 trait 并且为某些类型实现了:
#![allow(unused)] fn main() { trait BytesLeft { // Name clashes with the `remaining` method on the wildcard-imported // `bytes::Buf` trait. // `remaining` 方法跟通配符导入的 `bytes::Buf` trait 冲突了。 fn remaining(&self) -> usize; } impl BytesLeft for &[u8] { // Implementation clashes with `impl bytes::Buf for &[u8]`. // 实现和 `impl bytes::Buf for &[u8]` 冲突了。 fn remaining(&self) -> usize { self.len() } } }
如果新 trait 中任一个方法的名称与该类型现有的方法名称发生冲突,编译器就没法明确地识别出要调用的是哪个方法:
#![allow(unused)] fn main() { let arr = [1u8, 2u8, 3u8]; let v = &arr[1..]; assert_eq!(v.remaining(), 2); }
就像编译时候的错误显示那样:
#![allow(unused)] fn main() { error[E0034]: multiple applicable items in scope --> src/main.rs:40:18 | 40 | assert_eq!(v.remaining(), 2); | ^^^^^^^^^ multiple `remaining` found | note: candidate #1 is defined in an impl of the trait `BytesLeft` for the type `&[u8]` --> src/main.rs:18:5 | 18 | fn remaining(&self) -> usize { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ = note: candidate #2 is defined in an impl of the trait `bytes::Buf` for the type `&[u8]` help: disambiguate the method for candidate #1 | 40 | assert_eq!(BytesLeft::remaining(&v), 2); | ~~~~~~~~~~~~~~~~~~~~~~~~ help: disambiguate the method for candidate #2 | 40 | assert_eq!(bytes::Buf::remaining(&v), 2); | ~~~~~~~~~~~~~~~~~~~~~~~~~ }
因此,你应该避免从你无法控制的 crate 中进行通配符导入。
如果你可以控制被通配符导入的项目的代码,那么之前提到的问题就消失了。例如,test
模块通常会使用 use super::*;
。对于主要通过模块来划分代码的 crate 来说,从内部模块进行通配符导入也是一种可能的场景:
#![allow(unused)] fn main() { mod thing; pub use thing::*; }
然而,还有另一种常见的例外情况也是适用通配符导入的。有一些 crate 遵循一个约定,crate 中常见的条目会通过 prelude 模块重新导出,而这个就是特地被用于使用通配符导入的:
#![allow(unused)] fn main() { use thing::prelude::*; }
尽管从理论上来讲这种场景也会出现上面提及过的问题,但实际上这种 prelude 模块大多经过精心的设计,这样使用带来的更高的便利性可能会远超过未来出现问题的小概率风险。
最后,如果你不打断遵循这个条款的建议,考虑将你使用通配符引入的依赖项固定到一个特定的版本上(见 第 21 条),让依赖项不会自动升级次要版本。