Rust Trait:让类型接入语言与生态
From Display to Future
目录
- Trait 如何进入代码:trait、impl 与 Trait Bound
- 类型如何被打印:Debug 与 Display
- 复制、默认值与释放:Clone、Copy、Default、Drop
- 相等、排序与 Hash:PartialEq、Eq、PartialOrd、Ord、Hash
- 类型转换与 ?:From、Into、TryFrom、TryInto、FromStr
- 借用与参数适配:AsRef、Borrow、Deref、ToOwned
- for 循环背后:Iterator 与 IntoIterator
- 集合如何参与 collect():FromIterator 与 Extend
- 闭包的三种能力:FnOnce、FnMut、Fn
- 错误如何形成调用链:Error、Display、Debug、From
- 通用 I/O:Read、BufRead、Write、Seek
- 运算符与索引:Add、Sub、Index、IndexMut
- 跨越线程边界:Send 与 Sync
- 异步计算的协议:Future、IntoFuture、Unpin
- 动态大小类型:Sized 与 ?Sized
- 静态与动态分发:impl Trait 与 dyn Trait
- 后端生态的 Trait:Serialize、Stream、Sink、Service
- 什么时候实现哪个 Trait
Rust 里很多看似由语言直接提供的能力,背后其实是 Trait:
println!("{user_id}");
let id = "42".parse::<UserId>()?;
for order in orders {
// ...
}
tokio::spawn(process(order));
这几行代码分别要求类型实现文本格式化、字符串解析、迭代和跨线程传递相关的 Trait。
Trait 因而不只是“Rust 版本的 interface”。它还是类型接入语言语法和通用算法的方式。一个自定义类型实现了 Display,就能进入格式化系统;实现了 IntoIterator,就能被 for 消费;满足 Send,所有权才可以跨过线程边界。
标准库里的 Trait 很多,没有必要逐个记忆。更实用的学习方式是从问题出发:
这个类型应该怎样打印?
它能否成为 HashMap 的 Key?
一次转换会不会失败?
函数需要取得所有权,还是只需要一种引用?
这个容器应该怎样参与 for 和 collect?
它能否在线程和异步任务之间移动?
本文从一个后端服务里常见的 UserId、订单集合和错误类型开始,逐步为它们补上所需能力。重点不是把所有 Trait 都实现一遍,而是知道哪个 Trait 对应哪个问题,以及哪些实现最好不要轻易写。
Trait 如何进入代码:trait、impl 与 Trait Bound
先定义一个最简单的业务类型:
struct UserId(u64);
如果一个函数只接受 UserId,它知道参数的完整类型:
fn load_user(id: UserId) {
// ...
}
如果函数只关心“这个值能够被打印”,就可以把要求写成 Trait Bound:
use std::fmt::Display;
fn log_value<T>(value: T)
where
T: Display,
{
println!("{value}");
}
T: Display 并不是在描述 T 的继承关系。它是在告诉编译器:函数体只会依赖 Display 所承诺的能力。
Trait 的价值也正在这里。泛型代码不需要知道所有可能的类型,只需要声明自己会使用哪些操作:
想使用 == PartialEq
想放进 HashMap Eq + Hash
想调用 clone() Clone
想放进新线程 Send + 'static
想用 for 遍历 IntoIterator
至于 Trait 如何定义、孤儿规则、关联类型和静态分发,这些属于 Trait 本身的机制。下面更关注标准库已经定义好的通用协议,以及业务类型怎样接入它们。
类型如何被打印:Debug 与 Display
Debug 和 Display 都能把值写进 Formatter,但面向的读者不同。
Debug
面向开发者
适合日志、断言和临时排查
Display
面向类型的使用者
应当提供稳定、自然的文本表示
Debug 通常直接派生:
#[derive(Debug)]
struct UserId(u64);
let id = UserId(42);
println!("{id:?}");
输出可能是:
UserId(42)
这种格式便于确认类型结构,但不适合作为接口响应或持久化格式。派生出来的 Debug 表示可能随着字段调整而改变。
Display 通常需要手写,因为编译器不知道什么才是这个类型自然的外部表示:
use std::fmt;
impl fmt::Display for UserId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
现在可以写:
println!("user_id={user_id}");
let text = user_id.to_string();
我们没有为 UserId 实现 ToString。标准库已经为所有实现 Display 的类型提供了 ToString 的毯式实现。
这也是使用标准 Trait 的一个好处:实现最核心的协议,就能自动获得周边能力。
不应当把 Display 当成通用序列化机制。如果 UserId 需要稳定地编码进 JSON、数据库或网络协议,应使用对应的序列化规则,而不是依赖某段展示文本永远不变。
复制、默认值与释放:Clone、Copy、Default、Drop
这四个 Trait 都与值的生命周期有关,但表达的意思不同。
Clone:显式复制
#[derive(Clone)]
struct UserProfile {
name: String,
email: String,
}
let backup = profile.clone();
clone() 可能很便宜,也可能复制大量堆数据。Trait 只承诺能够得到一个独立的同类值,不承诺复杂度。
在泛型 API 中写下 T: Clone,等于允许实现代码复制调用者传入的值。这个要求不应该为了绕过所有权问题随手添加。很多时候,借用、移动所有权或使用 Arc 更能表达真实需求。
Copy:隐式的轻量复制
UserId 只包含一个 u64,复制成本明确,可以派生 Copy:
#[derive(Debug, Clone, Copy)]
struct UserId(u64);
let first = UserId(42);
let second = first;
println!("{first:?} {second:?}");
赋值以后 first 仍能使用,因为这里发生的是 Copy,而不是所有权移动。
实现 Copy 的类型必须也实现 Clone。它不能包含 String、Vec 等非 Copy 字段,也不能实现 Drop。
Copy 不只是性能标记,它会改变赋值和传参的语义。文件句柄、锁 Guard 或数据库事务即使内部只有一个整数,也不应该因为“复制很便宜”就实现 Copy,因为复制资源所有权本身没有合理含义。
Default:存在一个普通起点
配置和构建器经常适合 Default:
#[derive(Debug)]
struct RetryPolicy {
max_attempts: u8,
timeout_ms: u64,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 3,
timeout_ms: 500,
}
}
}
调用方可以只覆盖关心的字段:
let policy = RetryPolicy {
timeout_ms: 800,
..Default::default()
};
但并非所有类型都有合理默认值。如果 UserId(0) 在业务上非法,为了方便构造而派生 Default 会制造一个无效状态。此时应该提供返回 Result 的构造函数,而不是编造默认值。
Drop:作用域结束时收尾
Drop 是 RAII 的出口。下面的 Guard 在离开作用域时记录耗时:
use std::time::Instant;
struct TimerGuard {
name: &'static str,
started_at: Instant,
}
impl Drop for TimerGuard {
fn drop(&mut self) {
println!(
"{} took {:?}",
self.name,
self.started_at.elapsed(),
);
}
}
无论函数正常返回还是通过 ? 提前退出,已经创建的 Guard 都会被 Drop。
Drop::drop 不能返回错误,也不能 .await。需要可靠落库、发送网络请求或异步关闭资源时,不能只依赖 Drop;应该提供显式的 close()、commit() 或 shutdown().await,Drop 只负责最后的兜底清理。
相等、排序与 Hash:PartialEq、Eq、PartialOrd、Ord、Hash
业务 ID 经常需要比较、排序并作为集合的 Key:
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
)]
struct UserId(u64);
于是它可以用于:
use std::collections::{BTreeSet, HashMap};
let mut users = HashMap::new();
users.insert(UserId(42), "lanran");
let mut sorted = BTreeSet::new();
sorted.insert(UserId(42));
这些 Trait 是成组出现的,但并不完全相同:
PartialEq 可以使用 == 和 !=
Eq 相等关系满足自反性等完整约束
PartialOrd 某些值之间可能无法排序
Ord 任意两个值都存在全序关系
Hash 可以生成用于哈希集合的摘要
浮点数只实现 PartialEq 和 PartialOrd,没有实现 Eq 与 Ord:
assert!(f64::NAN != f64::NAN);
NaN 不满足 x == x,因此不能声称具有完整等价关系。
自定义实现 Eq 和 Hash 时必须保持一致:
如果
a == b,那么a和b必须产生相同的 Hash。
否则 HashMap 可能插入一个 Key 后,再也无法使用“相等”的值找到它。
派生实现会按照字段顺序比较和 Hash。对于实体类型,要先决定相等表示“ID 相同”,还是表示所有字段完全相同。不能因为派生方便,就让数据库实体的相等语义随着新增字段悄悄改变。
类型转换与 ?:From、Into、TryFrom、TryInto、FromStr
UserId 从数据库读取时可能是 u64,从 URL 参数进入时却是字符串。转换是否可能失败,决定了应该使用哪一组 Trait。
不会失败的转换:From 与 Into
从合法 UserId 取出数字不会失败:
impl From<UserId> for u64 {
fn from(value: UserId) -> Self {
value.0
}
}
调用方既可以写:
let raw = u64::from(user_id);
也可以写:
let raw: u64 = user_id.into();
实现 From<UserId> for u64 后,标准库会自动提供反向视角的 Into<u64> for UserId。定义转换时通常优先实现 From,泛型参数如果想接纳更广泛的调用方,则可以使用 Into Bound。
From 适合无损、明确、不会失败的转换。如果转换需要截断、默认值或业务校验,它就不应该伪装成 From。
可能失败的转换:TryFrom 与 TryInto
把任意 u64 转成 UserId 需要排除零:
#[derive(Debug)]
struct InvalidUserId;
impl TryFrom<u64> for UserId {
type Error = InvalidUserId;
fn try_from(value: u64) -> Result<Self, Self::Error> {
if value == 0 {
Err(InvalidUserId)
} else {
Ok(UserId(value))
}
}
}
使用时失败路径会留在类型里:
let id = UserId::try_from(raw_id)?;
实现 TryFrom<A> for B 后会自动获得 TryInto<B> for A。在泛型函数的输入 Bound 中,TryInto 往往能接纳更多实现:
fn load_user<I>(input: I) -> Result<User, Error>
where
I: TryInto<UserId>,
Error: From<I::Error>,
{
let id = input.try_into()?;
// ...
}
签名如果因此变得比业务逻辑更复杂,也没有必要强行泛化。只接受 UserId,让边界层先完成校验,通常更清楚。
字符串解析:FromStr
URL、环境变量和命令行参数经常来自字符串:
use std::str::FromStr;
impl FromStr for UserId {
type Err = ParseUserIdError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let raw = value
.parse::<u64>()
.map_err(ParseUserIdError::InvalidNumber)?;
UserId::try_from(raw)
.map_err(|_| ParseUserIdError::Zero)
}
}
于是可以直接使用标准解析入口:
let id: UserId = "42".parse()?;
? 在这里传播错误。不同错误类型之间需要转换时,通常通过 From 完成。例如为应用错误实现 From<ParseUserIdError> 后,解析错误就能沿着返回 Result<_, AppError> 的函数继续向上返回。
借用与参数适配:AsRef、Borrow、Deref、ToOwned
这几个 Trait 都能“得到另一种引用”,却服务于不同目的。
AsRef:便宜的引用转换
读取配置文件时,调用方可能持有 &str、String 或 PathBuf:
use std::fs;
use std::io;
use std::path::Path;
fn read_config(path: impl AsRef<Path>) -> io::Result<String> {
fs::read_to_string(path.as_ref())
}
调用都可以成立:
read_config("config.toml")?;
read_config(String::from("config.toml"))?;
read_config(Path::new("config.toml"))?;
AsRef 表示低成本的 reference-to-reference 转换。它不适合隐藏需要分配、解析或可能失败的操作。
输入参数也不必逢类型就写 impl AsRef<_>。如果函数只服务于一个明确的业务类型,直接接收 &UserId 或 &Path 通常更容易阅读。
Borrow:借用后仍保持集合语义
HashMap<String, User> 可以用 &str 查询:
let mut users = std::collections::HashMap::new();
users.insert(String::from("lanran"), user);
let found = users.get("lanran");
这是因为 String: Borrow<str>,而且 String 与它借出的 str 具有一致的相等、排序和 Hash 语义。
Borrow 比 AsRef 的承诺更强。它主要服务于泛型集合和算法:拥有所有权的 Key 可以使用对应的借用形式查询,又不会改变 Key 的语义。
Deref:让智能指针表现得像目标值
String 可以调用 str 的方法,Box<T> 和 Arc<T> 可以像 T 一样被借用,背后都与 Deref 有关。
use std::ops::Deref;
fn first_char(text: &String) -> Option<char> {
text.deref().chars().next()
}
实际代码直接写 text.chars() 即可,编译器会进行 Deref Coercion。
不要为了让 UserId 自动获得 u64 的所有方法而实现 Deref<Target = u64>。Deref 适合智能指针和具有透明目标关系的容器,不是模拟继承或省略 .0 的通用工具。业务转换使用显式方法或 From 更清楚。
ToOwned 与 Cow
ToOwned 可以从借用数据得到拥有所有权的版本:
str → String
[T] → Vec<T>
它经常通过 Cow 出现:
use std::borrow::Cow;
fn normalize_name(name: &str) -> Cow<'_, str> {
if name.chars().all(|ch| !ch.is_uppercase()) {
Cow::Borrowed(name)
} else {
Cow::Owned(name.to_lowercase())
}
}
无需修改时直接借用,需要修改时才分配。这比一进入函数就无条件 to_string() 更能表达真实成本。
for 循环背后:Iterator 与 IntoIterator
Iterator 表示一个正在产生元素的过程:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
只要实现 next(),就会获得 map、filter、fold、zip、take 等大量默认方法。
但集合本身不一定就是 Iterator。一个订单批次可以被遍历,同时保留自己的领域方法:
struct OrderBatch {
orders: Vec<Order>,
}
impl OrderBatch {
fn total_amount(&self) -> Money {
self.orders
.iter()
.fold(Money(0), |total, order| {
total + order.amount
})
}
}
让它进入 for 循环,需要实现 IntoIterator:
impl IntoIterator for OrderBatch {
type Item = Order;
type IntoIter = std::vec::IntoIter<Order>;
fn into_iter(self) -> Self::IntoIter {
self.orders.into_iter()
}
}
现在:
for order in batch {
process(order);
}
会消费整个 OrderBatch,每次得到一个拥有所有权的 Order。
如果希望借用遍历,还可以为引用实现:
impl<'a> IntoIterator for &'a OrderBatch {
type Item = &'a Order;
type IntoIter = std::slice::Iter<'a, Order>;
fn into_iter(self) -> Self::IntoIter {
self.orders.iter()
}
}
于是两种循环拥有不同语义:
for order in &batch {
// order: &Order,batch 仍可继续使用
}
for order in batch {
// order: Order,batch 被消费
}
这比简单记住 .iter()、.iter_mut() 和 .into_iter() 更重要:它们分别产生共享借用、可变借用和拥有所有权的元素。
集合如何参与 collect():FromIterator 与 Extend
collect() 是 Iterator 的方法,但它并不知道怎样构造所有集合。目标类型通过 FromIterator 告诉它如何接收一串元素。
impl FromIterator<Order> for OrderBatch {
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = Order>,
{
Self {
orders: iter.into_iter().collect(),
}
}
}
现在可以写:
let batch: OrderBatch = orders
.into_iter()
.filter(|order| order.is_pending())
.collect();
collect() 依靠返回类型推断目标集合。推断不出来时,可以使用 Turbofish:
let batch = orders.into_iter().collect::<OrderBatch>();
已有集合继续吸收元素则使用 Extend:
impl Extend<Order> for OrderBatch {
fn extend<I>(&mut self, iter: I)
where
I: IntoIterator<Item = Order>,
{
self.orders.extend(iter);
}
}
FromIterator
从零构造一个集合
Extend
向已有集合追加元素
这里有一个领域建模上的限制:FromIterator::from_iter 直接返回 Self,不能返回 Result<Self, E>。如果 OrderBatch 的构造需要校验“所有订单必须属于同一用户”,就不应该用一个会悄悄接受非法数据的 FromIterator 实现。
可以先收集为 Vec,再调用显式的校验构造函数:
let orders = iter.collect::<Vec<_>>();
let batch = OrderBatch::try_new(orders)?;
标准 Trait 很方便,但不能为了获得语法糖而削弱类型不变量。
闭包的三种能力:FnOnce、FnMut、Fn
闭包实现哪个 Trait,取决于它怎样使用捕获的数据。
FnOnce:可能消费捕获值
fn run_once<F>(task: F)
where
F: FnOnce(),
{
task();
}
let payload = String::from("event");
run_once(move || {
send(payload);
});
闭包把 payload 移给 send,只能调用一次。线程任务和很多异步任务天然是一次性操作,所以经常接受 FnOnce。
FnMut:多次调用并修改状态
fn retry<F, T, E>(mut operation: F) -> Result<T, E>
where
F: FnMut() -> Result<T, E>,
{
let mut attempts = 0;
loop {
attempts += 1;
match operation() {
Ok(value) => return Ok(value),
Err(error) if attempts == 3 => return Err(error),
Err(_) => continue,
}
}
}
FnMut 允许闭包修改捕获变量:
let mut attempts = 0;
retry(|| {
attempts += 1;
call_remote()
});
Fn:多次调用且不需要独占修改
过滤器、纯计算回调经常只读取环境:
fn select_orders<F>(orders: &[Order], predicate: F) -> Vec<&Order>
where
F: Fn(&Order) -> bool,
{
orders
.iter()
.filter(|order| predicate(order))
.collect()
}
三者形成能力层级:
Fn
也满足 FnMut 和 FnOnce
FnMut
也满足 FnOnce
FnOnce
只承诺至少能调用一次
设计参数时,接受满足需求的最宽松 Trait。只调用一次的 API 使用 FnOnce,就不会无故拒绝那些需要消费捕获值的闭包。
错误如何形成调用链:Error、Display、Debug、From
一个标准错误类型通常需要同时具备:
Debug 开发者调试
Display 人类可读描述
Error 接入错误链
From 在 ? 处转换错误
前面的 ParseUserIdError 可以写成:
use std::error::Error;
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum ParseUserIdError {
InvalidNumber(ParseIntError),
Zero,
}
impl fmt::Display for ParseUserIdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidNumber(_) => write!(f, "user id is not a number"),
Self::Zero => write!(f, "user id must not be zero"),
}
}
}
impl Error for ParseUserIdError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::InvalidNumber(error) => Some(error),
Self::Zero => None,
}
}
}
应用层错误可以接住它:
#[derive(Debug)]
enum AppError {
InvalidUserId(ParseUserIdError),
Database(DatabaseError),
}
impl From<ParseUserIdError> for AppError {
fn from(error: ParseUserIdError) -> Self {
Self::InvalidUserId(error)
}
}
于是:
fn parse_request(value: &str) -> Result<UserId, AppError> {
let id = value.parse::<UserId>()?;
Ok(id)
}
? 发现当前错误是 ParseUserIdError,函数返回的是 AppError,便通过 From 完成转换。
业务项目通常会使用 thiserror 派生这些样板实现,应用最外层也可能使用 anyhow 聚合上下文。但理解 Display、Error::source 和 From 的关系,才能知道这些库替我们生成了什么。
公共库不应只返回 String。字符串丢失了可匹配的错误类型、source 链和结构化信息,调用方只能解析文本判断错误类别。
通用 I/O:Read、BufRead、Write、Seek
假设服务需要导入一份逐行 JSON 订单文件。函数如果直接接收 File,测试就必须创建临时文件:
fn import_orders(file: std::fs::File) {
// ...
}
真正需要的能力其实只是按行读取:
use std::io::{self, BufRead};
fn import_orders<R>(reader: R) -> Result<Vec<Order>, ImportError>
where
R: BufRead,
{
reader
.lines()
.map(|line| {
let line = line?;
serde_json::from_str(&line).map_err(ImportError::from)
})
.collect()
}
生产环境传文件缓冲:
let file = std::fs::File::open(path)?;
let reader = std::io::BufReader::new(file);
let orders = import_orders(reader)?;
测试可以直接使用内存:
let input = std::io::Cursor::new(
b"{\"id\":1}\n{\"id\":2}\n"
);
let orders = import_orders(input)?;
同一段解析逻辑不需要知道字节来自文件、内存还是其他 Reader。
几个 Trait 的侧重点是:
Read
从某处读取字节
BufRead
在 Read 之上提供内部缓冲和按行读取
Write
向某处写入字节并支持 flush
Seek
调整当前读写位置
这些是同步 I/O Trait。Tokio 中的 AsyncRead 和 AsyncWrite 处理的是异步读写协议,不能把 std::io::Read 直接当成异步接口使用。
运算符与索引:Add、Sub、Index、IndexMut
Rust 的运算符也通过 Trait 开放给自定义类型。
金额相加具有清楚的领域含义:
use std::ops::{Add, Sub};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Money(i64);
impl Add for Money {
type Output = Money;
fn add(self, rhs: Money) -> Self::Output {
Money(self.0 + rhs.0)
}
}
impl Sub for Money {
type Output = Money;
fn sub(self, rhs: Money) -> Self::Output {
Money(self.0 - rhs.0)
}
}
let payable = price + shipping - discount;
Add<Rhs = Self> 的右操作数是泛型参数,Output 是关联类型,因此还可以表达 Duration + Timestamp -> Timestamp 之类左右类型不同的运算。
Index 和 IndexMut 支持 container[index],但索引不存在时通常会 panic。需要返回 Option 的查找仍应使用 get(),不要为了获得方括号语法隐藏正常的失败分支。
运算符实现应当符合读者直觉。Money + Money 很自然,User + Order 即使能够编译,也很难形成稳定语义。
跨越线程边界:Send 与 Sync
Send 和 Sync 没有方法,却决定类型能否进入并发代码:
T: Send
T 的所有权可以安全地转移到另一条线程
T: Sync
&T 可以安全地在线程之间共享
更准确地说,当且仅当 &T: Send 时,T: Sync。
它们是 Auto Trait。普通结构体只要所有字段满足条件,编译器通常会自动推导:
struct Job {
user_id: UserId,
payload: String,
}
// UserId 和 String 都是 Send,Job 自动成为 Send
Rc<T> 使用非原子引用计数,因此不能安全地在线程间移动或共享。Arc<T> 使用原子引用计数,但它也不会让内部的 T 自动线程安全:
Arc<T>: Send + Sync
仍要求 T 满足相应的 Send + Sync 条件
需要跨线程修改共享数据时,经常使用:
Arc<Mutex<State>>
Arc 解决共同所有权,Mutex 解决同一时刻谁能修改。它们不是互相替代的两个方案。
在 thread::spawn 或 tokio::spawn 报出 Future is not Send 时,应当检查哪些值跨越了线程或 .await,而不是尝试手写:
unsafe impl Send for MyType {}
手动实现 Send、Sync 属于底层 unsafe 设计,承诺错误会造成数据竞争和未定义行为。绝大多数业务类型应该让编译器自动判断。
异步计算的协议:Future、IntoFuture、Unpin
async fn 返回的匿名类型实现了 Future:
pub trait Future {
type Output;
fn poll(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Self::Output>;
}
.await 反复推进这个协议:
Poll::Pending
当前不能继续,登记 Waker 并让出执行权
Poll::Ready(value)
异步计算完成
普通业务代码很少手动实现 Future。编译器把 async fn 转换成状态机,Tokio 等 Runtime 负责 Pin 和 poll。
IntoFuture 与 IntoIterator 的角色相似:
IntoIterator
一个值怎样进入 for 循环
IntoFuture
一个值怎样进入 .await
直接 .await 一个 Future 时,有对应的通用实现。库也可以让 Builder 实现 IntoFuture,调用 .await 时再生成真正的异步过程。不过这种设计会让“构建配置”和“开始执行”共享一种语法,使用时要确保行为足够直观。
Unpin 表示类型不依赖 Pin 提供的位置稳定保证。多数普通类型自动实现 Unpin;编译器生成的 Future 则不应该被调用方擅自假定为 Unpin。
日常代码中:
直接 .await 不需要显式 Pin
tokio::spawn Runtime 处理 Pin
join! / select! 临时使用 宏通常处理 Pin
重复借用同一个 !Unpin Future
才常见 tokio::pin! 或 Box::pin
这几个 Trait 的重要性不在于需要频繁手写,而在于它们定义了编译器状态机和异步 Runtime 之间的接口。
动态大小类型:Sized 与 ?Sized
泛型参数默认带有 Sized Bound:
fn store<T>(value: T) {
// 实际隐含 T: Sized
}
编译器需要知道按值传递的 T 占多少空间,才能安排栈帧和移动操作。
但有些类型没有编译期固定大小:
str
[T]
dyn Trait
它们通常放在某种指针后面:
&str
&[T]
Box<dyn Error>
Arc<dyn Handler + Send + Sync>
泛型实现只通过引用处理这类类型时,可以放宽默认约束:
fn print_value<T>(value: &T)
where
T: std::fmt::Display + ?Sized,
{
println!("{value}");
}
?Sized 不是要求 T 必须没有大小,而是表示这里不再强制 T: Sized。函数既能接受普通确定大小类型,也能接受 str 或 dyn Display。
标准库中大量智能指针和引用实现都会出现 T: ?Sized,因为 Box<str>、Arc<[T]> 和 &dyn Trait 都是合理用法。
静态与动态分发:impl Trait 与 dyn Trait
Trait 可以通过泛型静态分发:
fn render(value: &impl Display) {
println!("{value}");
}
也可以通过 Trait Object 动态分发:
fn render(value: &dyn Display) {
println!("{value}");
}
表面只差一个关键字,选择发生在不同阶段:
impl Trait / T: Trait
编译期确定具体类型
静态分发,可以单态化和内联
dyn Trait
运行期通过 vtable 找到实现
可以隐藏和混合不同具体类型
函数一次只处理某一种已知 Writer,泛型通常更直接:
fn write_report<W: Write>(writer: &mut W) -> io::Result<()> {
// ...
}
插件系统需要在一个集合中保存不同实现,则适合 Trait Object:
let plugins: Vec<Box<dyn Plugin + Send + Sync>> = vec![
Box::new(AuditPlugin::new()),
Box::new(MetricsPlugin::new()),
];
不是所有 Trait 都能构造 dyn Trait。方法如果按值返回 Self、包含某些泛型参数,或者 Trait 本身要求 Self: Sized,就可能不满足 Dyn Compatibility。
不要把动态分发简单理解成“性能差”。虚表调用的成本通常不是后端系统的主要瓶颈。更关键的问题是:调用方是否需要知道具体类型,是否需要异构集合,以及 API 是否值得承担动态分配和生命周期复杂度。
后端生态的 Trait:Serialize、Stream、Sink、Service
标准 Trait 提供语言级协议,生态库也会围绕自己的领域建立通用接口。
Serde:Serialize 与 Deserialize
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(transparent)]
struct UserId(u64);
#[derive(Debug, Serialize, Deserialize)]
struct CreateOrder {
user_id: UserId,
items: Vec<OrderItem>,
}
类型实现 Serialize 后,可以被任何兼容 Serializer 转成 JSON、MessagePack 等格式;实现 Deserialize 后,则可以从兼容 Deserializer 构造。
业务类型
实现 Serialize / Deserialize
数据格式
实现 Serializer / Deserializer
二者通过 Serde Data Model 解耦,因此不需要为每一组“业务类型 × 数据格式”单独写转换代码。
业务代码通常使用 derive,只有字段布局、兼容协议或零拷贝借用有特殊要求时才手写实现。
异步序列:Stream 与 Sink
Future 最终只产生一次结果,Stream 则可以异步产生一串值:
Future<Output = T>
Pending ... Ready(T)
Stream<Item = T>
Pending ... Ready(Some(T)) ... Ready(None)
消息订阅、分页结果、WebSocket 输入都可以表示成 Stream。StreamExt 再提供 map、filter、buffer_unordered 等组合方法。
Sink<Item> 表示异步接收一串值的目标。Channel Sender、Socket Writer 等都可以抽象成 Sink。它的 poll_ready 还能表达下游当前没有容量,调用方需要等待背压解除。
Tower:Service
Tower 把一次异步服务调用抽象为:
pub trait Service<Request> {
type Response;
type Error;
type Future: Future<
Output = Result<Self::Response, Self::Error>
>;
fn poll_ready(
&mut self,
cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>>;
fn call(&mut self, request: Request) -> Self::Future;
}
call 表示:
Request → Future<Result<Response, Error>>
poll_ready 则让服务在接收请求前声明自己是否还有容量。限流、超时、重试和日志中间件都可以包装另一个 Service,而不需要知道它最终通过 HTTP、gRPC 还是内存函数处理请求。
Timeout<Service>
Retry<Service>
RateLimit<Service>
Trace<Service>
这很好地体现了 Trait 在大型生态中的作用:定义一个足够小、能够组合的核心协议,其余能力围绕它逐层叠加。
什么时候实现哪个 Trait
遇到自定义类型时,可以先从使用场景反推 Trait:
| 需求 | 常用 Trait |
|---|---|
| 输出调试信息 | Debug |
| 提供自然的用户文本 | Display |
| 显式复制值 | Clone |
| 允许赋值时隐式复制 | Copy + Clone |
| 提供普通默认配置 | Default |
| 离开作用域时清理资源 | Drop |
使用 == | PartialEq,必要时 Eq |
作为 HashMap Key | Eq + Hash |
| 参与完整排序 | Ord + Eq |
| 无失败的值转换 | From |
| 可能失败的值转换 | TryFrom |
| 从字符串解析 | FromStr |
| 接受多种引用参数 | AsRef |
| 使用借用形式查询集合 | Borrow |
| 表现为智能指针 | Deref |
支持 for | IntoIterator |
支持 collect() | FromIterator |
| 向已有集合追加元素 | Extend |
| 接受一次性回调 | FnOnce |
| 接受有状态回调 | FnMut |
| 接受可共享调用的回调 | Fn |
| 接入标准错误链 | Error + Display + Debug |
| 处理同步字节流 | Read / Write / BufRead |
| 跨线程移动所有权 | Send |
| 跨线程共享引用 | Sync |
| 表示一次异步计算 | Future |
| 动态保存不同实现 | dyn Trait |
| 接入 Serde | Serialize / Deserialize |
| 接入 Tower 中间件 | Service |
还可以按实现方式分成三类。
经常直接派生:
Debug
Clone
Copy
Default
PartialEq / Eq
PartialOrd / Ord
Hash
Serialize / Deserialize
经常根据领域语义手写:
Display
From / TryFrom / FromStr
Iterator / IntoIterator
Error
Drop
需要理解,但业务代码很少手写:
Send / Sync
Future / Unpin
Deref
Service
这里没有绝对边界。例如框架作者会频繁实现 Future 和 Service,而普通接口服务更多是在函数签名里使用它们。
Trait 最有价值的地方,不是为类型贴上越来越多标签。每个实现都应该回答一个具体问题:这个类型是否真的存在自然文本表示,转换是否真的不会失败,复制是否真的轻量,比较和 Hash 是否遵守同一套语义。
协议越通用,实现承担的承诺就越长期。选对 Trait,类型便能自然进入 Rust 已有的语法和生态;选错 Trait,得到的语法糖则可能把成本和业务约束一起藏起来。
References
- The Rust Programming Language: Traits
- Rust Standard Library: Formatting Traits
- Rust Standard Library:
Clone - Rust Standard Library:
Copy - Rust Standard Library: Comparison Traits
- Rust Standard Library: Conversion Traits
- Rust Standard Library:
Borrow - Rust Standard Library:
Deref - Rust Standard Library:
Iterator - Rust Standard Library:
IntoIterator - Rust Standard Library:
FromIterator - Rust Standard Library:
Fn,FnMut, andFnOnce - Rust Standard Library:
Error - Rust Standard Library:
Read - Rust Standard Library:
Send - Rust Standard Library:
Sync - Rust Standard Library:
Future - Rust Standard Library:
Unpin - Serde:
SerializeandDeserialize - Futures:
Stream - Tower:
Service