API 设计:输入输出、类型、trait 与 API 属性
提炼自指南的 Function inputs and outputs / Types / Traits / Concurrency / Macros / Other Rust features 章节。
总纲:库代码(供其他项目复用)必须遵守 API 设计规则;应用代码(单一程序内部)作为强烈建议,优先简单直接、避免过早泛化与抽象。
函数输入与输出
不可变借用参数取最灵活的形态
传"数据本身"的引用,不传智能指针/容器/包装类型的引用:
| 用 | 不用 | 原因 |
|---|---|---|
&T | &Box<T> | 借 box 里的数据 |
&[T] | &Vec<T> | 接受任意切片而非特定容器 |
&str | &String | 接受字符串切片而非 owned 字符串的引用 |
Option<&T> | &Option<T> | 借 Option 里的数据 |
例外:预期非线性字符数据(Rope/Cord)。对应 clippy:borrowed_box、ptr_arg。
理由:不绑定具体容器/包装的引用让函数更通用,调用方无需先复制。
返回值:迭代器 vs 所有权集合
- 库代码尽量返回迭代器(具体类型或
impl Iterator)而非Vec<T>。 - 应用代码:返回
Vec<T>没问题,简单优先。
返回 Vec 更合适的情形:多数调用方需要 owned vector;实现本来就构造 vector;返回值需要随机索引;算法更适合 for 循环且强制分配影响不大;函数目的就是创建新集合;返回已存在的 vector。
参数:迭代器 vs 切片
- 高复用性的库代码只需遍历输入时,接受迭代器或
impl IntoIterator(更灵活、免拷贝、可组合)。 - 应用代码:接受
&[T]更简单即可。
接受 Into
- 库 API 用户实参类型多样、调用点体验优先时,考虑
impl Into<T>代替T(如impl Into<String>可直接收String或&str字面量)。权衡泛型的代码膨胀与类型推断复杂度,不是多多益善。 - 应用代码几乎不用
impl Into<T>参数。
参数用 enum 不用 bool
参数用途不明显时用 enum。坚持用 bool 时,在参数列表加行内注释说明用途。
错误上报:Option / Result / panic!
Option:失败原因显而易见、更多细节无益。如checked_add的整数溢出。Result:可恢复的失败,调用方需要上下文区分失败模式并恢复,常源于外部因素(I/O、网络)。如open_file需区分 NotFound 与 PermissionDenied。panic!:报告 bug 与被违反的不变量(如索引越界)——应在代码里修,不是运行时处理;粗粒度恢复或可借助catch_unwind(其使用限制见 implementation.md)。
注:部分代码库选择完全不用 panic,连被破坏的不变量也要求用 Result 报告。
例外:面向开发者的二进制可用 panic 报非致命错误;测试断言可用 panic。
类型
newtype vs 类型别名
以下情况用 newtype:类型有显著语义差异;值得拥有自己的 API;想保留日后更换底层类型的自由(尤其不控制全部用例时);类型安全需要(Date 内部用 newtype 区分 Day/Month/Year)。
命名字段优先于元组
元组下标(.0、.1)很快会看不懂。例外:单字段 newtype 元组结构体;极局部、不会混淆的场景。
trait
实现时机
低层库代码应预判用户需求、主动实现;应用代码按需添加。
实现标准库 trait 而非自定义方法
方法与标准库 trait 的目的和行为一致时实现该 trait:impl FromStr for Foo 而不是在 Foo 上写 fn from_str(&str) -> Self。为可发现性可以两者都保留。对应 clippy should_implement_trait。
能 #[derive()] 就不用手写
泛型优先于 dyn Trait
库中函数参数用 T: Trait / impl Trait 优于 dyn Trait。例外:代码体积敏感。
理由:dyn 有胖指针与 vtable 开销、阻碍内联等编译器优化;部分 trait 不 dyn 兼容(含泛型函数、无 self 的关联函数、返回 Self 的 trait)。
何时定义 trait
只为以下目的定义 trait:泛型编程(T: Trait 约束);接受或返回 impl Trait / dyn Trait;定义扩展 trait。其余情况用固有实现(inherent impl)。不要用 trait 来组织代码。
扩展 trait(extension trait)
给其他 crate 的类型加方法时用扩展 trait——唯一目的是给该类型加方法,不用于泛型或动态分发。命名带 Ext 后缀(原文记法 ~Ext,如 ForeignTypeExt)。原则上由原类型的 owning team 同时维护扩展 trait。例外:预期广为人知、价值巨大的基础库/框架扩展。注意:作为输入约束的 trait(如 DateLike,实现于一堆外部类型)不属此列。
trait ForeignTypeExt {
fn additional_method(&self);
}
impl ForeignTypeExt for ForeignType { … }
也可为所有实现某外部 trait 的类型统一实现扩展 trait,效果等同于给该外部 trait 添加默认方法:
trait ForeignTraitExt {
fn additional_method(&self);
}
impl<T: ForeignTrait> ForeignTraitExt for T { … }
重要 trait 的实现建议
- 公开字段的类型:考虑实现所有子字段都实现的 trait。
- 错误类型:尽量实现
Copy(含String等非 Copy 字段时除外)。 Copy:有理由相信类型将长期保持 Copy 就实现。Clone:除表示外部资源的类型外都应实现。Default:实现了new()或有默认状态就实现(clippynew_without_default)。PartialEq/Eq:想让用户用==就实现PartialEq;实现Eq注意额外的自反性要求。PartialOrd/Ord:同理;Ord要求全序。- map key:类型可能用作 key 或被缓存 → 实现
Hash+Eq;再有清晰全序 → 加Ord。
重要 From 实现配对显式构造方法
重要的 From impl 若是主要构造方式之一,配一个固有 ::new() / ::new_from_foo() / ::from_foo()。
options 结构体实现 Default
全公开字段、主要作为函数"选项袋"的 struct 应实现 Default,以支持 Struct { a: 0, ..Default::default() }。
并发
- 永远不要从 poisoned mutex 里取内部数据,无论用例多诱人。
- [chrome] 不用
async/.await。 - [chrome] 不通过标准库起新线程,用 sequences。
- [Fuchsia] 用
fuchsia_sync::Mutex,不用std::sync或parking_lot。
宏
API 表达优先级:常规函数/类型/trait/对象/模块 > macro_rules! 宏 > 其他代码生成(如 genrule)> proc_macro。
决定用宏或代码生成前权衡成本:IDE、代码搜索等工具对宏代码支持更差;宏会制造语言方言,增加跨代码库工作成本;LSC 更难更贵。
macro_rules! 基于 AST 展开,比文本式代码生成更不易碎。
API 属性与访问器
getter / setter 命名
foo():返回&T,或T: Copy时按值返回。foo_mut():返回&mut T。take_foo():清掉值(置None或默认态)并返回旧值。set_foo():按值设置。with_foo()/with_mut_foo():经闭包访问,object.with_foo(|foo| …)。replace_foo():按值设置并返回旧值。
impl Stuff {
fn foo(&self) -> &Foo { … }
fn foo_mut(&mut self) -> &mut Foo { … }
fn take_baz(&mut self) -> Option<Baz> { … }
fn set_foo(&mut self, foo: Foo) { … }
fn with_bar<R>(&self, f: impl FnOnce(&Bar) -> R) -> R { … }
fn with_mut_bar<R>(&self, f: impl FnOnce(&mut Bar) -> R) -> R { … }
fn replace_foo(&mut self, foo: Foo) -> Foo { … }
}
闭包式访问器 with_foo() 的适用场景
- 属性在内部可变性包装里(
Mutex/RefCell):闭包提供访问作用域,优于直接返回MutexGuard<T>(那会泄漏实现细节)。 - 改属性后需要跟进校验/收尾、
set_foo()不可用时(如属性类型昂贵或无法克隆)。
闭包的返回值通常从 with_foo() 透传。
需要同时提供只读与可变两个闭包访问器时,按惯例命名 with_foo() / with_mut_foo();否则 with_foo() 也常用于可变访问。
API 成套实现
实现了 len() 就同时实现 is_empty()(clippy len_without_is_empty)。
#[doc(hidden)] 视为私有
不访问不属于自己代码的 #[doc(hidden)] 条目。
const fn 慎用
公开函数只在预期将持续保持 const(或承诺帮调用方迁移)时才标 const——const 改回普通函数是破坏性变更。
运算符重载只用于数学/逻辑本义
不做"语法糖"式的重载。
公开 enum 加 #[non_exhaustive]
跨团队使用的公开 API enum 标 #[non_exhaustive]:强制调用方 match 留通配臂(_ => …),未来加变体不破坏编译。
例外:enum 永不变更(描述既有本体如 enum Color { Red, Green, Blue };两态等价 bool 如 enum Visibility { Visible, Hidden });或承诺用 LSC 修复破坏。使用范围很小的 enum 可不遵守。clippy exhaustive_enums。
公开字段的 struct 加 #[non_exhaustive]
跨团队使用、含公开字段的 struct 同理:加新字段(哪怕私有)会破坏 struct 字面量构造;#[non_exhaustive] 使外部只能经构造函数创建。
例外:字段描述稳定本体(如 Rectangle);承诺 LSC;需要用户能使用 FRU 语法(Person { name: "foo", ..Default::default() })——与 #[non_exhaustive] 互斥。
#[must_use]
- 函数:忽略返回值就无法正确使用时——纯函数(如
HashMap::get());返回值承载错误或关键信息(返回类型本身已#[must_use]的,如Result,不用重复标注)。 - 类型:将被上述函数返回的类型——错误类型(类似
Result或StatusOr);惰性求值类型(不取返回值就不产生副作用)。
不要在 main() 之前运行代码
不用 ctor crate 之类手段。
unsafe 代码
没有等价安全实现时才写 unsafe。必须写时:
- 封装为安全抽象,与业务逻辑尽量隔离;理想:单独 crate、除标准库外极少依赖。
- 加以
SAFETY:开头的注释,解释代码为何正确。 - 给技术理由:为什么不能用等价的安全写法实现;若理由是性能,给出 benchmark。
不用 unstable 特性
例外:Android 嵌入式确需的(review 后一般会批);[chrome] style arbiters 可能对整库白名单一小批有稳定路径的特性;Fuchsia 另见其指引。