关于 rust-api-design
Rust API Design 是依据 Rust API Guidelines 设计 Rust 公共 crate API 的技能,面向 crate 作者。它帮助构建易用、可预测且类型安全的接口,并为后续演进预留空间。
主要功能
- 01命名与转换约定
- 02选择类型、trait 与泛型
- 03使用 newtype 和类型安全参数
- 04迭代器与 trait 设计
- 05为 API 演进预留空间
适用场景
- 01审查公共 crate API
- 02设计转换方法和访问器
- 03用领域类型替代含义不明的参数
- 04为未来变更准备公共接口
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ full-stack-skills/ rust-skills --skill rust-api-design安装后在 Claude Code 中输入/rust-api-design确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlexamples/anti-patterns.mdexamples/golden-api/Cargo.lockCargo.tomlexamples/golden-api/src/lib.rsreferences/api-guidelines-checklist.mdfuture-proofing.mdnaming-and-conversions.md详细介绍
Rust API 设计(Rust API Guidelines)
权威来源:Rust API Guidelines——事实上的标准检查清单,包含约 100 条 C-* 规则,std、tokio、serde、bevy 均在使用。每条规则的规范表述见完整检查清单。
此技能负责公共 Rust API 的设计:类型、trait、命名、转换,以及让 API 易用、符合人体工程学且不易遭到破坏的边界。它不负责 Cargo 清单(rust-cargo-build)、工作区拓扑(rust-workspace)、crate 内的 src/ 布局(rust-module-layout)或 semver/发布(rust-semver)。
能力边界
✅ 优势
- 将 API Guidelines 的约 100 条 C-* 规则转化为公共 crate 的具体设计决策
- 为类型、函数、方法和功能命名,使其符合 std 和生态系统惯例
- 在泛型、trait 对象、具体类型和 newtype 模式之间做出选择
- 设计
From/Into/TryFrom/AsRef/Borrow转换(拒绝Deref多态) - 选择要派生的自动 trait(
Debug、Clone、Eq、Hash、Send、Sync) - 封闭 trait、标记
#[non_exhaustive]、隐藏结构体字段,以保留演进空间 - 用枚举替代 bool 参数,用 newtype 替代原始整数,用
&str替代String - 正确设计迭代器、
Extend、Default、Display/FromStr - 在公共 API 中避免
panic!/unwrap/expect,优先使用Result
⚠️ 前置条件
- Rust 所有权、trait、生命周期——参见
rust-stable - 模块可见性和重新导出——参见
rust-module-layout - 用于
[features]和optional = true的 Cargo 清单——参见rust-cargo-build
❌ 不在范围内
- Semver 规则和发布工作流 →
rust-semver - Clippy lint 配置 →
rust-style-clippy - 文档测试和 rustdoc 注释 →
rust-documentation - 工作区拓扑 →
rust-workspace
数据隐私
此技能不会收集、存储或传输用户数据。
八个章节(每章对应 API Guidelines 的一个章节)
Guidelines 分为 11 个章节。此技能负责其中 8 个设计章节。文档、宏和必要事项分别由 rust-documentation、rust-macros 和 rust-cargo-build 负责。
1. 命名(C-CASE、C-CONV、C-GETTER、C-NAMING)
C-CASE — 大小写约定
| 项目 | 格式 | 示例 |
|---|---|---|
| 类型(struct/enum/trait)、模块、crate | UpperCamelCase | HttpClient、tokio |
| 函数、方法、局部变量、字段 | snake_case | send_request |
| 常量、静态变量 | SCREAMING_SNAKE_CASE | MAX_RETRIES |
| 泛型类型 | 单个 UpperCamelCase 字母或短词 | T、K、V、Req |
| 生命周期 | 简短的 'a/'b 或描述性的 'src | 'src |
| 功能 | kebab-case(Cargo 强制执行) | serde-json |
避免随意缩写:BufferedReader 符合 std 规范;BufRdr 则不符合。
C-CONV — 转换方法命名
| 转换 | 前缀 | 是否借用? | 示例 |
|---|---|---|---|
| 开销小,借用 | as_ | 是(&self → &T) | as_slice、as_bytes |
| 开销小,拥有所有权 | to_(不分配)/into_(消耗 self) | 视情况而定 | to_vec、into_bytes |
| 开销大,返回新值 | to_ | 否(&self → T) | to_lowercase |
| 消耗自身 | into_ | 否(self → T) | into_string、into_iter |
| 可失败 | TryFrom/TryInto | — | u32::try_from(byte) |
要求:(1) 名为 as_X 却返回拥有所有权的 X 的方法不正确——应改名为 to_X。(2) 可变借用对应的配对方法为 as_X_mut/X_mut。
C-GETTER — 访问器命名
// ✅ C-GETTER compliant
pub struct Buffer { data: Vec<u8> }
impl Buffer {
pub fn data(&self) -> &[u8] { &self.data } // no get_ prefix
pub fn data_mut(&mut self) -> &mut [u8] { &mut self.data }
pub fn len(&self) -> usize { self.data.len() } // not get_len
pub fn is_empty(&self) -> bool { self.data.is_empty() }
}
允许使用 get_ 的例外:Cell::get、Map::get(具有实际的查找语义)。
2. 互操作性(C-COMMON-TRAITS、C-CONVERT、C-ITER、C-SERDE)
C-COMMON-TRAITS — 派生显而易见的特征
对于每个公开类型,都应考虑派生 Debug、Clone、PartialEq、Eq、PartialOrd、Ord、Hash、Default。
| 特征 | 默认 | 例外 |
|---|---|---|
Debug | 是 | 密钥 → 手动实现 Debug 并隐藏密钥 |
Clone | 如果开销小则是 | 克隆开销大 → 省略并注明 |
Copy | 仅当 ≤16 字节且无损时 | — |
PartialEq/Eq | 如果是全序则是 | f64 无法实现 Eq |
Hash | 如果实现了 Eq 则是 | 必须与 Eq 保持一致 |
Default | 如果自然的空值/零值合适则是 | — |
C-CONVERT — From/Into/AsRef/Borrow;拒绝 Deref 多态
// ✅ Impl From<T> for U; Into comes for free
impl From<ErrorKind> for Error { fn from(k: ErrorKind) -> Self { Error::Kind(k) } }
// ✅ AsRef for borrowed views
impl AsRef<str> for Name { fn as_ref(&self) -> &str { &self.0 } }
// ✅ Borrow<str> if Eq/Hash should agree with str
impl Borrow<str> for Name { fn borrow(&self) -> &str { &self.0 } }
拒绝将 Deref 用作多态:Deref 用于智能指针(Box、Rc、Arc、String——这是 std 的先例)。使用 Deref 让 MyClient 对 HttpClient 透明,会隐式注入方法、破坏 &self 解析,并构成**Deref 多态反模式**。请改用 AsRef、显式方法或组合。
C-ITER — 迭代器设计
// ✅ Provide IntoIterator for &T, &mut T, T when sensible
impl<'a> IntoIterator for &'a Grid {
type Item = &'a Cell;
type IntoIter = std::slice::Iter<'a, Cell>;
fn into_iter(self) -> Self::IntoIter { self.cells.iter() }
}
impl Grid {
pub fn iter(&self) -> impl Iterator<Item = &Cell> { /* */ }
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Cell> { /* */ }
}
impl Extend<Cell> for Grid { /* */ }
规则:iter() 借用,iter_mut() 可变借用,into_iter() 消耗。迭代方法绝不返回 Vec——应返回 impl Iterator。
C-SERDE — 序列化互操作
- 在同一类型中一致地使用
#[serde(rename_all = "kebab-case")] #[non_exhaustive]枚举需要#[serde(other)],以便在遇到未知变体时保持向前兼容- 通过 feature 重新导出 serde(
serde = ["dep:serde"]),让下游可以选择不启用
3. 可预测性(C-INTUITIVE、C-CONST、C-COMMON)
C-INTUITIVE — 命名应反映语义
如果调用方必须阅读源码才能知道 read 的作用,那么这个 API 就有问题。不要让人意外:在 EOF 时会 panic 的 read 方法就是缺陷。
C-CONST — 尽可能使用 const fn
pub const fn new(value: u32) -> Self { Self(value) }
const fn is_power_of_two(n: u32) -> bool { n != 0 && n & (n - 1) == 0 }
std 库中任何可以成为 const fn 的 API 都值得考虑。这样调用方就能使用 const MAX: UserId = UserId::new(1000);。
C-COMMON — 通过 Default 和 builder 提供合理默认值
impl Default for Config {
fn default() -> Self {
Config { retry_count: 3, timeout: Duration::from_secs(30) }
}
}
// Builder for complex construction
let client = Client::builder().with_retry(3).with_timeout(Duration::from_secs(10)).build()?;
4. 灵活性(C-OVERLOAD、C-GENERIC、C-NEWTYPE、C-EXT)
C-GENERIC — 输入使用泛型,输出使用具体类型
// ✅ Generic over AsRef<str> — caller passes &str, String, Cow
pub fn parse(input: impl AsRef<str>) -> Result<Foo> { /* */ }
// ❌ Concrete &str — forces caller to borrow
pub fn parse(input: &str) -> Result<Foo> { /* */ }
权衡:泛型越多 → 编译时间越长,诊断越困难。输入类型使用泛型(AsRef<str>、IntoIterator);输出使用具体类型。
C-NEWTYPE — 包装基本类型以防止误用
// ✅ Newtypes around raw primitives
pub struct UserId(pub u64);
pub struct Email(String); // private inner — can't be constructed unsafely
fn delete_user(id: UserId) { /* */ } // can't accidentally pass a PostId
// ❌ Plain primitives — confusion and argument-order bugs
fn delete_user(id: u64) { /* */ }
零成本(编译为底层类型)、防止参数顺序错误,并允许附加方法(UserId::is_anonymous())。
C-EXT — 扩展特征使用 Ext 后缀
pub trait StringExt { fn slugify(&self) -> String; }
impl StringExt for str { fn slugify(&self) -> String { /* */ } }
// Caller opts in:
use my_crate::StringExt;
"Hello World".slugify();
不要在其他 crate 中直接为 String/Vec/HttpRequest 添加方法——应使用 Ext 特征。
5. 类型安全(C-BOOL、C-NONZERO、C-STR、C-SIGNED、C-BITFLAG、C-WRAPPER、C-INTERVAL)
权威依据:API Guidelines — Type Safety。规范表述见
references/api-guidelines-checklist.md。
C-BOOL — 用枚举替代 bool 参数
// ✅ Enum — caller intent is explicit at call site
pub enum Trim { Whitespace, None }
pub fn parse(input: &str, trim: Trim) -> Result<Foo> { /* */ }
parse(" x ", Trim::Whitespace);
// ❌ Bool — caller must remember what true means
pub fn parse(input: &str, trim: bool) -> Result<Foo> { /* */ }
parse(" x ", true); // true = ??
两个 bool 参数会叠加:f(true, false, true) 让人难以理解。至少应使用两个变体的枚举。设置 clippy.toml 中的 max-fn-params-bools = 1 和 max-struct-bools = 1,以便通过工具强制执行。
C-NONZERO — 零值无效时使用 NonZeroUsize
use std::num::NonZeroUsize;
// ✅ NonZeroUsize encodes "≥ 1" in the type
pub fn chunk_size(&self) -> NonZeroUsize { /* */ }
这能启用 niche 优化:Option<NonZeroU32> 与 u32 大小相同。使用 NonZeroU8/NonZeroU16/NonZeroU32/NonZeroU64/NonZeroUsize 以及 NonZeroI* 变体。
C-STR — 使用 &str 而非 &String;使用 &[T] 而非 &Vec<T>
// ✅ Borrow slices for inputs
pub fn process(data: &[u8], name: &str) { /* */ }
// ❌ Forces caller to have owned collections
pub fn process(data: &Vec<u8>, name: &String) { /* */ }
C-SIGNED — 值不可能为负时,优先使用无符号类型
// ✅ u64 — semantically "count" can't be negative
pub struct Counter { count: u64 }
// ❌ i64 — implies negative values are valid (they aren't)
pub struct Counter { count: i64 }
对于特殊范围(例如,Age 为 0..=150),使用带验证构造函数的新类型——让类型系统阻止无效值。
C-BITFLAG — 使用 bitflags! 宏表示标志集合
use bitflags::bitflags;
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Permissions: u32 {
const READ = 0b001;
const WRITE = 0b010;
const EXECUTE = 0b100;
}
}
let p = Permissions::READ | Permissions::WRITE; // type-checked composition
assert!(p.contains(Permissions::READ)); // built-in methods
避免使用原始 u32 表示标志集合——这样会失去类型安全,也无法使用 contains/insert/remove/intersects。
C-WRAPPER — 使用新类型为基本类型赋予有意义的语义
pub struct UserId(pub u64);
pub struct AccountId(pub u64);
pub struct OrderId(pub u64);
// Compiler rejects wrong-id bugs:
fn transfer(from: AccountId, to: AccountId, amount: Cents) { /* */ }
// transfer(UserId(1), UserId(2), Cents(100)) ← compile error
运行时零成本(编译为底层类型)。默认使用 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]。
C-INTERVAL — 将范围编码为类型,而非松散的成对值
// ✅ Dedicated range type with validating constructor
pub struct ChunkRange { start: u32, end: u32 } // invariant: end >= start
impl ChunkRange {
pub fn new(start: u32, end: u32) -> Result<Self, RangeError> {
if end < start { return Err(RangeError::Inverted); }
Ok(Self { start, end })
}
pub fn contains(&self, x: u32) -> bool { self.start <= x && x <= self.end }
}
// ❌ Loose pair — caller might pass end < start
pub fn process_chunk(start: u32, end: u32) { /* */ }
对于 std 范围,请使用 RangeInclusive/Range/RangeTo。对于领域范围(分页、时间窗口),请使用 newtype 封装。
C-COMMENT-HIDDEN — #[doc(hidden)] 不会将内容从公共 API 中排除
// ❌ Hides from rustdoc but is still semver-relevant
#[doc(hidden)]
pub mod unstable { /* */ } // downstream can still `use crate::unstable::Foo;`
// ✅ For actually-unstable items, gate behind a feature
#[cfg(feature = "unstable")]
pub mod unstable { /* */ }
#[doc(hidden)] 只会在 cargo doc 中隐藏内容。要管理 semver/稳定性,请使用功能标志或模块隐私。
对于拥有所有权的输入,请接受 String/Vec 或 impl Into<String>/impl IntoIterator。
6. 可靠性(C-PANIC、C-UNWRAP、C-TRANSMUTE)
C-PANIC — 仅在不可能发生的约束条件下 panic
// ✅ Documented, unreachable from public API
pub fn lookup(&self, id: UserId) -> &User {
self.table.get(&id).expect("internal: index invariant broken")
}
// ❌ Panics on user input
pub fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 { panic!("division by zero") }
a / b
}
// ✅ Return Result for fallible operations
pub fn divide(a: f64, b: f64) -> Result<f64, DivError> { /* */ }
可接受:程序员违反了约束条件,或函数已明确记录为不会失败。不可接受:由输入导致的失败。
C-UNWRAP — 禁止在公共路径中使用 unwrap/expect
// ❌ Library code
pub fn parse(input: &str) -> Config {
serde_json::from_str(input).unwrap() // panics on bad input
}
// ✅ Propagate
pub fn parse(input: &str) -> Result<Config, ParseError> {
Ok(serde_json::from_str(input)?)
}
在测试、const 上下文(不能使用 ?)以及已证明不可能到达的代码中,可以使用 unwrap。为便于诊断,使用 expect("reason"),不要直接使用 unwrap()。
C-TRANSMUTE — 类型转换绝不使用 std::mem::transmute
transmute 会重新解释位模式,绕过类型系统。请使用 From/Into/TryFrom、bytemuck::cast/zerocopy(经过验证的字节转换)、u64::from_ne_bytes/to_ne_bytes,或使用 as 进行扩宽转换。如果你想使用 transmute,请转到 rust-unsafe-ffi。
7. 可调试性(C-DEBUG)
每个公共类型都要实现 Debug。缺少 Debug 会妨碍调试;在 Debug 中共享过多信息则会泄露机密。
#[derive(Debug)]
pub struct Client { /* */ }
// ✅ Manual Debug for secrets
impl std::fmt::Debug for Password {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Password(***)")
}
}
8. 面向未来(C-SEALED、C-STRUCT-FIELD、C-NON-EXHAUSTIVE)
C-SEALED — 密封 trait,防止外部实现
mod private { pub trait Sealed {} }
pub trait Serializer: private::Sealed {
fn serialize(&self, v: &impl Serialize);
}
pub struct JsonSerializer;
impl private::Sealed for JsonSerializer {}
impl Serializer for JsonSerializer { /* */ }
如果不密封,任何人都可以 impl MyTrait for TheirType,导致你无法添加方法(否则会造成破坏性变更)。参见 references/future-proofing.md。
C-STRUCT-FIELD — 公共结构体字段永远稳定
// ✅ Private fields, public constructor — adding fields is non-breaking
pub struct Config { retry_count: u32, timeout: Duration }
impl Config {
pub fn new(retry: u32, timeout: Duration) -> Self { Self { retry_count: retry, timeout } }
pub fn builder() -> ConfigBuilder { /* */ }
}
// ❌ Pub fields + struct literal — adding a field breaks callers
let c = Config { retry_count: 3, timeout: Duration::from_secs(30) };
规则:pub 字段 = 永远稳定。私有字段 = 可以演进。
C-NON-EXHAUSTIVE — 为将来会扩展的枚举添加标记
#[non_exhaustive]
pub enum Error { Io(io::Error), Parse(ParseError) }
// Downstream must use a wildcard arm:
match err {
Error::Io(e) => /* */,
Error::Parse(e) => /* */,
_ => /* unknown */,
}
为计划扩展的错误/事件/状态枚举,以及可能添加公共字段的结构体使用此标记。
工作流
- 盘点公共接口 — 使用
cargo doc --open并列出每个pub项。这就是契约。 - 应用命名规则(第 1 节)— 修正大小写、转换前缀(
as_/to_/into_),移除get_。 - 应用互操作规则(第 2 节)— 派生
Debug/Clone等 trait,添加From/AsRef,设计迭代器。 - 应用可预测性和灵活性规则(第 3-4 节)— 使用
Default、const fn、为基本类型定义 newtype,以及使用Exttrait。 - 应用类型安全规则(第 5 节)— 将 bool 改为枚举,使用
NonZero*,优先选择&str而非&String。 - 应用可靠性规则(第 6 节)— 从公共路径中移除
unwrap/panic!;返回Result。 - 应用面向未来的规则(第 8 节)— 密封可扩展的 trait,为会扩展的枚举添加
#[non_exhaustive],并将结构体字段设为私有。 - 交接 — semver 检查 →
rust-semver;文档测试 →rust-documentation;lint 配置 →rust-style-clippy。
快速决策指南
| 问题 | 答案 |
|---|---|
Option 还是 Result? | “可能不存在”时用 Option;“可能失败”时用 Result |
| Trait 还是枚举? | 如果允许开放扩展(其他人可添加实现),用 trait;如果是封闭集合(变体由你掌控),用枚举 |
| 泛型还是具体类型? | 输入使用泛型,输出使用具体类型 |
字段设为 pub? | 默认不设。仅当预期使用结构体字面量构造时才设为 pub |
&str 还是 impl AsRef<str>? | 灵活性优先时用 AsRef<str>;追求简单时用 &str |
派生 Copy? | 仅当类型较小(≤16 字节)且按位复制正确时 |
| 密封此 trait? | 是,除非第三方必须添加实现(少见) |
#[non_exhaustive]? | 错误/事件枚举应使用;可能扩展且带有公共字段的结构体也应使用 |
反模式目录
Deref多态 — 通过Deref“继承”方法。应使用组合。get_X访问器 — 去掉get_。fn data(&self) -> &[u8]。- 布尔参数 —
fn f(x: bool, y: bool)。应使用两个枚举。 - 公共 API 中使用
unwrap— 会传播 panic。应使用Result。 - 输入参数使用
String/Vec— 会强制转移所有权。应使用&str/&[T]。 - 未封闭的扩展 trait — 会让你无法在未来添加方法。应封闭 trait。
- 错误类型缺少
#[non_exhaustive]— 添加变体会破坏下游代码。 - 缺少 newtype —
fn transfer(amount: u64, from: u64, to: u64)— 参数顺序很容易出错。 - 派生的
Debug泄露机密 —#[derive(Debug)] struct ApiKey(String)。应自定义格式化以隐藏机密。 - 使用
transmute进行类型转换 — 应使用From/as/from_ne_bytes。
每项的重构前后示例,请参阅 examples/anti-patterns.md。
资源
- API 指南检查清单 — 完整的约 100 条 C-* 规则及示例
- 命名与转换深入解析
- 面向未来的模式 — 封闭 trait、non_exhaustive、构建器模式
- 反模式目录 — 重构前后示例
examples/golden-api/— 遵循所有规则的小型 crate,cargo doc和 clippy 均通过