Rust API 指南
这是关于如何设计和展示 Rust 编程语言的 API 的一系列推荐。这些指南主要由 Rust 库团队撰写,基于构建 Rust 标准库和 Rust 生态系统中其他 crate 的经验。
这些只是指南,其中一些更为严格,而其他的则比较模糊且仍在开发中。Rust crate 作者应将它们视为开发惯用和可互操作的 Rust 库时需要考虑的一系列重要因素,可以根据需要使用它们。这些指南不应被视为 crate 作者必须遵循的强制性规定,尽管他们可能会发现,与这些指南高度兼容的 crate 比那些不兼容的 crate 更好地集成到现有的 crate 生态系统中。
本书分为两部分:适合在 crate 评审期间快速浏览的所有单独指南的简明 [清单];以及包含详细指南解释的专题章节。
如果你有兴趣为 API 指南做出贡献,请查阅 contributing.md 并加入我们的 [Gitter 频道]。
Rust API 指南检查清单
- 命名 (crate 符合 Rust 命名约定)
- 互操作性 (crate 与其他库功能良好交互)
-
类型积极实现常见特性 (C-COMMON-TRAITS)
Copy
,Clone
,Eq
,PartialEq
,Ord
,PartialOrd
,Hash
,Debug
,Display
,Default
-
转换使用标准特性
From
,AsRef
,AsMut
(C-CONV-TRAITS) -
集合实现
FromIterator
和Extend
(C-COLLECT) -
数据结构实现 Serde 的
Serialize
,Deserialize
(C-SERDE) -
类型在可能的情况下是
Send
和Sync
(C-SEND-SYNC) - 错误类型有意义且行为良好 (C-GOOD-ERR)
-
二进制数类型提供
Hex
,Octal
,Binary
格式化 (C-NUM-FMT) -
通用读/写函数按值接收
R: Read
和W: Write
(C-RW-VALUE)
-
类型积极实现常见特性 (C-COMMON-TRAITS)
- 宏 (crate 提供行为良好的宏)
- 输入语法唤起输出 (C-EVOCATIVE)
- 宏与属性良好组合 (C-MACRO-ATTR)
- 项目宏在允许项目的任何地方工作 (C-ANYWHERE)
- 项目宏支持可见性说明符 (C-MACRO-VIS)
- 类型片段灵活 (C-MACRO-TY)
- 文档 (crate 文档丰富)
- crate 级别文档详尽并包含示例 (C-CRATE-DOC)
- 所有项目都有 rustdoc 示例 (C-EXAMPLE)
-
示例使用
?
,而不是try!
或unwrap
(C-QUESTION-MARK) - 函数文档包含错误、恐慌和安全性考虑 (C-FAILURE)
- 文章包含相关内容的超链接 (C-LINK)
-
Cargo.toml 包含所有常见元数据 (C-METADATA)
- 作者、描述、许可证、主页、文档、仓库、关键词、类别
- 发行说明记录所有重大变化 (C-RELNOTES)
- Rustdoc 不显示无用的实现细节 (C-HIDDEN)
- 可预测性 (crate 使代码易读且行为如表面所示)
- 智能指针不添加固有方法 (C-SMART-PTR)
- 转换存在于所涉及的最具体类型上 (C-CONV-SPECIFIC)
- 具有明确接收者的函数是方法 (C-METHOD)
- 函数不接收输出参数 (C-NO-OUT)
- 操作符重载不令人惊讶 (C-OVERLOAD)
-
只有智能指针实现
Deref
和DerefMut
(C-DEREF) - 构造函数是静态的固有方法 (C-CTOR)
- 灵活性 (crate 支持多样的现实使用场景)
- 函数暴露中间结果以避免重复工作 (C-INTERMEDIATE)
- 调用者决定在哪里复制和放置数据 (C-CALLER-CONTROL)
- 函数通过使用泛型最小化对参数的假设 (C-GENERIC)
- 如果特性作为特性对象可能有用,则它们是对象安全的 (C-OBJECT)
- 类型安全 (crate 有效利用类型系统)
- 新类型提供静态区分 (C-NEWTYPE)
-
参数通过类型而不是
bool
或Option
传达意义 (C-CUSTOM-TYPE) -
一组标志的类型是
bitflags
,而不是枚举 (C-BITFLAG) - 构建器使复杂值的构建成为可能 (C-BUILDER)
- 可靠性 (crate 不太可能做错事)
- 函数验证其参数 (C-VALIDATE)
- 析构函数从不失败 (C-DTOR-FAIL)
- 可能阻塞的析构函数有替代方案 (C-DTOR-BLOCK)
- 可调试性 (crate 便于调试)
-
所有公共类型实现
Debug
(C-DEBUG) -
Debug
表示永不为空 (C-DEBUG-NONEMPTY)
-
所有公共类型实现
- 未来保障 (crate 可以在不破坏用户代码的情况下改进)
- 密封特性防止下游实现 (C-SEALED)
- 结构体有私有字段 (C-STRUCT-PRIVATE)
- 新类型封装实现细节 (C-NEWTYPE-HIDE)
- 数据结构不重复派生特性边界 (C-STRUCT-BOUNDS)
- 必要性 (对相关人员而言,它们非常重要)
- 稳定 crate 的公共依赖是稳定的 (C-STABLE)
- crate 及其依赖项具有宽松的许可证 (C-PERMISSIVE)
命名
大小写遵循 RFC 430 (C-CASE)
基本的 Rust 命名约定在 RFC 430 中有描述。
一般来说,Rust 倾向于使用 UpperCamelCase
(大驼峰命名法)用于“类型级”构造(类型和特征),使用 snake_case
(蛇形命名法)用于“值级”构造。更准确地说:
项目 | 约定 |
---|---|
Crates | 不明确 |
Modules | snake_case |
Types | UpperCamelCase |
Traits | UpperCamelCase |
Enum variants | UpperCamelCase |
Functions | snake_case |
Methods | snake_case |
General constructors | new or with_more_details |
Conversion constructors | from_some_other_type |
Macros | snake_case! |
Local variables | snake_case |
Statics | SCREAMING_SNAKE_CASE |
Constants | SCREAMING_SNAKE_CASE |
Type parameters | 简洁的 UpperCamelCase ,通常是单个大写字母:T |
Lifetimes | 简短的 lowercase ,通常是单个字母:'a ,'de ,'src |
Features | 不明确 但是见 C-FEATURE |
在 UpperCamelCase
中,缩略词和复合词的缩写算作一个词:使用 Uuid
而不是 UUID
,Usize
而不是 USize
,或 Stdin
而不是 StdIn
。在 snake_case
中,缩略词和缩写则要小写:is_xid_start
。
在 snake_case
或 SCREAMING_SNAKE_CASE
中,一个“单词”不应该只包含一个字母,除非它是最后一个“单词”。所以,我们有 btree_map
而不是 b_tree_map
,但是有 PI_2
而不是 PI2
。
Crate 名称不应使用 -rs
或 -rust
作为后缀或前缀。每个 crate 都是 Rust!没必要一直提醒用户这一点。
标准库中的示例
整个标准库。这条准则应该很容易!
临时转换遵循 as_
,to_
,into_
约定 (C-CONV)
转换应该以方法的形式提供,其名称根据以下前缀命名:
前缀 | 成本 | 所有权 |
---|---|---|
as_ | 免费 | 借用 -> 借用 |
to_ | 昂贵 | 借用 -> 借用 借用 -> 拥有 (非 Copy 类型) 拥有 -> 拥有 (Copy 类型) |
into_ | 可变 | 拥有 -> 拥有 (非 Copy 类型) |
例如:
str::as_bytes()
提供字符串作为一片 UTF-8 字节视图,这个操作是免费的。输入是借用的&str
,输出是借用的&[u8]
。Path::to_str
在操作系统路径的字节上执行昂贵的 UTF-8 检查。输入和输出都是借用的。称之为as_str
在运行时有非琐碎的成本是不正确的。str::to_lowercase()
生成str
的 Unicode 正确的小写版本,这涉及字符串字符的迭代并可能需要内存分配。输入是借用的&str
,输出是拥有的String
。f64::to_radians()
将浮点数从度转换为弧度。输入是f64
。传递引用&f64
是不必要的,因为f64
的复制成本很低。称此函数为into_radians
会产生误导,因为输入不会被消耗。String::into_bytes()
提取String
的底层Vec<u8>
,这是免费的。它获取String
的所有权并返回拥有的Vec<u8>
。BufReader::into_inner()
获取一个缓冲读取器的所有权,并提取出底层读取器,这是免费的。缓冲的数据会被丢弃。BufWriter::into_inner()
获取一个缓冲写入器的所有权,并提取出底层写入器,这可能需要昂贵的刷新操作来处理任何缓冲数据。
以 as_
和 into_
为前缀的转换通常 降低抽象,要么暴露对底层表示的视图(as
),要么将数据分解为其底层表示(into
)。而以 to_
为前缀的转换通常保持在同一抽象级别,但会进行一些工作以在表示之间进行转换。
当一个类型包装单个值以将其与更高级别的语义关联时,访问被包装的值应通过 into_inner()
方法提供。这适用于提供缓冲的包装器,如 BufReader
,编码或解码如 GzDecoder
,原子访问如 AtomicBool
,或任何类似语义的情况。
如果转换方法名称中的 mut
修饰符是返回类型的一部分,则其应如同其在类型中出现的方式出现。例如 Vec::as_mut_slice
返回 mut 切片;它名副其实。该名称优于 as_slice_mut
。
#![allow(unused)] fn main() { // 返回类型是一个可变切片。 fn as_mut_slice(&mut self) -> &mut [T]; }
更多标准库的示例
Getter 名称遵循 Rust 约定 (C-GETTER)
除少数例外,Rust 代码中的 getter 不使用 get_
前缀。
#![allow(unused)] fn main() { pub struct S { first: First, second: Second, } impl S { // 不是 get_first。 pub fn first(&self) -> &First { &self.first } // 不是 get_first_mut、get_mut_first 或 mut_first。 pub fn first_mut(&mut self) -> &mut First { &mut self.first } } }
get
命名仅在有单一且明显的东西可以通过 getter 获得时使用。例如 Cell::get
用于访问 Cell
的内容。
对于进行运行时验证(如边界检查)的 getter,考虑添加不安全的 _unchecked
变体。通常这些方法具有以下签名:
#![allow(unused)] fn main() { fn get(&self, index: K) -> Option<&V>; fn get_mut(&mut self, index: K) -> Option<&mut V>; unsafe fn get_unchecked(&self, index: K) -> &V; unsafe fn get_unchecked_mut(&mut self, index: K) -> &mut V; }
getters 和转换 (C-CONV) 之间的区别可能很微妙,并不总是很明确。例如 TempDir::path
可以被理解为临时目录的文件系统路径的 getter,而 TempDir::into_path
是一种转换,将删除临时目录的责任转移给调用者。由于 path
是一个 getter,将其称为 get_path
或 as_path
是不正确的。
标准库的示例
std::io::Cursor::get_mut
std::ptr::Unique::get_mut
std::sync::PoisonError::get_mut
std::sync::atomic::AtomicBool::get_mut
std::collections::hash_map::OccupiedEntry::get_mut
<[T]>::get_unchecked
生成迭代器的方法遵循 iter
,iter_mut
,into_iter
约定 (C-ITER)
根据 RFC 199。
对于包含元素类型 U
的容器,迭代器方法应被命名为:
#![allow(unused)] fn main() { fn iter(&self) -> Iter // Iter 实现了 Iterator<Item = &U> fn iter_mut(&mut self) -> IterMut // IterMut 实现了 Iterator<Item = &mut U> fn into_iter(self) -> IntoIter // IntoIter 实现了 Iterator<Item = U> }
此准则适用于概念上同质集合的数据结构。作为反例,str
类型是一片保证为有效 UTF-8 的字节。这在概念上比同质集合更为复杂,因此它没有提供 iter
/iter_mut
/into_iter
一组迭代器方法,而是提供了 str::bytes
以字节迭代和 str::chars
以字符迭代。
这一准则只适用于方法,而不适用于函数。例如,url
crate 中的 percent_encode
返回一个迭代器,具有对百分编码字符串片段的迭代功能。使用 iter
/iter_mut
/into_iter
约定并不会带来清晰性。
标准库的示例
迭代器类型名称与生成它们的方法匹配 (C-ITER-TY)
名为 into_iter()
的方法应返回一个名为 IntoIter
的类型,而其他所有返回迭代器的方法也是如此。
这一准则主要适用于方法,但通常也对函数适用。例如,url
crate 中的 percent_encode
函数返回一个名为 PercentEncode
的迭代器类型。
这些类型名称在其所属模块的前缀下最有意义,例如 vec::IntoIter
。
标准库的示例
Vec::iter
返回Iter
Vec::iter_mut
返回IterMut
Vec::into_iter
返回IntoIter
BTreeMap::keys
返回Keys
BTreeMap::values
返回Values
功能名称不应包含占位符词语 (C-FEATURE)
不要在 Cargo 功能 的名称中包含无意义的词,比如 use-abc
或 with-abc
。直接将功能命名为 abc
。
这在对 Rust 标准库有可选依赖的 crate 中尤为普遍。正确的做法是:
# 在 Cargo.toml 中
[features]
default = ["std"]
std = []
#![allow(unused)] fn main() { // 在 lib.rs 中 #![no_std] #[cfg(feature = "std")] extern crate std; }
不要将功能命名为 use-std
或 with-std
,或其他不具创意且不是 std
的名字。这种命名约定与 Cargo 为可选依赖推断的隐式功能命名一致。考虑 crate x
对 Serde 和 Rust 标准库的可选依赖:
[package]
name = "x"
version = "0.1.0"
[features]
std = ["serde/std"]
[dependencies]
serde = { version = "1.0", optional = true }
当我们依赖 x
时,可以通过 features = ["serde"]
启用可选 Serde 依赖。同样地,我们可以通过 features = ["std"]
启用可选标准库依赖。Cargo 为可选依赖推断的隐式功能名为 serde
,而不是 use-serde
或 with-serde
,因此我们希望显式功能以相同方式工作。
另外需要注意的是,Cargo 要求功能是增量性的,因此像 no-abc
这样的负面命名几乎从来都不正确。
名称使用一致的词序 (C-WORD-ORDER)
以下是标准库中的一些错误类型:
JoinPathsError
ParseBoolError
ParseCharError
ParseFloatError
ParseIntError
RecvTimeoutError
StripPrefixError
以上所有都使用了动词-对象-错误词序。如果我们要添加一个表示地址解析失败的错误,为了一致性,我们应该按照动词-对象-错误词序命名它为 ParseAddrError
,而不是 AddrParseError
。
特定的词序选择并不重要,但要注意内部的统一性,以及与标准库中类似功能的一致性。
互操作性
类型应尽早实现常见的 trait(C-COMMON-TRAITS)
Rust 的 trait 系统不允许存在“孤儿”:大致上,每个 impl
必须存在于定义该 trait 的 crate 或实现该类型的 crate 中。因此,定义新类型的 crate 应该尽早实现所有适用的常见 trait。
为了理解这一点,请考虑以下情况:
- Crate
std
定义了 traitDisplay
。 - Crate
url
定义了类型Url
,但没有实现Display
。 - Crate
webapp
从std
和url
中导入了内容。
由于 webapp
既不定义 Display
也不定义 Url
,因此它无法为 Url
添加 Display
。 (注意:newtype 模式可以提供一种有效但不便的解决方法。)
最重要的常见 trait 实现来自 std
:
请注意,通常类型同时实现 Default
和空的 new
构造函数是常见且预期的。new
是 Rust 中的构造函数惯例,用户希望它存在,所以如果基本构造函数不需要参数,那么即使它与 default
功能相同,也应该存在。
转换使用标准的 From
、AsRef
、AsMut
trait(C-CONV-TRAITS)
在合适的情况下,应该实现以下转换 trait:
以下转换 trait 不应被实现:
这些 trait 基于 From
和 TryFrom
提供了通用实现。应优先实现这些。
标准库中的示例
From<u16>
实现了u32
,因为较小的整数总是可以转换为较大的整数。From<u32>
并未为u16
实现,因为如果整数太大,转换可能无法完成。TryFrom<u32>
为u16
实现,当整数太大而无法容纳在u16
中时返回错误。From<Ipv6Addr>
实现了IpAddr
,它是一种可以表示 v4 和 v6 IP 地址的类型。
集合实现 FromIterator
和 Extend
(C-COLLECT)
FromIterator
和 Extend
使集合可以方便地与以下迭代器方法一起使用:
FromIterator
用于创建一个包含迭代器中项的新集合,而 Extend
则用于将迭代器中的项添加到现有集合中。
标准库中的示例
Vec<T>
实现了FromIterator<T>
和Extend<T>
。
数据结构实现 Serde 的 Serialize
和 Deserialize
(C-SERDE)
扮演数据结构角色的类型应实现 Serialize
和 Deserialize
。
从数据结构到非数据结构存在一个连续体,其中有一部分是灰色区域。LinkedHashMap
和 IpAddr
是数据结构。有人希望从 JSON 文件中读取 LinkedHashMap
或 IpAddr
,或者通过 IPC 将其发送到另一个进程,这是完全合理的。而 LittleEndian
不是数据结构。它是 byteorder
crate 使用的一种标记,用于在编译时针对特定字节顺序进行优化,实际上 LittleEndian
的实例在运行时永远不会存在。这些是明确的例子;#rust 或 #serde IRC 频道可以帮助评估更多模棱两可的情况。
如果一个 crate 没有因为其他原因依赖于 Serde,它可以选择通过 Cargo 配置选项来控制 Serde 的实现。这样,下游库只需要在需要这些实现时支付编译 Serde 的代价。
为了与其他基于 Serde 的库保持一致,Cargo 配置的名称应为 "serde"
。不要使用 serde_impls
或 serde_serialization
等不同的名称。
在不使用派生的情况下,规范的实现看起来像这样:
[dependencies]
serde = { version = "1.0", optional = true }
#![allow(unused)] fn main() { pub struct T { /* ... */ } #[cfg(feature = "serde")] impl Serialize for T { /* ... */ } #[cfg(feature = "serde")] impl<'de> Deserialize<'de> for T { /* ... */ } }
使用派生时:
[dependencies]
serde = { version = "1.0", optional = true, features = ["derive"] }
#![allow(unused)] fn main() { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct T { /* ... */ } }
类型在可能的情况下实现 Send
和 Sync
(C-SEND-SYNC)
当编译器确定合适时,Send
和 Sync
会自动实现。
在操作原始指针的类型中,要谨慎确保你的类型的 Send
和 Sync
状态准确反映其线程安全特性。类似于以下的测试可以帮助捕捉类型是否实现 Send
或 Sync
的非预期回归。
#![allow(unused)] fn main() { #[test] fn test_send() { fn assert_send<T: Send>() {} assert_send::<MyStrangeType>(); } #[test] fn test_sync() { fn assert_sync<T: Sync>() {} assert_sync::<MyStrangeType>(); } }
错误类型应具有意义且表现良好(C-GOOD-ERR)
错误类型是任何 Result<T, E>
中的 E
类型,它由你的 crate 的任何公共函数返回。错误类型应始终实现 std::error::Error
trait,这是错误处理库如 error-chain
抽象不同错误类型的机制,并且允许错误作为另一个错误的 source()
。
宏
输入语法能引发对输出的联想 (C-EVOCATIVE)
Rust 宏允许你设计出几乎任何形式的输入语法。尽量保持输入语法与用户代码中其他部分一致,通过尽可能地模仿现有的 Rust 语法,使其更具亲和力和连贯性。注意关键词和标点符号的选择与放置。
一个好的指引是使用类似于宏输出结果的语法,特别是关键词和标点符号。
例如,如果你的宏在输入中声明了一个具有特定名称的结构体,请在该名称前加上关键词 struct
,以向读者标明正在定义一个具有给定名称的结构体。
#![allow(unused)] fn main() { // 优先这样写... bitflags! { struct S: u32 { /* ... */ } } // ...而不是省略关键词... bitflags! { S: u32 { /* ... */ } } // ...或使用随意的词。 bitflags! { flags S: u32 { /* ... */ } } }
另一个例子是分号 vs 逗号。在 Rust 中,常量后用分号,所以如果你的宏声明了一连串常量,即使语法略有不同,也应当使用分号。
#![allow(unused)] fn main() { // 普通常量使用分号。 const A: u32 = 0b000001; const B: u32 = 0b000010; // 所以更建议这样写... bitflags! { struct S: u32 { const C = 0b000100; const D = 0b001000; } } // ...而不是这样。 bitflags! { struct S: u32 { const E = 0b010000, const F = 0b100000, } } }
宏的多样性使得这些具体例子可能不适用,但请考虑如何在你的情况下运用相同的原则。
项目宏与属性很好地组合 (C-MACRO-ATTR)
生成多个输出项目的宏应支持为任何一个输出项目添加属性。一个常见的用法是通过 cfg 置于单个项目后。
#![allow(unused)] fn main() { bitflags! { struct Flags: u8 { #[cfg(windows)] const ControlCenter = 0b001; #[cfg(unix)] const Terminal = 0b010; } } }
生成结构体或枚举作为输出的宏应该支持属性,以便输出可以与派生属性一起使用。
#![allow(unused)] fn main() { bitflags! { #[derive(Default, Serialize)] struct Flags: u8 { const ControlCenter = 0b001; const Terminal = 0b010; } } }
项目宏可在任何允许放置项目的地方运行 (C-ANYWHERE)
Rust 允许项目放置在模块级别或更紧凑的范围内,如函数内。项目宏应该在这些地方与普通项目一样正常工作。测试套件应包括在至少模块范围和函数范围内调用宏的情况。
#![allow(unused)] fn main() { #[cfg(test)] mod tests { test_your_macro_in_a!(module); #[test] fn anywhere() { test_your_macro_in_a!(function); } } }
一个简单的错误示例是,这个宏在模块范围内工作良好,但在函数范围内失败。
#![allow(unused)] fn main() { macro_rules! broken { ($m:ident :: $t:ident) => { pub struct $t; pub mod $m { pub use super::$t; } } } broken!(m::T); // 好的,展开为 T 和 m::T fn g() { broken!(m::U); // 编译失败,super::U 指向的是包含的模块而不是 g } }
项目宏支持可见性说明符 (C-MACRO-VIS)
遵循 Rust 语法,宏生成的项目默认是私有的,如果指定 pub
则是公有的。
#![allow(unused)] fn main() { bitflags! { struct PrivateFlags: u8 { const A = 0b0001; const B = 0b0010; } } bitflags! { pub struct PublicFlags: u8 { const C = 0b0100; const D = 0b1000; } } }
类型片段是灵活的 (C-MACRO-TY)
如果你的宏在输入中接受类型片段如 $t:ty
,它应能与以下所有内容一起使用:
- 基本类型:
u8
,&str
- 相对路径:
m::Data
- 绝对路径:
::base::Data
- 向上相对路径:
super::Data
- 泛型:
Vec<String>
一个简单的错误示例是,这个宏在使用基本类型和绝对路径时工作良好,但在使用相对路径时失败。
#![allow(unused)] fn main() { macro_rules! broken { ($m:ident => $t:ty) => { pub mod $m { pub struct Wrapper($t); } } } broken!(a => u8); // 好的 broken!(b => ::std::marker::PhantomData<()>); // 好的 struct S; broken!(c => S); // 编译失败 }
文档
crate 级别的文档应详尽并包含示例 (C-CRATE-DOC)
请参阅 RFC 1687。
所有条目都应有一个 rustdoc 示例 (C-EXAMPLE)
每个公共模块、trait、struct、enum、函数、方法、宏和类型定义都应有一个示例来展示其功能。
该指南应在合理的范围内应用。
链接到另一个条目的相关示例可能是足够的。例如,如果只有一个函数使用了某个特定类型,那么在该函数或类型上编写一个示例并从另一个条目中链接到它可能是合适的。
示例的目的并不总是为了展示如何使用该条目。读者可以预期能够理解如何调用函数、匹配枚举以及其他基础任务。相反,示例通常是为了展示为什么有人会想要使用该条目。
// 这是一个使用 clone() 的糟糕示例。它机械地展示了*如何*调用 clone(),但没有展示*为什么*有人会需要这样做。 fn main() { let hello = "hello"; hello.clone(); }
示例应使用 ?
,而不是 try!
,也不是 unwrap
(C-QUESTION-MARK)
无论喜不喜欢,用户通常会逐字逐句地复制示例代码。解包错误(unwrap)应是用户需要自行做出的有意识决策。
编写可能失败的示例代码的一个常见结构如下所示。以 #
开头的行在构建示例时由 cargo test
编译,但不会出现在用户可见的 rustdoc 中。
/// ```rust
/// # use std::error::Error;
/// #
/// # fn main() -> Result<(), Box<dyn Error>> {
/// your;
/// example?;
/// code;
/// #
/// # Ok(())
/// # }
/// ```
函数文档应包含错误、panic 和安全性考虑 (C-FAILURE)
错误情况应在“Errors”部分中记录。这也适用于 trait 方法——对于那些允许或预期返回错误的实现,应该在“Errors”部分中进行记录。
例如,在标准库中,某些 std::io::Read::read
trait 方法的实现可能会返回错误。
/// 从该源中读取一些字节到指定的缓冲区中,并返回读取的字节数。
///
/// ... 更多信息 ...
///
/// # Errors
///
/// 如果此函数遇到任何形式的 I/O 或其他错误,将返回一个错误变体。如果返回错误,则必须确保没有字节被读取。
panic 情况应在“Panics”部分中记录。这也适用于 trait 方法——对于那些允许或预期会 panic 的实现,应在“Panics”部分中进行记录。
在标准库中,Vec::insert
方法可能会 panic。
/// 在向量中的 `index` 位置插入一个元素,并将其后的所有元素向右移动。
///
/// # Panics
///
/// 如果 `index` 超出范围,将会 panic。
没有必要记录所有可能的 panic 情况,尤其是在 panic 发生在调用者提供的逻辑中。例如,记录以下代码中的 Display
panic 似乎是多余的。但在不确定的情况下,宁可多记录 panic 情况。
#![allow(unused)] fn main() { /// # Panics /// /// 如果 `T` 的 `Display` 实现 panic,该函数将 panic。 pub fn print<T: Display>(t: T) { println!("{}", t.to_string()); } }
不安全函数应在“Safety”部分中记录,解释调用者在正确使用该函数时需要遵守的所有不变性。
不安全的 std::ptr::read
要求调用者遵守以下规则。
/// 从 `src` 读取值,而不移动它。这将保持 `src` 中的内存不变。
///
/// # Safety
///
/// 除了接受一个原始指针外,这个函数是不安全的,因为它在语义上将值从 `src` 中移出,而不阻止 `src` 的进一步使用。
/// 如果 `T` 不是 `Copy`,则必须确保在数据被再次覆盖之前(例如使用 `write`、`zero_memory` 或 `copy_memory`),不要使用 `src` 中的值。请注意,`*src = foo` 也算作使用,因为它将尝试删除之前在 `*src` 处的值。
///
/// 指针必须对齐;如果不是这种情况,请使用 `read_unaligned`。
文章中应包含到相关内容的超链接 (C-LINK)
常规链接可以使用通常的 Markdown 语法 [text](url)
内联添加。可以通过使用 [`text`]
标记的方式添加到其他类型的链接,然后在文档字符串末尾用 [`text`]: <target>
添加链接目标,其中 <target>
如下所述。
指向同一类型中的方法的链接目标通常如下所示:
[`serialize_struct`]: #method.serialize_struct
指向其他类型的链接目标通常如下所示:
[`Deserialize`]: trait.Deserialize.html
链接目标也可以指向父模块或子模块:
[`Value`]: ../enum.Value.html
[`DeserializeOwned`]: de/trait.DeserializeOwned.html
此指南由 RFC 1574 正式推荐,标题为 "Link all the things"。
Cargo.toml 应包含所有常见的元数据 (C-METADATA)
Cargo.toml
的 [package]
部分应包括以下值:
authors
description
license
repository
keywords
categories
此外,还有两个可选的元数据字段:
documentation
homepage
默认情况下,crates.io 会链接到 docs.rs 上的 crate 文档。只有当文档托管在 docs.rs 以外的地方时,才需要设置 documentation
元数据,例如因为 crate 链接到 docs.rs 构建环境中不可用的共享库。
只有当 crate 有一个独立于源代码仓库或 API 文档的独特网站时,才应设置 homepage
元数据。不要让 homepage
与 documentation
或 repository
值重复。例如,serde 将 homepage
设置为 https://serde.rs,这是一个专用网站。
发布说明应记录所有重要更改 (C-RELNOTES)
crate 的用户可以阅读发布说明以找到每个发布版本中的更改摘要。发布说明的链接或发布说明本身应包含在 crate 级文档和/或 Cargo.toml 中链接的仓库中。
发布说明中应清楚标识破坏性更改(根据 RFC 1105 定义)。
如果使用 Git 来跟踪 crate 的源代码,则发布到 crates.io 的每个版本都应有一个对应的标签,标识已发布的提交。对于非 Git 的版本控制工具,也应采用类似的流程。
# 标记当前提交
GIT_COMMITTER_DATE=$(git log -n1 --pretty=%aD) git tag -a -m "Release 0.3.0" 0.3.0
git push --tags
推荐使用带注释的标签,因为如果存在带注释的标签,有些 Git 命令会忽略未注释的标签。
示例
Rustdoc 不应显示无用的实现细节 (C-HIDDEN)
Rustdoc 应该包含用户完整使用 crate 所需的所有内容,但不应包含更多内容
可预测性
智能指针不添加固有方法 (C-SMART-PTR)
例如,这就是 Box::into_raw
函数定义方式的原因。
#![allow(unused)] fn main() { impl<T> Box<T> where T: ?Sized { fn into_raw(b: Box<T>) -> *mut T { /* ... */ } } let boxed_str: Box<str> = /* ... */; let ptr = Box::into_raw(boxed_str); }
如果将其定义为一个固有方法,那么在调用点就会产生混淆,不知道被调用的方法是 Box<T>
上的方法还是 T
上的方法。
#![allow(unused)] fn main() { impl<T> Box<T> where T: ?Sized { // 不要这样做。 fn into_raw(self) -> *mut T { /* ... */ } } let boxed_str: Box<str> = /* ... */; // 这是通过智能指针 Deref 实现访问的 str 上的方法。 boxed_str.chars() // 这是 Box<str> 上的方法...? boxed_str.into_raw() }
转换发生在涉及的最具体的类型上 (C-CONV-SPECIFIC)
在有疑问时,优先使用 to_
/as_
/into_
而不是 from_
,因为它们更符合使用习惯(并且可以与其他方法链式调用)。
对于两种类型之间的许多转换,其中一种类型显然更加“具体”:它提供了一些在其他类型中不存在的附加不变性或解释。例如,str
比 &[u8]
更具体,因为它是一个 UTF-8 编码的字节序列。
转换应当存在于涉及的类型中更具体的那个上。因此,str
提供了 as_bytes
方法和 from_utf8
构造器用于与 &[u8]
值之间的转换。除了直观之外,这种约定还避免了使用具体类型如 &[u8]
被无穷尽的转换方法污染。
具有明确接收对象的函数是方法 (C-METHOD)
倾向于
#![allow(unused)] fn main() { impl Foo { pub fn frob(&self, w: widget) { /* ... */ } } }
而非
#![allow(unused)] fn main() { pub fn frob(foo: &Foo, w: widget) { /* ... */ } }
用于任何明显与特定类型关联的操作。
方法相较于函数有很多优势:
- 它们不需要导入或限定即可使用:你只需要一个合适类型的值。
- 它们的调用执行自动借用(包括可变借用)。
- 它们使回答“我可以用类型
T
的值做什么”这一问题变得容易(特别是在使用 rustdoc 时)。 - 它们提供
self
表示法,更简洁且通常更清晰地传达所有权区别。
函数不接受输出参数 (C-NO-OUT)
倾向于
#![allow(unused)] fn main() { fn foo() -> (Bar, Bar) }
而非
#![allow(unused)] fn main() { fn foo(output: &mut Bar) -> Bar }
用于返回多个 Bar
值。
诸如元组和结构体的复合返回类型编译得非常高效,不需要堆分配。如果一个函数需要返回多个值,应通过这些类型之一来返回。
主要的例外情况:有时候函数旨在修改调用者已经拥有的数据,例如重用一个缓冲区:
#![allow(unused)] fn main() { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> }
操作符重载应不令人惊讶 (C-OVERLOAD)
带有内建语法(*
, |
, 等)的操作符可以通过实现 std::ops
中的特征为类型提供。这些操作符伴随着强烈的期望:仅为与乘法有某种相似性的运算(并共享期望的属性, 如结合律)实现 Mul
,其他特征同理。
只有智能指针实现 Deref
和 DerefMut
(C-DEREF)
Deref
特征在很多情况下被编译器隐式使用,并与方法解析交互。相关规则专门设计用于适应智能指针,因此这些特征应仅用于此目的。
标准库中的示例
构造函数是静态的、固有的方法 (C-CTOR)
在 Rust 中,“构造函数”只是一个约定。关于构造函数命名有各种约定,区别往往微妙。
最基本形式的构造函数是一个无参数的 new
方法。
#![allow(unused)] fn main() { impl<T> Example<T> { pub fn new() -> Example<T> { /* ... */ } } }
构造函数是为它们所构造的类型定义的静态(无 self
)固有方法。结合完全导入类型名称的做法,这种约定可导致信息丰富但简洁的构造:
#![allow(unused)] fn main() { use example::Example; // 构造一个新的 Example。 let ex = Example::new(); }
名称 new
通常用于实例化类型的主要方法。有时它不带参数,如上例。有时它确实带有参数,如将值放入 Box
的 Box::new
。
一些类型的构造函数,尤其是 I/O 资源类型,使用与其构造函数相对应的命名约定,如 File::open
、Mmap::open
、TcpStream::connect
和 UdpSocket::bind
。在这些情况下,名称是根据具体领域选择的。
通常,有多种方式构造一个类型。在这些情况下,辅助构造函数常常带有 _with_foo
后缀,如 Mmap::open_with_offset
。如果你的类型有多种构造选项,请考虑使用构建器模式 (C-BUILDER)。
一些构造函数是“转换构造函数”,即从其他类型的现有值创建新类型的方法。它们通常具有以 from_
开头的名称,如 std::io::Error::from_raw_os_error
。但也请注意 From
特征 (C-CONV-TRAITS),它与此非常相似。from_
前缀的转换构造函数和 From<T>
实现之间有三个区别:
from_
构造函数可以是 unsafe 的;而From
实现不能。例如Box::from_raw
。from_
构造函数可以接受附加参数来消除源数据的意义,以u64::from_str_radix
为例。From
实现仅适用于当源数据类型足以确定输出数据类型的编码时。当输入只是数据包,如在u64::from_be
或String::from_utf8
中,转换构造函数名称能够标识其意义。
注意,通常且预期的做法是类型同时实现 Default
和一个 new
构造函数。对于同时拥有这两者的类型,它们应具有相同的行为。任何一个都可以通过另一个来实现。
标准库中的示例
std::io::Error::new
是用于 IO 错误的常用构造函数。std::io::Error::from_raw_os_error
是基于从操作系统接收到的错误代码的转换构造函数。Box::new
创建一个新容器类型,接受一个参数。File::open
打开一个文件资源。Mmap::open_with_offset
打开一个内存映射文件,并带有附加选项。
灵活性
函数暴露中间结果以避免重复工作 (C-INTERMEDIATE)
许多函数在回答问题的同时也计算了相关的有趣数据。如果这些数据对客户端可能有用,应该考虑在 API 中暴露这些数据。
标准库中的示例
-
Vec::binary_search
不返回一个表示是否找到值的bool
,也不返回一个表示可能找到值的索引的Option<usize>
。它返回的是有关索引的信息,如果找到则返回索引,如果没找到则返回插入值所需的索引。 -
String::from_utf8
如果输入字节不是 UTF-8,则可能会失败。在错误情况下,它返回一个中间结果,该结果显示输入中有效 UTF-8 的字节偏移量,并返还输入字节的所有权。 -
HashMap::insert
返回一个Option<T>
,它返回给定键的预存在值,如果有的话。对于用户想要恢复这个值的情况,由插入操作返回它可以避免用户进行第二次哈希表查找。
调用者决定复制和放置数据的位置 (C-CALLER-CONTROL)
如果函数需要参数的所有权,它应该接收参数的所有权,而不是借用和克隆参数。
#![allow(unused)] fn main() { // 优先这样: fn foo(b: Bar) { /* 直接使用 b 的所有权 */ } // 而不是这样: fn foo(b: &Bar) { let b = b.clone(); /* 克隆后使用 b 的所有权 */ } }
如果函数不需要参数的所有权,它应该借用参数,而不是获取所有权并丢弃参数。
#![allow(unused)] fn main() { // 优先这样: fn foo(b: &Bar) { /* 借用 b 使用 */ } // 而不是这样: fn foo(b: Bar) { /* 借用 b 使用,函数返回前会隐式丢弃 */ } }
只有在绝对需要时才应将 Copy
trait 用作约束,而不是作为标记副本应该容易制作的方式。
函数通过使用泛型来最小化对参数的假设 (C-GENERIC)
函数对其输入做出的假设越少,其可用性就越广。
优先
#![allow(unused)] fn main() { fn foo<I: IntoIterator<Item = i64>>(iter: I) { /* ... */ } }
而不是任何以下实现
#![allow(unused)] fn main() { fn foo(c: &[i64]) { /* ... */ } fn foo(c: &Vec<i64>) { /* ... */ } fn foo(c: &SomeOtherCollection<i64>) { /* ... */ } }
如果函数只需要迭代数据。
更一般地,考虑使用泛型来准确地点出函数需要对其参数进行的假设。
泛型的优势
-
重用性。泛型函数可以应用于一个开放式的类型集合,同时为这些类型需要提供的功能提供一个清晰的契约。
-
静态调度和优化。每次使用泛型函数时,都会专门为实现 trait 约束的特定类型进行“单态化”,这意味着 (1) trait 方法的调用为静态的、直接的实现调用,(2)编译器可以内联和进一步优化这些调用。
-
内联布局。如果一个
struct
和enum
类型是某个类型参数T
的泛型,T
类型的值将在struct
/enum
中内联布局,而没有任何间接性。 -
推断。由于通常可以推断给泛型函数的类型参数,泛型函数可以帮助减少代码中的冗长,通常无需显式转换或其他方法调用。
-
精确类型。因为泛型为实现一个 trait 的特定类型提供了一个_名称_,所以可以准确地表示需要或产生该确切类型的地方。例如,一个函数
#![allow(unused)] fn main() { fn binary<T: Trait>(x: T, y: T) -> T }
可以保证使用和返回完全相同类型
T
的元素;不能用不同类型的参数调用此函数,即使这些类型都实现了Trait
。
泛型的劣势
-
代码大小。专用的泛型函数意味着函数主体被复制。必须权衡代码大小的增加与静态调度的性能收益之间的关系。
-
同质类型。这是“精确类型”问题的另一面:如果
T
是一个类型参数,它代表一个_单一_实际类型。因此,例如,Vec<T>
包含单一具体类型的元素(实际上,矢量表示被专门用于内联这些元素)。有时,异类集合是有用的;见 trait 对象。 -
签名冗长。大量使用泛型可能使函数的签名更难以阅读和理解。
标准库中的示例
std::fs::File::open
接受一个通用类型AsRef<Path>
的参数。这允许通过字符串字面量"f.txt"
、Path
、OsString
和一些其他类型方便地打开文件。
具有潜在用途的 trait 对象是对象安全的 (C-OBJECT)
trait 对象有一些显著的限制:通过 trait 对象调用的方法不能使用泛型,并且不能在接收者位置之外使用 Self
。
设计 trait 时,及早决定是将它用作对象还是用作泛型的约束。
如果一个 trait 是被用作对象,其方法应该接收和返回 trait 对象而不是使用泛型。
where
子句 Self: Sized
可用于将特定方法从 trait 的对象中排除出去。以下 trait 由于泛型方法而不是对象安全的。
#![allow(unused)] fn main() { trait MyTrait { fn object_safe(&self, i: i32); fn not_object_safe<T>(&self, t: T); } }
为泛型方法添加 Self: Sized
要求,将其从 trait 对象中排除,使 trait 对象安全。
#![allow(unused)] fn main() { trait MyTrait { fn object_safe(&self, i: i32); fn not_object_safe<T>(&self, t: T) where Self: Sized; } }
trait 对象的优点
- 异构性。当你需要它时,你真的需要它。
- 代码大小。与泛型不同,trait 对象不生成专用(单态化)的代码版本,这可以大大减少代码大小。
trait 对象的缺点
- 无泛型方法。trait 对象目前不能提供泛型方法。
- 动态调度和胖指针。trait 对象本身涉及间接性和 vtable 调度,这可能带来性能上的代价。
- 无 Self。除接收者参数以外,trait 对象上的方法不能使用
Self
类型。
标准库中的示例
io::Read
和io::Write
trait 经常用作对象。Iterator
trait 有几个用where Self: Sized
标记的泛型方法,以保留将Iterator
用作对象的能力。
类型安全
新类型提供静态区分 (C-NEWTYPE)
新类型可以静态地区分底层类型的不同解释。
例如,一个 f64
值可能用来表示以英里或公里为单位的数量。通过使用新类型,我们可以跟踪预期的解释:
#![allow(unused)] fn main() { struct Miles(pub f64); struct Kilometers(pub f64); impl Miles { fn to_kilometers(self) -> Kilometers { /* ... */ } } impl Kilometers { fn to_miles(self) -> Miles { /* ... */ } } }
一旦我们将这两种类型分开,我们就可以静态地确保不会混淆它们。例如,以下函数
#![allow(unused)] fn main() { fn are_we_there_yet(distance_travelled: Miles) -> bool { /* ... */ } }
不能被意外地传入一个 Kilometers
值。编译器会提醒我们进行转换,从而避免某些灾难性的错误。
参数通过类型传达意义,而不是 bool
或 Option
(C-CUSTOM-TYPE)
推荐使用
#![allow(unused)] fn main() { let w = Widget::new(Small, Round) }
而不是
#![allow(unused)] fn main() { let w = Widget::new(true, false) }
bool
、u8
和 Option
等核心类型有许多可能的解释。
使用明确的类型(无论是枚举、结构体还是元组)来传达解释和不变量。在上面的例子中,如果不查找参数名称,很难立即理解 true
和 false
所表达的含义,而 Small
和 Round
则更具暗示性。
使用自定义类型使得以后扩展选项变得更加容易,例如通过添加 ExtraLarge
变体。
有关包装现有类型并赋予其区别名称的无成本方法,请参见新类型模式 (C-NEWTYPE)。
标志集的类型应使用 bitflags
,而不是枚举 (C-BITFLAG)
Rust 支持具有显式指定判别值的 enum
类型:
#![allow(unused)] fn main() { enum Color { Red = 0xff0000, Green = 0x00ff00, Blue = 0x0000ff, } }
当 enum
类型需要与其他系统/语言兼容地序列化为整数值时,自定义判别值非常有用。它们支持“类型安全”的 API:通过接受 Color
类型而不是整数,函数可以确保获得格式良好的输入,即使稍后将这些输入视为整数。
一个 enum
允许 API 从多个选项中准确请求一个选择。而有时,API 的输入是多个标志的存在或不存在。在 C 代码中,这通常通过让每个标志对应一个特定位来实现,从而允许一个整数表示 32 或 64 个标志。Rust 的 bitflags
crate 提供了这种模式的类型安全表示。
use bitflags::bitflags; bitflags! { struct Flags: u32 { const FLAG_A = 0b00000001; const FLAG_B = 0b00000010; const FLAG_C = 0b00000100; } } fn f(settings: Flags) { if settings.contains(Flags::FLAG_A) { println!("执行操作 A"); } if settings.contains(Flags::FLAG_B) { println!("执行操作 B"); } if settings.contains(Flags::FLAG_C) { println!("执行操作 C"); } } fn main() { f(Flags::FLAG_A | Flags::FLAG_C); }
构建器支持复杂值的构建 (C-BUILDER)
有些数据结构由于其构建需要:
- 大量输入
- 复合数据(例如切片)
- 可选的配置数据
- 在多种模式之间进行选择
这很容易导致大量的独立构造函数,每个构造函数都有许多参数。
如果 T
是这样一个数据结构,可以考虑引入一个 T
构建器:
- 引入一个单独的数据类型
TBuilder
,用于逐步配置一个T
值。如果可能的话,选择一个更好的名称:例如Command
是一个 子进程 的构建器,Url
可以从ParseOptions
创建。 - 构建器的构造函数应该只接受构造
T
所需的数据作为参数。 - 构建器应提供一套方便的方法进行配置,包括逐步设置复合输入(如切片)。这些方法应返回
self
以允许链式调用。 - 构建器应提供一个或多个“终端”方法来实际构建一个
T
。
当构建一个 T
涉及副作用(如启动任务或进程)时,构建器模式尤其合适。
在 Rust 中,有两种不同的构建器模式,区别在于所有权的处理,如下所述。
非消费型构建器(首选)
在某些情况下,构建最终的 T
并不需要消费构建器本身。以下是 std::process::Command
的一个示例:
#![allow(unused)] fn main() { // 注意:实际的 Command API 并不使用拥有的字符串; // 这是一个简化版本。 pub struct Command { program: String, args: Vec<String>, cwd: Option<String>, // 等等 } impl Command { pub fn new(program: String) -> Command { Command { program: program, args: Vec::new(), cwd: None, } } /// 添加一个传递给程序的参数。 pub fn arg(&mut self, arg: String) -> &mut Command { self.args.push(arg); self } /// 添加多个传递给程序的参数。 pub fn args(&mut self, args: &[String]) -> &mut Command { self.args.extend_from_slice(args); self } /// 设置子进程的工作目录。 pub fn current_dir(&mut self, dir: String) -> &mut Command { self.cwd = Some(dir); self } /// 以子进程的形式执行命令,并返回子进程。 pub fn spawn(&self) -> io::Result<Child> { /* ... */ } } }
注意,spawn
方法实际上使用构建器配置来启动进程,它通过共享引用来获取构建器。这是可能的,因为启动进程并不需要配置数据的所有权。
由于终端 spawn
方法只需要引用,配置方法接受并返回 self
的可变借用。
优点
通过在整个过程中使用借用,Command
可以方便地用于一行代码和更复杂的构建:
#![allow(unused)] fn main() { // 一行代码 Command::new("/bin/cat").arg("file.txt").spawn(); // 复杂配置 let mut cmd = Command::new("/bin/ls"); if size_sorted { cmd.arg("-S"); } cmd.arg("."); cmd.spawn(); }
消费型构建器
有时构建器在构建最终类型 T
时必须转移所有权,这意味着终端方法必须接受 self
而不是 &self
。
#![allow(unused)] fn main() { impl TaskBuilder { /// 为即将创建的任务命名。 pub fn named(mut self, name: String) -> TaskBuilder { self.name = Some(name); self } /// 重定向任务本地的标准输出。 pub fn stdout(mut self, stdout: Box<io::Write + Send>) -> TaskBuilder { self.stdout = Some(stdout); self } /// 创建并执行一个新的子任务。 pub fn spawn<F>(self, f: F) where F: FnOnce() + Send { /* ... */ } } }
这里,stdout
配置涉及传递 io::Write
的所有权,该所有权必须在构建时转移到任务中(在 spawn
中)。
当构建器的终端方法需要所有权时,存在一个基本的权衡:
-
如果其他构建器方法接受/返回可变借用,复杂配置情况会很好处理,但一行代码的配置变得不可能。
-
如果其他构建器方法接受/返回拥有的
self
,一行代码的配置仍然可以正常工作,但复杂配置变得不太方便。
在使简单的事情
可靠性
函数应验证其参数 (C-VALIDATE)
Rust API 通常不遵循鲁棒性原则:“对外发送信息时要保守;对接收到的信息要宽容”。
相反,Rust 代码应尽可能地强制输入的有效性。
可以通过以下机制实现强制验证(按优先顺序列出)。
静态验证
选择一种可以排除无效输入的参数类型。
例如,优先选择
#![allow(unused)] fn main() { fn foo(a: Ascii) { /* ... */ } }
而不是
#![allow(unused)] fn main() { fn foo(a: u8) { /* ... */ } }
其中 Ascii
是 u8
的一个包装类型,它保证最高位为零;有关创建类型安全包装器的更多细节,请参见 newtype 模式 (C-NEWTYPE)。
静态验证通常几乎不带来运行时成本:它将成本推到边界(例如,当 u8
首次转换为 Ascii
时)。它还可以在编译期间捕获错误,而不是通过运行时失败来发现错误。
另一方面,有些属性很难或不可能用类型来表达。
动态验证
在处理输入时(或在必要时提前)验证输入。动态检查通常比静态检查更容易实现,但也有几个缺点:
- 运行时开销(除非检查可以作为处理输入的一部分完成)。
- 错误的检测延迟。
- 引入失败情况,无论是通过
panic!
还是Result
/Option
类型,这些都必须由客户端代码处理。
使用 debug_assert!
的动态验证
与动态验证相同,但可以轻松地在生产构建中关闭昂贵的检查。
动态验证的选择退出
与动态验证相同,但增加了可以选择退出检查的同类函数。
惯例是使用类似 _unchecked
的后缀标记这些选择退出检查的函数,或者将它们放在 raw
子模块中。
在以下情况下,可以慎重使用未经检查的函数:(1) 性能要求避免检查,并且 (2) 客户端可以确信输入是有效的。
析构函数不应失败 (C-DTOR-FAIL)
析构函数在发生 panic 时执行,在这种情况下,如果析构函数失败会导致程序中止。
与其让析构函数失败,不如提供一个单独的方法来检查是否正常结束,例如 close
方法,该方法返回一个 Result
以表示问题。如果未调用该 close
方法,则 Drop
实现应进行清理,并忽略或记录/跟踪它产生的任何错误。
可能阻塞的析构函数应提供替代方案 (C-DTOR-BLOCK)
同样,析构函数不应调用阻塞操作,这会使调试更加困难。再次考虑提供一个单独的方法来准备无错误的、非阻塞的清理工作。
调试能力
所有公共类型都实现了 Debug
(C-DEBUG)
如果有例外情况,也应该极为罕见。
Debug
的表现形式永远不为空(C-DEBUG-NONEMPTY)
即使对于概念上为空的值,Debug
的表现形式也不应该是空的。
#![allow(unused)] fn main() { let empty_str = ""; assert_eq!(format!("{:?}", empty_str), "\"\""); let empty_vec = Vec::<bool>::new(); assert_eq!(format!("{:?}", empty_vec), "[]"); }
未来适应性
封闭的特征防止下游实现 (C-SEALED)
某些特征(traits)仅应在定义它们的 crate 内部实现。在这种情况下,我们可以通过使用封闭特征模式来保留改变特征的能力,而不引入不兼容更改。
#![allow(unused)] fn main() { /// 此特征是封闭的,不能在本 crate 之外为类型实现。 pub trait TheTrait: private::Sealed { // 一个或多个用户可调用的方法。 fn ...(); // 一个或多个私有方法,不允许用户调用。 #[doc(hidden)] fn ...(); } // 为某些类型实现。 impl TheTrait for usize { /* ... */ } mod private { pub trait Sealed {} // 为相同的类型实现,但不为其他类型实现。 impl Sealed for usize {} } }
空的私有 Sealed
超特征无法被下游 crate 命名,因此我们可以确保 Sealed
(因此也包括 TheTrait
)的实现仅存在于当前 crate 中。我们可以在非破坏性的版本中添加方法到 TheTrait
,即使这通常是对于未封闭特征的破坏性更改。此外,我们可以更改未公开文档的方法的签名。
请注意,移除公共方法或更改封闭特征中公共方法的签名仍然是破坏性的更改。
为了避免用户尝试实现该特征的挫折,应该在 rustdoc 中记录该特征是封闭的且不应在当前 crate 之外实现。
示例
结构体具有私有字段 (C-STRUCT-PRIVATE)
将字段设为公共是一项强大的承诺:它固定了一种表示选择,并且禁止该类型提供任何验证或维持字段内容的不变量,因为客户端可以随意修改它。
公共字段最适用于 C 精神中的 struct
类型:组合的、被动的数据结构。否则,请考虑提供 getter/setter 方法并隐藏字段。
新类型封装实现细节 (C-NEWTYPE-HIDE)
新类型可以用于隐藏表示细节,同时向客户端做出精确的承诺。
例如,考虑一个返回复合迭代器类型的函数 my_transform
。
#![allow(unused)] fn main() { use std::iter::{Enumerate, Skip}; pub fn my_transform<I: Iterator>(input: I) -> Enumerate<Skip<I>> { input.skip(3).enumerate() } }
我们希望向客户端隐藏该类型,以使客户端对返回类型的视图大致为 Iterator<Item = (usize, T)>
。我们可以使用新类型模式来实现:
#![allow(unused)] fn main() { use std::iter::{Enumerate, Skip}; pub struct MyTransformResult<I>(Enumerate<Skip<I>>); impl<I: Iterator> Iterator for MyTransformResult<I> { type Item = (usize, I::Item); fn next(&mut self) -> Option<Self::Item> { self.0.next() } } pub fn my_transform<I: Iterator>(input: I) -> MyTransformResult<I> { MyTransformResult(input.skip(3).enumerate()) } }
除了简化签名之外,这种新类型的使用允许我们向客户端承诺更少。客户端不知道结果迭代器是如何构造或表示的,这意味着表示可以在不破坏客户端代码的情况下更改。
Rust 1.26 还引入了 impl Trait
特性,比新类型模式更为简洁,但有一些额外的权衡,特别是在表达上有限制。例如,返回实现 Debug
或 Clone
或其他迭代器扩展特征组合的迭代器可能会有问题。总之,对于内部 APIs,impl Trait
作为返回类型可能很好,甚至可能适用于公共 APIs,但可能并非所有情况下都合适。详情请参见新版指南中的 "impl Trait
for returning complex types with ease" 部分。
#![allow(unused)] fn main() { pub fn my_transform<I: Iterator>(input: I) -> impl Iterator<Item = (usize, I::Item)> { input.skip(3).enumerate() } }
数据结构不重复派生特征边界 (C-STRUCT-BOUNDS)
泛型数据结构不应使用可以派生的或其他不增加语义价值的特征边界。derive
属性中的每个特征都会被扩展为一个单独的 impl
块,只有泛型参数实现该特征时才适用。
#![allow(unused)] fn main() { // 推荐这样写: #[derive(Clone, Debug, PartialEq)] struct Good<T> { /* ... */ } // 而不是这样: #[derive(Clone, Debug, PartialEq)] struct Bad<T: Clone + Debug + PartialEq> { /* ... */ } }
在 Bad
上重复派生特征作为边界是不必要的,而且是向后兼容性的隐患。要说明这一点,考虑在上一个示例的结构上派生 PartialOrd
:
#![allow(unused)] fn main() { // 非破坏性更改: #[derive(Clone, Debug, PartialEq, PartialOrd)] struct Good<T> { /* ... */ } // 破坏性更改: #[derive(Clone, Debug, PartialEq, PartialOrd)] struct Bad<T: Clone + Debug + PartialEq + PartialOrd> { /* ... */ } }
一般来说,向数据结构添加特征边界是破坏性更改,因为该结构的每个消费者都需要开始满足额外的边界。使用 derive
属性从标准库派生更多特征不是破坏性更改。
以下特征不应在数据结构的边界中使用:
Clone
PartialEq
PartialOrd
Debug
Display
Default
Error
Serialize
Deserialize
DeserializeOwned
对于其他不可派生的特征边界,它们严格来说不是结构定义所必需的,例如 Read
或 Write
,存在一个灰色地带。它们可能更好地在定义中表达了类型的预期行为,但也限制了未来的扩展性。在数据结构上包含语义上有用的特征边界比包含可派生的特征边界的问题要小。
例外情况
在以下三种情况下,结构体需要特征边界:
- 数据结构指代特征上的相关类型。
- 边界是
?Sized
。 - 数据结构有一个需要特征边界的
Drop
实现。 Rust 当前要求Drop
实现上的所有特征边界也应出现在数据结构上。
标准库中的示例
std::borrow::Cow
指代Borrow
特征上的相关类型。std::boxed::Box
放弃了隐式的Sized
边界。std::io::BufWriter
在其Drop
实现中需要一个特征边界。
必要性
稳定版 Crate 的公共依赖必须稳定 (C-STABLE)
一个 Crate 如果要被标记为稳定版(>=1.0.0),则它所有的公共依赖也必须是稳定的。
公共依赖是指在当前 Crate 的公共 API 中使用的其他 Crate 的类型。
#![allow(unused)] fn main() { pub fn do_my_thing(arg: other_crate::TheirThing) { /* ... */ } }
如果一个 Crate 包含了上述函数,那么 other_crate
也必须是稳定的,否则这个 Crate 无法被标记为稳定版。
要小心,因为公共依赖可能会在意想不到的地方出现。
#![allow(unused)] fn main() { pub struct Error { private: ErrorImpl, } enum ErrorImpl { Io(io::Error), // 即使 other_crate 不是稳定的,这里也没问题, // 因为 ErrorImpl 是私有的。 Dep(other_crate::Error), } // 哦不!这使得 other_crate 出现在了当前 Crate 的公共 API 中。 impl From<other_crate::Error> for Error { fn from(err: other_crate::Error) -> Self { Error { private: ErrorImpl::Dep(err) } } } }
Crate 及其依赖必须具有宽松的许可证 (C-PERMISSIVE)
Rust 项目生成的软件是双许可证的,既可以在 MIT 许可证下发布,也可以在 Apache 2.0 许可证下发布。为了与 Rust 生态系统保持最大的兼容性,建议 Crate 也采用相同的双许可证方式,具体方法如下所述。其他许可选项在下面描述。
这些 API 指南并不详细解释 Rust 的许可证,但在 Rust FAQ 中有一些相关说明。这里的指南主要关注与 Rust 的互操作性问题,并未涵盖所有的许可选项。
要将 Rust 许可证应用于你的项目,可以在 Cargo.toml
中定义 license
字段,如下所示:
[package]
name = "..."
version = "..."
authors = ["..."]
license = "MIT OR Apache-2.0"
然后在项目根目录下添加 LICENSE-APACHE
和 LICENSE-MIT
文件,文件内容应为许可证的文本(可以从 choosealicense.com 获取,例如 Apache-2.0 和 MIT)。
并在你的 README.md 的结尾添加:
## License
Licensed under either of
* Apache License, Version 2.0
([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license
([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
## Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.
除了双 MIT/Apache-2.0 许可证外,Rust Crate 作者使用的另一种常见的许可方式是采用单一宽松许可证,例如 MIT 或 BSD。这种许可方式与 Rust 的许可完全兼容,因为它遵循了 Rust MIT 许可证的最低限制。
不建议仅选择 Apache 许可证的 Crate,因为虽然 Apache 许可证是一种宽松的许可证,但它比 MIT 和 BSD 许可证附加了更多限制,可能会在某些场景下阻碍或阻止它们的使用,因此仅有 Apache 许可证的软件在某些情况下无法使用 Rust 的大部分运行时栈。
Crate 的依赖许可证可能会影响该 Crate 本身的分发限制,因此具有宽松许可证的 Crate 通常应该只依赖于具有宽松许可证的其他 Crate。