Appearance
第4章 Executor trait:连接、Pool、Transaction 的共同接口
"If you can't say it in one primitive, you have the wrong primitive." —— trait 设计的迭代真理
本章要点
Executor<'c>trait(sqlx-core/src/executor.rs:33)定义了**"能跑查询的东西"**——4 个必实现方法(fetch_many+fetch_optional+prepare_with+describe),6 个带默认实现(execute/execute_many/fetch/fetch_all/fetch_one/prepare)—— 驱动作者只需实现 4 个原语、其余由 core 默认构造。- 两个核心流式原语——
fetch_many返回BoxStream<Either<QueryResult, Row>>(流式 + 多结果集),fetch_optional返回BoxFuture<Option<Row>>(最多一行)。高层默认实现:execute_many/fetch/fetch_all基于fetch_many;fetch_one基于fetch_optional(未取到行时返RowNotFound);execute基于execute_many;prepare基于prepare_with。 - 只有两种类型实现 Executor:
&Pool<DB>(pool/executor.rs:12)和&mut DB::Connection(每个驱动各自的connection/executor.rs)——极简清单。 &mut Transaction和&mut PoolConnection被从 Executor impl 列表里删掉不是因为 coherence——是因为 "fails to compile due to lack of lazy normalization"(transaction.rs:132注释原文)和 "Causes an overflow when evaluating&mut DB::Connection: Executor"(pool/executor.rs:71注释原文)。两个都是 Rust trait 求解器的具体限制。Execute<'q, DB>trait 是 query shape 的抽象——&str、(&str, Option<Arguments>)、Query<DB, A>各自实现它。为什么不为String实现?刻意留的防御层(§4.5)。prepare/describe方法是query!宏的私有接口——describe标注#[doc(hidden)],专为编译期类型检查设计。
4.1 问题引入:什么东西应该能跑查询
上一章的 Database trait 描述了"一个 sqlx 驱动应该提供哪些类型"。现在切换视角:用户代码在调 .fetch_one(&x) 时,这个 x 是什么? 用户会传什么?答案有三个:
&pool——从连接池随机借一个连接、跑完归还。&mut conn——显式拿到的单个连接,能保证两条 SQL 落到同一物理连接。&mut tx——事务中的连接,保证这些 SQL 在同一事务边界。
这三个"能跑查询的东西"形态不同:Pool 是 Arc<PoolInner>,Connection 是独占的 TCP stream,Transaction 是包装了 Connection 的 RAII guard。让它们共用同一套 .fetch_one() / .fetch_all() / .execute() API,就需要一个操作接口级的 trait——这就是 Executor。
注意这里的边界:Database trait 是类型级 manifest(驱动提供什么类型),Executor 是操作级 contract(什么东西能跑查询)。前者是"名词",后者是"动词"。
sqlx 把这个"能跑查询的东西"抽象成 Executor<'c> trait——其中 'c 是 executor 自身的借用生命周期(Pool 的 &pool 借用、Connection 的 &mut conn 借用)。这个 trait 的设计相当简约——4 个必实现方法(fetch_many / fetch_optional / prepare_with / describe)+ 6 个默认方法(execute / execute_many / fetch / fetch_all / fetch_one / prepare),下文逐一拆。
4.2 Executor trait 的全貌
打开 sqlx-core/src/executor.rs:33-180,省略文档注释:
rust
pub trait Executor<'c>: Send + Debug + Sized {
type Database: Database;
// 必实现
fn fetch_many<'e, 'q: 'e, E>(self, query: E) -> BoxStream<'e, Result<
Either<<Self::Database as Database>::QueryResult,
<Self::Database as Database>::Row>,
Error>>
where 'c: 'e, E: 'q + Execute<'q, Self::Database>;
fn fetch_optional<'e, 'q: 'e, E>(self, query: E) -> BoxFuture<'e, Result<
Option<<Self::Database as Database>::Row>, Error>>
where 'c: 'e, E: 'q + Execute<'q, Self::Database>;
fn prepare_with<'e, 'q: 'e>(self, sql: &'q str,
parameters: &'e [<Self::Database as Database>::TypeInfo])
-> BoxFuture<'e, Result<<Self::Database as Database>::Statement<'q>, Error>>
where 'c: 'e;
#[doc(hidden)]
fn describe<'e, 'q: 'e>(self, sql: &'q str)
-> BoxFuture<'e, Result<Describe<Self::Database>, Error>>
where 'c: 'e;
// 默认方法:execute、execute_many、fetch、fetch_all、fetch_one、prepare
}四个必实现方法、六个默认方法。让我们把它们放进一张分层图:
这张图揭示了 sqlx Executor 最重要的设计判断:fetch_many 是唯一的读原语。所有"读行"类操作都建立在它之上。
4.3 fetch_many 与 fetch_optional 为什么是两个原语
fetch_many 的返回类型是整章最需要停下来看清楚的:
rust
BoxStream<'e, Result<Either<DB::QueryResult, DB::Row>, Error>>拆开读:
- 外层
Stream——异步可迭代的一连串 item。 - 每个 item 是
Result<Either<QueryResult, Row>, Error>。 Either::Left(QueryResult)——"这条 SQL 执行完了,影响了 N 行"。Either::Right(Row)——"下一行数据"。
为什么要这种"混合流"?因为一次 SQL 调用可能包含多条语句:
sql
UPDATE users SET active = false WHERE last_login < '2020-01-01';
SELECT id, name FROM users WHERE active = true;Postgres / MySQL 的 simple query protocol 允许把这一整段文本一次发过去——服务端会按分号切分、顺序执行、每条语句返回自己的结果。第一条是 UPDATE,返回一个 "UPDATE 42" 命令完成消息(对应 QueryResult { rows_affected: 42 });第二条是 SELECT,返回 0..N 行数据 + 最后一个 "SELECT N" 完成消息。
fetch_many 的 Either 流把这个行为直接暴露到类型系统——你作为调用方能看到这个交替序列。从 execute_many / fetch / execute 这三个派生方法的实现可以看出是怎么"降维"的。
fetch(executor.rs:73-88)——只要行,丢掉 QueryResult:
rust
fn fetch<'e, 'q: 'e, E>(self, query: E) -> BoxStream<'e, Result<DB::Row, Error>>
where 'c: 'e, E: 'q + Execute<'q, Self::Database>,
{
self.fetch_many(query)
.try_filter_map(|step| async move {
Ok(match step {
Either::Left(_) => None,
Either::Right(row) => Some(row),
})
})
.boxed()
}execute_many(executor.rs:50-68)——只要 QueryResult,丢掉行:
rust
fn execute_many<'e, 'q: 'e, E>(self, query: E) -> BoxStream<'e, Result<DB::QueryResult, Error>>
where 'c: 'e, E: 'q + Execute<'q, Self::Database>,
{
self.fetch_many(query)
.try_filter_map(|step| async move {
Ok(match step {
Either::Left(rows) => Some(rows),
Either::Right(_) => None,
})
})
.boxed()
}execute(executor.rs:37-46)——execute_many.try_collect(),把所有 QueryResult 累加成一个:
rust
fn execute<'e, 'q: 'e, E>(self, query: E) -> BoxFuture<'e, Result<DB::QueryResult, Error>>
where 'c: 'e, E: 'q + Execute<'q, Self::Database>,
{
self.execute_many(query).try_collect().boxed()
}try_collect 依赖 DB::QueryResult: Extend<DB::QueryResult>——这条 bound 在第 3 章讲过,正是为这里准备的。如果你的 SQL 有两条 UPDATE,execute 返回的是"总 rows_affected = 42 + 17",实现靠 PgQueryResult::extend 里一行 self.rows_affected += other.rows_affected。
fetch_all(executor.rs:96-104)——fetch.try_collect(),把所有 Row 收集进 Vec:
rust
fn fetch_all<'e, 'q: 'e, E>(self, query: E) -> BoxFuture<'e, Result<Vec<DB::Row>, Error>>
where 'c: 'e, E: 'q + Execute<'q, Self::Database>,
{
self.fetch(query).try_collect().boxed()
}fetch_one(executor.rs:107-121)——fetch_optional + 空值报错:
rust
fn fetch_one<'e, 'q: 'e, E>(self, query: E) -> BoxFuture<'e, Result<DB::Row, Error>>
where 'c: 'e, E: 'q + Execute<'q, Self::Database>,
{
self.fetch_optional(query)
.and_then(|row| match row {
Some(row) => future::ok(row),
None => future::err(Error::RowNotFound),
})
.boxed()
}七个"用户视角的读/执行方法"(execute、execute_many、fetch、fetch_many、fetch_all、fetch_one、fetch_optional)背后只有两个必实现原语:fetch_many 提供流式读主干、fetch_optional 留作驱动的 "最多一行" 协议级优化入口(再加 prepare_with / describe 两个 meta 方法、共 4 个必实现)。这是 sqlx Executor 最值得称道的设计——用单个 Either 流表达所有读路径,然后用 futures-util 里的 combinator 做投影。驱动作者只要写对 fetch_many,其它方法自动获得正确行为。
为什么 fetch_optional 也要做成必实现?从源码注释和其他默认实现的包装方式推测:因为 fetch_one 只需要一行、fetch_optional 要不要拉满、两者都可以在 fetch_many 的基础上通过 take(1) 实现,但驱动可以做协议级的优化——比如 Postgres 的 Extended Query 在 Bind 阶段可以用 max_rows=1 限制服务端,少传送后面的数据。把 fetch_optional 留给驱动覆盖是优化留口。
4.3.1 Either 流的实际形态
为了把 Either<QueryResult, Row> 这个抽象落地,我们看三个具体场景它长什么样:
场景 A:单条 SELECT,返回 3 行
Right(Row1)
Right(Row2)
Right(Row3)
Left(QueryResult { rows_affected: 3 }) // Postgres 以 CommandComplete 结尾;部分驱动也会给一条 QueryResult场景 B:单条 UPDATE,影响 17 行
Left(QueryResult { rows_affected: 17 }) // UPDATE 不返回行,直接一条 QueryResult场景 C:多条语句用分号拼接
sql
UPDATE users SET active = false WHERE last_login < '2020-01-01';
SELECT id, name FROM users WHERE active = true;
DELETE FROM audit_log WHERE created < '2019-01-01';流形态:
Left(QueryResult { rows_affected: 42 }) // UPDATE 完成
Right(Row{id:1, name:"Alice"}) // SELECT 第一行
Right(Row{id:2, name:"Bob"}) // SELECT 第二行
Left(QueryResult { rows_affected: 2 }) // SELECT 完成(影响的"行数"就是返回的行数)
Left(QueryResult { rows_affected: 1000 }) // DELETE 完成场景 C 是 fetch_many 的真正用武之地——你能一条流看到三条语句的交替结果。如果用 fetch_all,你拿到的是 Vec<Row> 只含 Alice 和 Bob,两条 QueryResult 被丢弃——大多数业务不 care、丢了也就丢了,但迁移脚本或管理工具可能需要知道 UPDATE 和 DELETE 的影响行数。这时候 execute_many(只保留 Left)或 fetch_many(全保留)才有意义。
这也是为什么 sqlx-cli 的 sqlx migrate run 用 execute_many 而不是 execute——它要把每条迁移 SQL 的结果都记录进 _sqlx_migrations 表。
4.3.2 为什么不用 AsyncIterator
Rust 生态里表示"异步迭代"的 trait 有两种:
futures_core::Stream(sqlx 用的)——fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Item>>std::async_iter::AsyncIterator(#![feature(async_iterator)]还在 nightly)
sqlx Executor::fetch_many 返回 BoxStream——前者。这个选择是时代产物:Stream 在 futures-rs 0.1 时代就稳定了,生态里所有的 combinator(try_filter_map、try_collect)都围绕它构建。sqlx 0.1 到 0.8 一直用 Stream,没理由切换到一个还在 nightly 的 trait。
BoxStream 而不是 impl Stream——这是异步 trait 方法的固定代价。impl Stream 在 async fn trait 方法里还不稳定(2026 年 4 月,RPIT in traits 已稳定但 AFIT 对 Stream 返回仍有 Send 推导问题)。Box::pin 强制堆分配,但让 trait 本身 object-safe(虽然带 GAT 依然不行,见 §3.3.1),也让生命周期分析简单。
这条选择在第 9.0-alpha 里传闻会变——用 -> impl Stream<Item = ...> + Send 替换 BoxStream,省一次堆分配。但那依赖 Rust 编译器更多的 AFIT 改进。
4.4 谁实现 Executor?只有两个
整个 sqlx 生态里实现 Executor 的类型极少:
&Pool<DB>——sqlx-core/src/pool/executor.rs:12&mut PgConnection/&mut MySqlConnection/&mut SqliteConnection——各自驱动的connection/executor.rs(Postgres 在sqlx-postgres/src/connection/executor.rs:380)&mut AnyConnection——sqlx-core/src/any/connection/executor.rs:11&mut PgListener——sqlx-postgres/src/listener.rs:388,用于 Postgres LISTEN/NOTIFY 场景
以上是全部。用户代码看到的所有 .fetch_one(X) 里的 X,最终都走这四条路径之一——其中前两条覆盖 99% 用例。
4.4.1 &Pool<DB> 的实现
sqlx-core/src/pool/executor.rs:12-73 的完整实现值得通读:
rust
impl<'p, DB: Database> Executor<'p> for &'_ Pool<DB>
where
for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
{
type Database = DB;
fn fetch_many<'e, 'q: 'e, E>(self, query: E)
-> BoxStream<'e, Result<Either<DB::QueryResult, DB::Row>, Error>>
where E: 'q + Execute<'q, Self::Database>,
{
let pool = self.clone();
Box::pin(try_stream! {
let mut conn = pool.acquire().await?;
let mut s = conn.fetch_many(query);
while let Some(v) = s.try_next().await? {
r#yield!(v);
}
Ok(())
})
}
// fetch_optional / prepare_with / describe 类似——都是 pool.acquire().await?.XXX
}几个值得注意的细节:
where for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>——一条高阶 trait bound(HRTB)。它说:对于任何生命周期'c,&'c mut DB::Connection都必须实现Executor<'c>。这条约束是必要的——因为fetch_many内部要调conn.fetch_many(query),而那个conn是PoolConnection<DB>,用 DerefMut 投影到&mut DB::Connection。如果不保证后者是 Executor,这条链就断了。let pool = self.clone();——Pool<DB>内部是Arc<PoolInner<DB>>,Clone就是增加引用计数。clone 的原因是要把 pool move 进try_stream!生成的异步块,延长生命周期(从&'p Pool到 stream 自身的'e)。try_stream! { ... }——来自async-streamcrate 的宏,把 "生产多个值的异步代码" 包装成一个Stream。r#yield!(v)把值送到 stream 的poll_next。- 连接归还:
let mut conn = pool.acquire().await?;拿到PoolConnection——它在 stream 结束(无论正常还是错误)时被 drop,drop 实现里归还连接给 pool。这就是为什么&pool.fetch_one(...)用起来这么丝滑——连接的获取和归还全被 Executor impl 包掉了。
4.4.2 &pool vs &mut conn vs &mut *tx:三种 Executor 的选择
既然只有 &Pool<DB> 和 &mut DB::Connection 两种 Executor,用户代码里会看到四种实际写法:
| 写法 | 类型(实际) | 适用场景 | 连接归属 |
|---|---|---|---|
fetch_one(&pool) | &Pool<DB> | 单条独立查询,无跨查询一致性需求 | 每次 acquire + 立即归还 |
fetch_one(&mut conn) | &mut DB::Connection | 已经从 pool 借出,要复用一段时间 | 持续占用,drop 时归还 |
fetch_one(&mut pool_conn) | &mut PoolConnection<DB>(自动 deref) | 同上,但从 pool 借出 | 同上 |
fetch_one(&mut *tx) | &mut DB::Connection(经 DerefMut) | 事务中查询 | 同事务内 |
&pool 的成本:每次调用都执行一次 acquire + fetch + drop。acquire 在 pool 空闲时是 O(1)(从 ArrayQueue 弹一个),繁忙时会排队等 permit(AsyncSemaphore)。单看一次调用极快,但高频调用(例如 handler 里六条查询)每条都过 acquire 路径,累计 6 次 permit 争用。
&mut conn 的成本:一次 pool.acquire().await? 借出,后面所有查询共用这一个 TCP 连接。优点是省 6 次 permit;缺点是你占住了池子里一条连接直到变量 drop——其他 handler 如果也用 pool,可能排队等你。
经验法则(在第 22 章会再细化):
- 单条查询 + 不在循环里:
&pool。 - 同一逻辑单元里 ≥ 3 条查询:
let mut conn = pool.acquire().await?;然后重复用&mut conn。 - 跨查询一致性要求(读-改-写、读后避免其他事务插入):开 Transaction,用
&mut *tx。
所有这些写法都通过 Executor 这一个 trait 统一——用户不需要记四个 API,只需要记"能跑查询的就是 Executor"。
4.4.3 &mut PgConnection 的实现
sqlx-postgres/src/connection/executor.rs:380 是真正干活的地方:
rust
impl<'c> Executor<'c> for &'c mut PgConnection {
type Database = Postgres;
fn fetch_many<'e, 'q, E>(self, mut query: E)
-> BoxStream<'e, Result<Either<PgQueryResult, PgRow>, Error>>
where 'c: 'e, E: Execute<'q, Self::Database>, 'q: 'e, E: 'q,
{
let sql = query.sql();
let metadata = query.statement().map(|s| Arc::clone(&s.metadata));
let arguments = query.take_arguments().map_err(Error::Encode);
let persistent = query.persistent();
Box::pin(try_stream! {
let arguments = arguments?;
let mut s = pin!(self.run(sql, arguments, persistent, metadata).await?);
while let Some(v) = s.try_next().await? {
r#yield!(v);
}
Ok(())
})
}
// ...
}注意这里不再 clone——&mut PgConnection 是独占借用,本来就是"只借一次"。整个 Executor impl 把用户传入的 query: E 拆成 sql、metadata、arguments、persistent 四块,然后调用 PgConnection::run(sqlx-postgres/src/connection/executor.rs 的底部方法)——这是整个 Postgres 协议交互的入口,也是第 16 章的主题。
4.4.4 &mut PgListener 为什么单独实现 Executor
sqlx-postgres/src/listener.rs:388 多了一个看起来突兀的 impl:
rust
impl<'c> Executor<'c> for &'c mut PgListener { ... }PgListener 是 Postgres 独有的 LISTEN / NOTIFY 事件订阅者——用户调 PgListener::listen("channel_name").await? 注册感兴趣的 channel,然后 .recv().await? 阻塞等通知。它内部持有一个 PgConnection,但不直接暴露——如果把 PgListener 当成普通 PgConnection 用,会把 LISTEN 状态搞坏。
为什么还要让它实现 Executor?因为有时候用户确实想在 Listener 的同一个连接上跑一条 SQL——比如在 recv 之前跑一条初始化查询。如果不让 PgListener 实现 Executor,用户必须"开 listener 之前先用 pool 借一条连接跑 SQL、再换 listener"——两条连接开销加倍。
Listener 的 Executor impl 内部委托给它持有的 connection,但在 SQL 执行前后小心不要把通道订阅状态破坏。这是"特殊情况特殊处理"的一个案例——显示 Executor trait 的 implementor 集合不是一刀切的"所有连接类型都 impl",而是有意识地为需要的场景保留入口。
MySQL 和 SQLite 没有对应的 Listener 类型,所以也没有对应的 Executor impl。这种"只给有用场景留 impl"的精打细算是 sqlx 的一贯做法。
4.5 Execute<'q, DB>:查询形态的抽象
fetch_many 的参数 E: Execute<'q, Self::Database> 里的 Execute 是另一个 trait——query shape 的抽象(executor.rs:194-218):
rust
pub trait Execute<'q, DB: Database>: Send + Sized {
fn sql(&self) -> &'q str;
fn statement(&self) -> Option<&DB::Statement<'q>>;
fn take_arguments(&mut self) -> Result<Option<DB::Arguments<'q>>, BoxDynError>;
fn persistent(&self) -> bool;
}四个方法对应一条 SQL 查询的四个属性:
sql——SQL 字符串。statement——如果 SQL 已经预处理过,复用 statement 缓存。take_arguments——参数(可选——无参数的查询用 simple query protocol)。persistent——是否缓存 prepared statement 到连接。
这个 trait 有两个内置实现(同一个文件 220-260 行):
rust
impl<'q, DB: Database> Execute<'q, DB> for &'q str {
fn sql(&self) -> &'q str { self }
fn statement(&self) -> Option<&DB::Statement<'q>> { None }
fn take_arguments(&mut self) -> Result<Option<DB::Arguments<'q>>, BoxDynError> { Ok(None) }
fn persistent(&self) -> bool { true }
}
impl<'q, DB: Database> Execute<'q, DB> for (&'q str, Option<DB::Arguments<'q>>) {
fn sql(&self) -> &'q str { self.0 }
fn statement(&self) -> Option<&DB::Statement<'q>> { None }
fn take_arguments(&mut self) -> Result<Option<DB::Arguments<'q>>, BoxDynError> { Ok(self.1.take()) }
fn persistent(&self) -> bool { true }
}第一条让你直接 pool.execute("DELETE FROM cache").await? ——字符串字面量本身就是 Execute。
第二条让你 (sql_str, Some(args)) 也算 Execute——这是 query(sql).bind(x) 这条链的最底层表达。
第三个实现是 Query<'q, DB, A> 自己——在 sqlx-core/src/query.rs 定义,我们在第 9 章详细看。
4.5.1 为什么不给 String 实现 Execute?
executor.rs:221 上方有一行意味深长的注释:
rust
// NOTE: `Execute` is explicitly not implemented for String and &String to make it slightly more
// involved to write `conn.execute(format!("SELECT {val}"))`"刻意不为 String 和 &String 实现 Execute,让 conn.execute(format!("SELECT {val}")) 稍微难写一点。"
这是 SQL 注入防御的 trait 级拒绝。考虑下面这两种写法:
rust
// 类型系统不拦:
conn.execute("SELECT * FROM users WHERE id = 1").await?;
// 类型系统拒绝(String 不是 Execute):
let id = "1 OR 1=1";
conn.execute(format!("SELECT * FROM users WHERE id = {id}")).await?;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// expected `&str` or tuple, found `String`要让后者编译过,用户必须显式转换 &format!(...) 或 format!(...).as_str()——这一步多出的打字让"字符串拼 SQL"这个危险操作留下可 grep 的痕迹。审计代码的人 grep 一下 \.as_str\(\).*execute 或 &format!.*execute,就能发现所有潜在注入点。
相比之下,如果 String 直接是 Execute,execute(format!(...)) 就零摩擦——类型系统帮不了你。这一行注释背后的判断是:trait 实现集合是 API 契约的一部分,能用类型系统传达"不鼓励这样用"就不要留方便的 overload。这是 Rust 生态里 trait 设计的一个经典安全案例。
当然,如果你真的有必要动态拼 SQL(例如 "这 N 个 UUID 用 IN 查询"),sqlx 提供了 QueryBuilder——第 10 章详细。QueryBuilder 内部有 push 和 push_bind——前者拼裸 SQL(由你保证安全),后者追加参数槽(注入安全)。
4.6 为什么 &mut Transaction 和 &mut PoolConnection 不是 Executor
这是第 1 章"让步二·附"的承诺——现在从 trait 边界视角把这件事讲清楚。
sqlx-core/src/pool/executor.rs:72-142 有一整段被注释掉的 impl Executor for &mut PoolConnection,注释的开头就是原因:
rust
// Causes an overflow when evaluating `&mut DB::Connection: Executor`.sqlx-core/src/transaction.rs:132-200 同样有一整段被注释的 impl Executor for &mut Transaction,注释的开头:
rust
// NOTE: fails to compile due to lack of lazy normalization这两个注释记录了 0.7→0.8 这次破坏性变更的真正技术动因——不只是"coherence 冲突"这种表述,而是 Rust 编译器的两个具体限制。
4.6.1 问题一:trait solver overflow
第一个问题出在 PoolConnection 上。原来的 impl(注释前的形态)是这样的:
rust
impl<'c, DB: Database> Executor<'c> for &'c mut PoolConnection<DB>
where
&'c mut DB::Connection: Executor<'c, Database = DB>,
{
type Database = DB;
fn fetch_many<'e, 'q: 'e, E: 'q>(self, query: E) -> ... {
(**self).fetch_many(query) // DerefMut 到 DB::Connection,再调 fetch_many
}
// ...
}问题是:当 Rust trait solver 尝试解 &mut PoolConnection: Executor 时:
- 看 impl,得到约束
&mut DB::Connection: Executor。 - 为了验证
&mut DB::Connection: Executor,trait solver 再次查找 impl——找到impl Executor for &mut PgConnection(具体类型)。 - 但它看见上面这条
impl Executor for &mut PoolConnection也可能匹配(如果DB::Connection被替换为PoolConnection<X>)。 - 于是递归展开
&mut PoolConnection<X>: Executor要求&mut X::Connection: Executor…… - 解不出终点,触发 "overflow evaluating the requirement" 错误。
问题的根源是 DB::Connection 这个关联类型投影,编译器在 Pool Connection 这一层无法惰性规约——它必须把所有可能的 DB 实例都展开一遍才能知道到底是哪个具体 Connection 类型。这就是 "fails to compile due to lack of lazy normalization" 的含义:关联类型 Self::Database::Connection 在泛型上下文中不会被延迟规约(lazy normalize)到具体类型,必须立刻展开,但展开时可能触发无限递归。
4.6.2 问题二:lazy normalization 的同类症状
Transaction 的问题是同一种——impl Executor for &mut Transaction<'c, DB> 要求 &mut DB::Connection: Executor。因为 Transaction 本身有 DerefMut<Target = DB::Connection>(transaction.rs:224),直觉上 &mut tx 应该自动 coerce 成 &mut DB::Connection。但 auto-deref coercion 对 impl Executor 的 trait bound 不生效——Rust 的 trait solver 不会在 "匹配 Executor impl" 这一步做 Deref。
开发者的 workaround 是把 DerefMut 展开留给用户:
rust
// 用户显式写这一步 deref
sqlx::query(...).fetch_one(&mut *tx).await?;&mut *tx 明确告诉编译器:"我先 deref 到 Transaction 里面的 DB::Connection,再 &mut 它"——这样编译器看到的类型直接是 &mut PgConnection,匹配具体的 impl Executor for &mut PgConnection,无 overflow 无 normalization 问题。
4.6.3 设计教训
这次变更的背后有一条普适教训:trait 家族的可组合性受 Rust 编译器的具体能力限制。你在白板上设计的 trait 层级可能完全合理,但落到 rustc 的 trait solver 上会遇到现实问题——lazy normalization(关联类型的按需规约)、coherence(impl 冲突)、higher-ranked subtyping(HRTB 下的子类型)都可能让一个逻辑上"应该行"的 impl 根本编译不过。
sqlx 团队选择的做法是不对抗编译器,让用户写一次 *——这是最小代价的务实解。这和第 2 章 §2.3 讨论的"proc-macro 物理约束"一样——都是把语言层面的限制诚实地暴露给用户,而不是在内部堆补丁。
0.9.0-alpha.1 的一项传闻改动正是"重写 Executor trait"——可能针对这两个问题做更彻底的修复(比如换成 AsyncFn、等 Rust 的 next_solver 功能稳定)。但 0.8 时代我们只能接受 &mut *tx 这个妥协。
4.6.4 Executor trait 的版本演进
把 Executor trait 放到版本演进里看,能理解每次改动的因果:
- 0.4 到 0.5:
Executor从一个"有execute、fetch_all、fetch_one的大 trait"瘦身成fetch_many为原语的精炼 trait。这次 refactor 让驱动作者少写大量重复代码(以前每个驱动要同时实现execute和fetch,很多逻辑冗余)。 - 0.5 到 0.6:
Executortrait 被挪到sqlx-core(第 2 章 §2.7.1 里讲过 workspace 演进),接口本身没大改。 - 0.6 到 0.7:带来了 GAT——
fetch_many签名里的'q生命周期约束变得比for<'q> Hxxx<'q>时代清爽得多。同时也是这次把驱动 crate 独立发布,&mut DB::Connection: Executor的 impl 从sqlx-core挪到每个驱动 crate(以前在 core 里,现在在sqlx-postgres等)。 - 0.7 到 0.8(本书锁定):
&mut Transaction和&mut PoolConnection的 blanket impl 被删除。理由上面 §4.6.1-4.6.3 讲完了——lack of lazy normalization+overflow两个编译器限制。 - 0.9.0-alpha.1:传闻中
Executortrait 会被进一步改造,可能利用 Rust 更新的 AFIT(async fn in traits)和 RPITIT 特性,让BoxStream变成impl Stream + Send,少一次堆分配。
这条演进里反复出现的主题是"实现成本 vs 用户友好性"的权衡——0.5 的 refactor 是为了降低驱动作者的实现成本,0.7 的 GAT 是为了让上层代码清爽,0.8 的删除是为了解决编译器限制。每次改动都有取舍,每次取舍都留下一个可追溯的源码印记(比如 executor.rs:22-30 的说明注释就是 0.8 那次改动留下的)。
读懂这条演进线的价值是:当你遇到 sqlx 某一处看起来"绕"的设计,你应该能把它归因到某一次演进——而不是觉得作者当初就这么想的。
4.7 prepare / describe:宏系统的私有接口
两个方法讨论得较少,是因为用户几乎不直接调它们:
rust
fn prepare<'e, 'q: 'e>(self, query: &'q str)
-> BoxFuture<'e, Result<DB::Statement<'q>, Error>>
where 'c: 'e
{
self.prepare_with(query, &[])
}
fn prepare_with<'e, 'q: 'e>(self, sql: &'q str,
parameters: &'e [DB::TypeInfo])
-> BoxFuture<'e, Result<DB::Statement<'q>, Error>>
where 'c: 'e;
#[doc(hidden)]
fn describe<'e, 'q: 'e>(self, sql: &'q str)
-> BoxFuture<'e, Result<Describe<DB>, Error>>
where 'c: 'e;prepare 的意义有两条:
- 让你在执行前检查一个查询的元数据——
conn.prepare("SELECT ...").await?.columns()能拿到列名和列类型。这对生成动态 UI(表头)或迁移校验很有用。 prepare_with接收parameters: &[TypeInfo]—— Postgres 能利用这些信息做参数类型推断(文档说"Only some database drivers (PostgreSQL, MSSQL) can take advantage of this extra information")。SQLite / MySQL 忽略。
describe 是宏的私有通道。#[doc(hidden)] 意味着文档站看不到、不是公开 API。它的用户只有一个:sqlx-macros-core 的 CachingDescribeBlocking<DB>(第 2 章 §2.3.1 讨论过)——编译期把 SQL 字符串送进 describe,拿回 Describe<DB>(包含参数 OIDs 和列 TypeInfos),再用这些信息生成 try_get_unchecked::<i32, _>(0) 这种带类型的代码。
Describe<DB> 结构(sqlx-core/src/describe.rs)简单:
rust
pub struct Describe<DB: Database> {
pub columns: Vec<DB::Column>,
pub parameters: Option<Either<Vec<DB::TypeInfo>, usize>>,
pub nullable: Vec<Option<bool>>,
}parameters 的 Either<Vec<TypeInfo>, usize> 正是第 3 章 §3.5.3 讨论过的"Postgres 给完整类型 / SQLite 只给个数"的差异,在 Describe 里以统一形态表达。nullable 是 Vec<Option<bool>>——每一列是否可空,None 表示驱动也说不准(比如表达式列)。
这三个字段就是编译期类型检查的全部信息源——query!("SELECT id, name FROM users WHERE id = $1", id) 的类型检查只看 describe 的返回。第 11 章会详细讲这条编译期校验路径。
4.7.1 describe 一次实际走过的路径
用一次具体 query! 展开来理解 describe 怎么被宏消费。假设用户代码是:
rust
sqlx::query!("SELECT id, name, email FROM users WHERE id = $1", user_id)编译期发生的事:
sqlx-macros的expand_query入口函数(sqlx-macros-0.8.6/src/lib.rs:10)把 TokenStream 交给sqlx-macros-core::query::expand_input。expand_input解析出 SQL 字符串"SELECT id, name, email FROM users WHERE id = $1"和参数表达式user_id。- 它根据
DATABASE_URL环境变量决定去哪个驱动:postgres://localhost/test就选QueryDriver::new::<Postgres>()(来自FOSS_DRIVERS,第 2 章 §2.3.1)。 - 调用
Postgres::describe_blocking(sql, database_url)——这是DatabaseExttrait 的方法,内部用sqlx-macros-core的block_on(第 2.5.1 节)跑一个 Tokio 当前线程 runtime。 block_on里的逻辑大致是:let conn = PgConnection::connect(url).await?; conn.describe(sql).await?——调的正是本章讨论的Executor::describe。- Postgres 驱动的
describe实现发 Parse + Describe 消息给服务端,拿回ParameterDescription(参数 OIDs)和RowDescription(列信息)。 - 返回的
Describe<Postgres>含:columns: Vec<PgColumn>,三列:id(OID 23 = INT4)、name(OID 25 = TEXT)、email(OID 25 = TEXT)。parameters: Some(Left(vec![PgTypeInfo::INT4]))——一个参数,期望 INT4。nullable: vec![Some(false), Some(false), Some(true)]——从 pg_catalog 查出的可空性。
expand_input拿着Describe做两件事:- 验证
user_id的 Rust 类型能Encode到PgTypeInfo::INT4(第 5 章详细)。 - 根据
columns+nullable生成出参结构({id: i32, name: String, email: Option<String>})。
- 验证
- 最终生成的 TokenStream 就是第 1 章 §1.5.1 讨论过的那段带
try_get_unchecked::<i32, _>(0)的展开。
整条路径唯一和 Executor trait 直接接触的是步骤 5 的 conn.describe(sql).await?——这是宏系统看 sqlx 的"窥视孔"。#[doc(hidden)] 这个标注不是简单为了"不想暴露"——它是明确告诉用户"这不是给你调的 API,是宏内部用的"。公共 API 里 fetch_* 系列永远够用,describe 是保留字段。
4.8 生命周期约束 'c: 'e, 'q: 'e
Executor trait 的每个方法签名都有两条生命周期约束,值得单独讲。以 fetch_many 为例:
rust
fn fetch_many<'e, 'q: 'e, E>(self, query: E)
-> BoxStream<'e, Result<Either<DB::QueryResult, DB::Row>, Error>>
where 'c: 'e, E: 'q + Execute<'q, Self::Database>;四个生命周期:
'c——Executor 自身的借用(&'c pool或&'c mut conn)。来自Executor<'c>。'e——返回的 stream/future 的生命周期。'q——查询(SQL 字符串 + 参数)自身的生命周期。E: 'q——E 的实现至少活到'q(即 E 里的任何引用都比'q长)。
约束 'c: 'e:executor 的借用至少活到 stream 结束——这是必须的,因为 stream 内部要持有 executor(比如 Pool 的 clone、Connection 的 &mut self)。如果 executor 先 drop,stream 的 poll_next 就变成悬垂引用。
约束 'q: 'e:query 至少活到 stream 结束——Postgres 协议在 Parse 阶段就把 SQL 字符串发给服务端,之后 SQL 本身可以丢弃,但 sqlx 保留了 borrow 链一路到 stream 完成。这是保守的约束,实际上对大多数驱动可以更宽松(比如 MySQL simple query 是一次性发的)。
这些约束组合起来解决了"异步借用合法性"这个 Rust 的常见问题——返回的 Future/Stream 不能比它借用的东西(executor、query)更长命。Rust 1.85 之前这类约束必须显式写出来(RPIT lifetime capture rules);1.85 之后部分场景可以依赖 use<> 推导,但 sqlx 为了 MSRV 兼容一直保留显式标注。
4.8.1 一次具体的生命周期错误
想象用户写了一个返回 stream 的 helper:
rust
fn query_users(pool: &PgPool) -> BoxStream<Result<PgRow>> {
let sql = format!("SELECT * FROM users"); // String,活到这行结束
pool.fetch(&*sql) // 返回 stream
} // 这里 sql 被 drop 了!编译器的报错大致是:
error[E0597]: `sql` does not live long enough
--> src/main.rs:4:18
|
3 | let sql = format!("SELECT * FROM users");
| --- binding `sql` declared here
4 | pool.fetch(&*sql)
| -----------^^^^^-
| | |
| | borrowed value does not live long enough
| argument requires that `sql` is borrowed for `'q`
| where `'q: 'e`
5 | }
| - `sql` dropped here while still borrowed错误的根因是 fetch 签名 'q: 'e——stream 的生命周期 'e 至少要和 'q(SQL 字符串的借用)一样短。但 sql 是局部变量,&*sql 的生命周期不能延伸到函数外。
修法是把 String 的所有权移入 stream:
rust
fn query_users(pool: &PgPool) -> BoxStream<'_, Result<PgRow>> {
let sql = "SELECT * FROM users"; // &'static str
pool.fetch(sql)
}或者用 QueryBuilder 把动态字符串移入 Query 对象(Query 拥有 String 的所有权,生命周期就和 stream 绑定):
rust
fn query_users_dyn(pool: &PgPool, table: &str) -> BoxStream<'_, Result<PgRow>> {
let mut qb = QueryBuilder::<Postgres>::new("SELECT * FROM ");
qb.push(table); // 字符串被 move 进 QueryBuilder
qb.build().fetch(pool)
}这类错误是 sqlx 新手最常遇到的一种。读懂 'c: 'e, 'q: 'e 的含义,就能把错误原因直接翻译成"SQL 字符串的所有权没有正确传递给 stream"。
4.9 Executor 实现者清单的可视化
把本章讨论的所有实现关系画一张图:
几个观察:
- 四种用户写法里,三种(
&mut conn/&mut *tx/&mut pool_conn)最终都落到驱动自己的 Executor impl——这是 DerefMut 的功劳。 - 没有一条线落到已删除的两个 impl——从用户视角看,这两个 impl 的缺席是隐形的,因为
&mut *tx直接到达&mut PgConnection。 &pool是唯一走 pool/executor.rs 的路径——Pool 自己实现 Executor 是在 sqlx-core,连接管理的逻辑在那里集中。
这张图总结了本章的拓扑——六种用户入口、六个 Executor impl、两个被删除的 impl 靠 DerefMut 绕过。
4.10 跨章关联:Executor 与 tower::Service
读完 Executor trait,值得把它和另一个 trait 家族放一起对照——tower 的 Service trait(《Hyper 与 Tower:工业级 HTTP 栈》第 2 章):
rust
// tower::Service
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, req: Request) -> Self::Future;
}
// sqlx::Executor(简化)
pub trait Executor<'c>: Send + Debug + Sized {
type Database: Database;
fn fetch_many<E: Execute<DB>>(self, query: E) -> BoxStream<...>;
fn fetch_optional<E: Execute<DB>>(self, query: E) -> BoxFuture<...>;
// 其他默认方法
}两者在设计意图上高度相似——都抽象"能处理某类请求的东西":
- tower 的 Service 抽象"能处理 HTTP 请求的东西"——HTTP handler、中间件、客户端 backends 都实现它。
- sqlx 的 Executor 抽象"能跑 SQL 查询的东西"——Pool、Connection 都实现它。
但设计上三处关键差异:
| 维度 | tower::Service | sqlx::Executor |
|---|---|---|
| 输入 | 任意 Request 类型参数 | Execute<'q, DB> trait bound |
| 输出 | 关联 Future | BoxFuture + BoxStream |
| 背压 | poll_ready 显式拉取 | 无(假设连接池提供背压) |
| 方法 | 两个(poll_ready + call) | 四个必实现 + 六个默认方法 |
最大的差异是 tower 的 poll_ready 显式背压——每次 call 前先 poll_ready 检查是否 ready,Poll::Pending 表示"我现在不处理新请求,请稍后"。sqlx 的 Executor 没有等价机制——背压由 Pool::acquire 的 Semaphore 隐式提供(第 14 章会看)。
这条差异的根因是工作负载模式不同:
- tower 的 Service 可能是一个下游 HTTP 后端,接一百个并发请求时需要显式告诉上游"慢一点"。背压是网络层面的必需品。
- sqlx 的 Executor 要么是 Pool(背压在 Semaphore 层),要么是 Connection(独占,天然串行)——没有中间的"还能接 N 个但超了要拒绝"的状态。
换句话说:Service 抽象分布式系统的节点、Executor 抽象本地数据库访问——设计的侧重点决定了 trait 的形态。这个对照不是说"哪个设计更好",而是让你看到 trait 设计随"场景约束"的变形。第 10 章 QueryBuilder 还会遇到类似对照——sqlx 的 QueryBuilder vs diesel 的 DSL vs SQL 原生字符串,同样是"场景约束"决定"抽象形态"。
4.10.1 一个 sqlx migrate run 的 fetch_many 走查
把上面讨论的所有机制合起来,看 sqlx migrate run 一次迁移执行时 Executor 链路长什么样。假设 migrations/20240424120000_add_index.sql 内容是:
sql
-- 20240424120000_add_index.sql
ALTER TABLE users ADD COLUMN last_seen TIMESTAMPTZ;
CREATE INDEX idx_users_last_seen ON users (last_seen);
UPDATE users SET last_seen = created_at WHERE last_seen IS NULL;sqlx-cli 的 migrate.rs 大致这样执行:
rust
let raw_sql = fs::read_to_string(path)?;
let mut tx = pool.begin().await?;
let results: Vec<AnyQueryResult> = (&mut *tx)
.execute_many(&*raw_sql)
.try_collect::<Vec<_>>()
.await?;
// 每个 AnyQueryResult 对应一条 ALTER/CREATE/UPDATE 的 rows_affected
tx.commit().await?;这条链路里触到本章每个概念:
&mut *tx—— DerefMut 到&mut AnyConnection(§4.4 提到 Any 也实现 Executor)execute_many—— 默认方法,内部调fetch_many过滤 Right(§4.3)fetch_many(&*raw_sql)——&str作为 Execute 实现(§4.5)- 服务端顺序执行三条语句,每条返回一个 Either::Left(QueryResult),Either::Right 流里没有(全是 DDL/UPDATE 不返回行)
try_collect把三个 QueryResult 收到Vec
如果其中一条失败(比如 CREATE INDEX 遇到已有同名索引),Either 流会 yield Err(Error::Database(...)),try_collect 立刻短路、剩下的 SQL 不执行。事务 drop 时 start_rollback 被触发(§第 1.6 节 Transaction Drop 讨论过)——整次迁移全部回滚。
这个场景是 execute_many 比 execute 有价值的地方:你能拿到每一条 DDL 的影响行数,用来给 DBA 看"ALTER 改了 0 行(预期)、UPDATE 改了 1.2 万行(和线上数据量一致)"。如果自己想做 "迁移预演" 工具—— execute_many + 事务 rollback 的组合能精确报告每条 SQL 的影响、而 schema 不落地。
4.10.2 实现 Executor 的三个易错点
如果你要给某个第三方数据库(例如 ClickHouse)写驱动,实现 Executor 的时候最容易踩三个坑:
坑 1:fetch_many 不应该 eagerly 拉取所有行。fetch_many 返回 Stream,语义是"按需拉取"——用户用 try_next() 逐个消费。如果你的实现内部 while let Some(row) = stream.next().await? { collected.push(row); } return collected;,相当于把异步流变成同步 Vec,失去了背压和早停能力。正确实现是 Box::pin(try_stream! { ... }),每拉到一行立即 r#yield!(Right(row))。
坑 2:Either::Left 和 Right 的顺序不能随便。fetch_one 依赖的语义是"Right 先于 Left"——数据行先出,最后一个 Either::Left 才标志查询完成。如果你的驱动颠倒顺序(先发 QueryResult 再发 rows),fetch_one 会收到一个 Left 后 try_filter_map 转 Ok(None),最终 fetch_optional 返回 None,用户看到 Error::RowNotFound ——诡异的 bug。
坑 3:prepare_with 返回的 Statement 生命周期必须和参数 sql 绑定。签名是 Statement<'q> where 'q 是 sql 的生命周期——你不能 Box::leak 或 clone 让它变 'static(除非你确定 Statement 内部拷贝了 sql 字符串)。否则上层代码的 &'q str 可能在 Statement 还存活时被 drop。
这三个坑大多数驱动作者第一次实现都会踩——sqlx-core 的集成测试里针对每条都有对应的测试用例。如果你要写新驱动,先读一遍 tests/any/ 目录下的测试,它们是 Executor 契约的活 spec。
4.11 本章小结
本章把 Executor trait 拆成了它的三条设计主干:
- 双原语
fetch_many+fetch_optional(§4.3)——前者用BoxStream<Either<QueryResult, Row>>表达"多条 SQL 的交替结果"、后者给驱动留 "最多一行" 的协议级优化口(Postgres Bind 的max_rows=1能省服务端内存)。高层方法execute/fetch/fetch_all基于fetch_many默认实现、fetch_one基于fetch_optional。这种"两个原语覆盖所有读路径"让驱动作者只需实现两条 + 两条 meta(prepare_with/describe)= 共 4 个必实现方法。 - 实现者极少(§4.4)——只有
&Pool<DB>和&mut DB::Connection(以及 Any、PgListener 两个特殊路径)。Pool::fetch_many内部pool.acquire().await?借连接、Connection 的 Executor impl 调用具体驱动的协议交互。 Execute<'q, DB>是 query shape 抽象(§4.5)——&str、(&str, Option<Arguments>)、Query<DB, A>各自实现。刻意不为String实现 是类型系统对 SQL 注入的间接防御(§4.5.1)。
两条关于 0.7→0.8 的破坏性变更:
&mut Transaction和&mut PoolConnection的 Executor impl 被删除(§4.6)——不只是"coherence 冲突"那么简单,是 **"trait solver overflow"(PoolConnection)**和 **"lack of lazy normalization"(Transaction)**两个 Rust 编译器的具体限制。workaround 是用户手写&mut *tx——把 Deref 展开的责任推给用户而不是编译器。- 设计教训:trait 家族的可组合性受 rustc 具体能力约束——白板上合理的设计可能落到编译器上不成立。sqlx 选择"诚实暴露限制、让用户写一次
*"而不是内部堆补丁。
两个配套的宏系统接口:
prepare/prepare_with(§4.7)——暴露 Statement 元数据给用户;Postgres 独享参数类型影响。describe(§4.7,#[doc(hidden)])——query!宏的私有通道,编译期拿Describe<DB>(columns / parameters / nullable)做类型推断。
一条 trait 级生命周期技巧:
'c: 'e, 'q: 'e(§4.8)——executor 和 query 都要至少活到 stream 结束,这是所有异步 trait 返回的普适生命周期约束。
下一章(第 5 章)我们进入类型映射三位一体——Encode / Decode / Type 三个 trait 如何共同表达"Rust 类型 ↔ 数据库类型"的双向映射,以及 Postgres 的 OID 查表、MySQL 的字符集、SQLite 的动态类型三家如何各自落地这套抽象。
4.12 对本章的设计审查
回头审视 Executor trait 的设计,从"如果你是代码评审员"视角做几点判断:
做得好的地方:
- 原语 fetch_many 足够小——一个流原语表达了所有读路径,驱动作者只需正确实现一个方法(加 fetch_optional 这个优化口),其他七个自动获得。
- Either 类型选择恰当——不用自己的枚举、直接用生态里成熟的
either::Either,避免了重复的 From/TryFrom 样板。这也和Statement::parameters返回Either<&[TypeInfo], usize>形成风格一致。 - 不为 String 实现 Execute——这种"类型系统级的拒绝"传递的是 API 文档写十遍都传不出的设计意图:注入风险代码应当显眼。
describe被#[doc(hidden)]——清晰地把"用户 API"和"宏内部 API"分开,文档站不出现 describe,但 sqlx-cli 和 sqlx-macros-core 能用。
有争议的地方:
BoxStream/BoxFuture的堆分配——每次.fetch_one(...)都要Box::pin一次,对微基准测试可观测。0.9.0-alpha 试图改成impl Stream + Send消除这一层,但还没稳定。where 'c: 'e这种生命周期约束——对新手友好度不高;错误消息拉长。但替代方案(用 GAT 声明返回类型)会让 trait 更难实现。&mut *tx的手动 DerefMut——这是 §4.6 讨论过的 Rust 编译器限制的妥协。每个 sqlx 用户都要学一次,文档里也要反复解释。
不该做的地方(反面素材):
- 如果 sqlx 给 Pool 和每个 Connection 类型都单独定义一套
fetch_one/fetch_all方法(而不用 Executor trait),代码会重复 N 遍且每次驱动增加方法要同时改 Pool 侧——这就是没有 trait 时的世界。 - 如果把 fetch_many 的 Either 流改成 "先返回
Vec<Row>再返回QueryResult" 两段式 API,用户拿不到交替顺序,sqlx migrate run这种"按 DDL 顺序执行并收集"的场景就无法实现。
这些判断不是"绝对标准答案"——它们是把本章的具体设计细节提升到"Rust trait 家族设计通用原则"层面的一次归纳。理解了这些,你在自己的项目里写 trait 的时候能更自觉地问"我这里到底是 impl 的负担 vs 用户的负担、我的 trait 对新手友好吗、哪些是编译器限制而不是我的选择"——这才是"读源码学设计"的真正价值。