跳到主要内容

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

相关链接