Skip to content
Clone
main
218 lines 10.4 KB Markdown

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() 或有默认状态就实现(clippy new_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 另见其指引。