mcpField
R
rust-api-design

rust-api-design

github.com/full-stack-skills开发12117 次安装5 周前更新
代码仓库

关于 rust-api-design

Rust API Design 是依据 Rust API Guidelines 设计 Rust 公共 crate API 的技能,面向 crate 作者。它帮助构建易用、可预测且类型安全的接口,并为后续演进预留空间。

主要功能

  1. 01命名与转换约定
  2. 02选择类型、trait 与泛型
  3. 03使用 newtype 和类型安全参数
  4. 04迭代器与 trait 设计
  5. 05为 API 演进预留空间

适用场景

  1. 01审查公共 crate API
  2. 02设计转换方法和访问器
  3. 03用领域类型替代含义不明的参数
  4. 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.yaml
examples/
anti-patterns.md
examples/golden-api/
Cargo.lock
Cargo.toml
examples/golden-api/src/
lib.rs
references/
api-guidelines-checklist.md
future-proofing.md
naming-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)。

能力边界

✅ 优势
  1. 将 API Guidelines 的约 100 条 C-* 规则转化为公共 crate 的具体设计决策
  2. 为类型、函数、方法和功能命名,使其符合 std 和生态系统惯例
  3. 在泛型、trait 对象、具体类型和 newtype 模式之间做出选择
  4. 设计 From/Into/TryFrom/AsRef/Borrow 转换(拒绝 Deref 多态)
  5. 选择要派生的自动 trait(Debug、Clone、Eq、Hash、Send、Sync)
  6. 封闭 trait、标记 #[non_exhaustive]、隐藏结构体字段,以保留演进空间
  7. 用枚举替代 bool 参数,用 newtype 替代原始整数,用 &str 替代 String
  8. 正确设计迭代器、Extend、Default、Display/FromStr
  9. 在公共 API 中避免 panic!/unwrap/expect,优先使用 Result
⚠️ 前置条件
  1. Rust 所有权、trait、生命周期——参见 rust-stable
  2. 模块可见性和重新导出——参见 rust-module-layout
  3. 用于 [features] 和 optional = true 的 Cargo 清单——参见 rust-cargo-build
❌ 不在范围内
  1. Semver 规则和发布工作流 → rust-semver
  2. Clippy lint 配置 → rust-style-clippy
  3. 文档测试和 rustdoc 注释 → rust-documentation
  4. 工作区拓扑 → 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)、模块、crateUpperCamelCaseHttpClient、tokio
函数、方法、局部变量、字段snake_casesend_request
常量、静态变量SCREAMING_SNAKE_CASEMAX_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 */,
}

为计划扩展的错误/事件/状态枚举,以及可能添加公共字段的结构体使用此标记。


工作流

  1. 盘点公共接口 — 使用 cargo doc --open 并列出每个 pub 项。这就是契约。
  2. 应用命名规则(第 1 节)— 修正大小写、转换前缀(as_/to_/into_),移除 get_。
  3. 应用互操作规则(第 2 节)— 派生 Debug/Clone 等 trait,添加 From/AsRef,设计迭代器。
  4. 应用可预测性和灵活性规则(第 3-4 节)— 使用 Default、const fn、为基本类型定义 newtype,以及使用 Ext trait。
  5. 应用类型安全规则(第 5 节)— 将 bool 改为枚举,使用 NonZero*,优先选择 &str 而非 &String。
  6. 应用可靠性规则(第 6 节)— 从公共路径中移除 unwrap/panic!;返回 Result。
  7. 应用面向未来的规则(第 8 节)— 密封可扩展的 trait,为会扩展的枚举添加 #[non_exhaustive],并将结构体字段设为私有。
  8. 交接 — 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]?错误/事件枚举应使用;可能扩展且带有公共字段的结构体也应使用

反模式目录

  1. Deref 多态 — 通过 Deref“继承”方法。应使用组合。
  2. get_X 访问器 — 去掉 get_。fn data(&self) -> &[u8]。
  3. 布尔参数 — fn f(x: bool, y: bool)。应使用两个枚举。
  4. 公共 API 中使用 unwrap — 会传播 panic。应使用 Result。
  5. 输入参数使用 String/Vec — 会强制转移所有权。应使用 &str/&[T]。
  6. 未封闭的扩展 trait — 会让你无法在未来添加方法。应封闭 trait。
  7. 错误类型缺少 #[non_exhaustive] — 添加变体会破坏下游代码。
  8. 缺少 newtype — fn transfer(amount: u64, from: u64, to: u64) — 参数顺序很容易出错。
  9. 派生的 Debug 泄露机密 — #[derive(Debug)] struct ApiKey(String)。应自定义格式化以隐藏机密。
  10. 使用 transmute 进行类型转换 — 应使用 From/as/from_ne_bytes。

每项的重构前后示例,请参阅 examples/anti-patterns.md。

资源

上游来源

评论

支持 Markdown