API 设计最佳实践
问题
如何设计符合 Rust 惯例的、具有良好人体工程学的 API?
答案
核心原则
| 原则 | 说明 | 示例 |
|---|---|---|
| 类型驱动 | 用类型系统表达约束 | NonZeroU32 而非 u32 |
| 渐进式披露 | 简单用法简单写 | .build() vs .build_with_config() |
| 不可误用 | 编译期阻止错误 | Builder + Typestate |
| 零成本抽象 | 抽象不带运行时开销 | 泛型而非 trait object |
Builder 模式 + Typestate
use std::marker::PhantomData;
// 类型状态标记
struct NoHost;
struct HasHost;
struct ServerBuilder<H> {
host: Option<String>,
port: u16,
max_connections: usize,
_state: PhantomData<H>,
}
impl ServerBuilder<NoHost> {
fn new() -> Self {
Self {
host: None,
port: 8080,
max_connections: 100,
_state: PhantomData,
}
}
// 设置 host 后状态推进到 HasHost
fn host(self, host: impl Into<String>) -> ServerBuilder<HasHost> {
ServerBuilder {
host: Some(host.into()),
port: self.port,
max_connections: self.max_connections,
_state: PhantomData,
}
}
}
impl ServerBuilder<HasHost> {
fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
// 只有 HasHost 状态才能 build
fn build(self) -> Server {
Server {
host: self.host.unwrap(),
port: self.port,
max_connections: self.max_connections,
}
}
}
// 用法:编译期保证 host 已设置
let server = ServerBuilder::new()
.host("127.0.0.1")
.port(3000)
.build();
参数设计
// ❌ 不好:太多 bool 参数
fn connect(host: &str, port: u16, tls: bool, verify: bool) {}
// ✅ 好:使用 Config 结构体
struct ConnectConfig {
host: String,
port: u16,
tls: TlsMode,
}
enum TlsMode {
Disabled,
Enabled { verify_cert: bool },
}
// ✅ 好:impl Into<T> 接受多种类型
fn greet(name: impl Into<String>) {
println!("Hello, {}!", name.into());
}
greet("world"); // &str
greet(String::from("world")); // String
错误设计
// ✅ 库级别用 thiserror 定义精确错误类型
#[derive(Debug, thiserror::Error)]
pub enum StorageError {
#[error("key not found: {key}")]
NotFound { key: String },
#[error("connection failed")]
Connection(#[from] std::io::Error),
}
// ✅ 返回 Result 而非 panic
pub fn get(&self, key: &str) -> Result<Value, StorageError> {
// ...
}
迭代器优先
// ✅ 返回迭代器而非 Vec,让调用者决定如何消费
pub fn even_numbers(limit: usize) -> impl Iterator<Item = usize> {
(0..limit).filter(|n| n % 2 == 0)
}
// 调用者可以 collect、take、for_each 等
let first_5: Vec<_> = even_numbers(100).take(5).collect();
常见面试问题
Q1: impl Into<String> 和 &str 参数哪个更好?
答案:
- 如果函数内部需要 所有权(存储到结构体),用
impl Into<String>,避免调用者不必要的 clone - 如果只 借用(读取、比较),用
&str,零开销 - 经验法则:存 →
Into<String>,读 →&str
Q2: 什么时候用泛型,什么时候用 trait object?
答案:
- 泛型:性能关键路径、编译期确定类型 → 单态化,零开销
- Trait Object (
dyn Trait):需要异构集合、插件系统 → 动态分发,有 vtable 开销 - 经验法则:默认用泛型,需要运行时多态时才用 trait object