TWiR 译文辑录

在线版本见: https://twir.han.rs.

Current commit Current commit date

版权说明

英文原文版权由原作者所有, 中文翻译版权遵照 CC BY-NC-SA 4.0 协议开放. 如原作者有异议请邮箱联系.

Rust 语言术语中英文对照表

English 英文Simplified Chinese 简体中文Traditional Chinese 繁體中文Note 备注
A
Abstract Syntax Tree抽象语法树抽象語法樹
ABI应用程序二进制接口應用程式二進位介面Application Binary Interface 缩写
accumulator累加器累加器
accumulator variable累加器变量累加器變數
address (n.)地址位址
address (v.)寻址定址
ahead-of-time compiled预编译預編譯
ahead-of-time compiled language预编译语言預編譯語言
algebraic data types(ADT)代数数据类型代數資料型別
alias别名別名
aliasing别名使用別名使用参见 Wikipedia
angle brackets尖括号,“<”和“>”尖括號,“<”和“>”
annotate标注,注明,标记,标识(动词)標註,註明,標記,標示(動詞)
annotation标注,注明,标记,标识(名词)標註,註明,標記,標示(名詞)
ARC原子引用计数器最小參考計數器Atomic Reference Counter
anonymity匿名匿名
argument参数,实参,实际参数引數,實參,實際參數,實際引數不严格区分的话, argument(参数)和
parameter(参量)可以互换地使用
argument type参数类型引數型別
array数组陣列
assembler汇编器組譯器
assignment赋值賦值
associated functions关联函数關聯函式
associated items关联项關聯項目
associated types关联类型關聯型別
asterisk星号(*)星號(*)
atomic原子的最小的
atomic operation原子操作最小操作
attribute属性屬性
automated building自动构建自動建構
automated test自动测试,自动化测试自動測試,自動化測試
B
benchmark基准基準
binary二进制的二進位的
binary executable二进制的可执行文件二進位的執行檔
bind绑定綁定
bit位元
block语句块,代码块區塊,程式碼區塊
boolean布尔型,布尔值布林型,布林值
borrow check借用检查借用檢查
borrower借用者,借入者借用者,借入者
borrowing借用借用
bound约束,限定,限制約束,限定,限制此词和 constraint 意思相近,
constraint 在 C# 语言中翻译成“约束”
box箱子,盒子,装箱类型盒子,盒子,裝箱型別一般不译,作动词时翻译成“装箱”,
具有所有权的智能指针
boxed装箱,装包裝箱,裝包
boxing装箱,装包裝箱,裝包
brace大括号,“{”或“}”大括號,“{”或“}”
buffer缓冲,缓冲区,缓冲器缓衝,緩衝區,緩衝器
build构建建構
builder pattern创建者模式建造者模式
byte字节位元組
C
call调用呼叫
caller调用者呼叫者
capacity容量容量
capture捕获捕獲
cargo(Rust 包管理器,不译)(Rust 專案管理器,不譯)该词作名词时意思是“货物”,
作动词时意思是“装载货物”
cargo-fyCargo 化,使用 Cargo 创建项目Cargo 化,使用 Cargo 建立專案
case analysis事例分析情況分析
cast类型转换,转型型態轉換,轉型
casting类型转换型態轉換
chaining method call链式方法调用連鎖方法呼叫
channel信道,通道通道,信道
char字符字元作关键字时不译
character字符字元
closure闭包閉包
coercion强制类型转换,强制转换強制型別轉換,強制轉換coercion 原意是“强制,胁迫”
collection集合集合参见 Wikipedia
combinator组合算子,组合器組合子,組合器
comma逗号,“,”逗號,“,”
command命令指令
command line命令行指令列
comment注释註解
compile编译(动词)編譯(動詞)
compile time编译期,编译期间,编译时編譯期,編譯期間,編譯時
compilation编译(名词)編譯(名詞)
compilation unit编译单元編譯單元
compiler编译器編譯器
compiler intrinsics编译器固有功能編譯器內建功能
compound复合(类型,数据)複合(型別,資料)
concurrency并发並行
conditional compilation条件编译條件編譯
configuration配置設定
const常数,常量常數,常量作关键字时不译
constant常数,常量常數,常量
constructor构造器建構子
consumer消费者使用者
container容器容器
container type容器类型容器型別
convert转换,转化,转轉換,轉化,轉
copy复制,拷贝複製,拷貝
crate包,包装箱,装包專案,套件,包裝箱一般不译,crate 是 Rust 的基本编译单元
curly braces大括号,包含“{”和“}”大括號,包含“{”和“}”
custom type自定义类型自訂型別
Coherence连贯性理由是:实际上这个 Coherence 这个词在其相关 RFC 2451 中上下文语境中的意思是,让编译器推理特质更加流畅更加连贯,不要出错。所以“连贯性”更符合这个词在 Rust 中的本意。
D
dangling pointer悬垂指针懸空指標,迷途指標use after free 在释放后使用
data race数据竞争資料競爭
dead code死代码,无效代码,不可达代码死代碼,無效代碼,不可達代碼
deallocate释放,重新分配釋放,重分配
declarative macro声明宏陳述式巨集参见 Rust 程序设计语言
declare声明宣告
deep copy深拷贝,深复制深複製,深度複製
dependency依赖相依項,依賴
deque双端队列雙端佇列Double-ended queue 的缩写
deref coercion解引用强制转换解參考強制轉換
dereference解引用解參考Rust 文章中有时简写为 Deref
derive派生派生
designator指示符指示符
destruction销毁,毁灭毀滅,銷毀
destructor析构器,析构函数解構子,解構函式
destructure解构解構
destructuring解构,解构赋值解構,解構賦值
desugar脱糖去糖
debug调试除錯、偵錯
debugger调试器除錯器、偵錯器
device drive设备驱动設備驅動程式
directory目录目錄
disambiguate(泛型)消歧?
dispatch分发分派
diverge function发散函数發散函式
diverging functions发散函数發散函式
documentation文档文件
dot operator点运算符點運算子
DST动态大小类型動態大小型別dynamic sized type,一般不译,
使用英文缩写形式
dynamic language动态类型语言動態型別語言
dynamic trait type动态特质类型動態特質型別
E
enumeration枚举列舉
encapsulation封装封裝
equality test相等测试相等測試
elision省略省略
exhaustiveness checking穷尽性检查,无遗漏检查穷盡性檢查,無遺漏檢查
expression表达式表達式
expression-oriented language面向表达式的语言面向表達式的語言
explicit显式明確
explicit discriminator显式的辨别值明確的辨別值
explicit type conversion显式类型转换明確型別轉換
extension扩展名副檔名
extern外,外部外,外部作关键字时不译
F
fat pointer胖指针胖指標
feature gate功能开关功能開關
field字段欄位
field-level mutability字段级别可变性欄位級別可變性
file文件檔案
fmt格式化,是 format 的缩写格式化,是 format 的縮寫
formatter格式化程序,格式化工具,格式器格式化程式,格式化工具,格式器
floating-point number浮点数浮點數
flow control流程控制流程控制
Foreign Function Interface(FFI)外部语言函数接口外部語言函式介面
fragment specifier片段分类符片段分類符
free variable自由变量自由變數
freeze冻结冷凍
function函数函式
function declaration函数声明函式宣告
functional函数式函數式
G
garbage collector垃圾回收垃圾回收
generalize泛化,泛型化泛化,泛型化
generator生成器產生器
generic泛型泛型
generic type泛型类型泛型型別
global variable全局变量全域變數
growable可增长的可增長的
guard守卫守護
H
handle error句柄错误處理錯誤
hash哈希,哈希值,散列雜湊,雜湊值,散列
hash map散列映射,哈希表雜湊映射,雜湊表
heap堆積
hierarchy层次,分层,层次结构層次,分層,層次結構
higher rank lifetime高阶生命周期高階生命週期
higher rank trait bound高阶特质约束高階特質約束
higher rank type高阶类型高階型別
hygiene卫生衛生
hygienic macro system卫生宏系统衛生巨集系統
I
ICE编译内部错误編譯器內部錯誤internal compiler error 的缩写
immutable不可变的不可變的
implement实现實作
implementor实现者實作者
implicit隐式隱含
implicit discriminator隐式的辨别值隱含的辨別值
implicit type conversion隐式类型转换隱含型別轉換
import导入匯入
in assignment在赋值(语句)在賦值(語句)
indent缩进縮排,定位點
index索引索引英语复数形式:indices
infer推导(动词)推論(動詞)
inference推导(名词)推論(名詞)
inherited mutability承袭可变性承襲可變性
inheritance继承繼承
integrated development
environment(IDE)
集成开发环境整合開發環境中文著作中通常直接写成 IDE
integration-style test集成测试集成測試
interior mutability内部可变性內部可變性
installer安装程序,安装器安裝程式,安裝器
instance实例實例
instance method实例方法實例方法
integer整型,整数整型,整數
interact相互作用,相互影响相互作用,相互影響
interior mutability内部可变性內部可變性
intrinsic固有的固有的
invariant不变的(与协变、逆变并列);
保证(陈述一个应当被保持的条件)
不變的(與協變、逆變並列);
保證(陳述一個應當被保持的條件)
invoke调用呼叫
item项,条目,项目項,條目,項目
iterate重复重複
iteration迭代疊代
iterator迭代器疊代器
iterator adaptors迭代器适配器疊代器配接器
iterator invalidation迭代器失效疊代器無效
L
LHS左操作数左操作數left-hand side 的非正式缩写,
与 RHS 相对
lender借出者借出者
library函式庫,程式庫
lifetime生存时间,寿命,生命周期生存時間,壽命,生命週期
lifetime elision生命周期省略生命週期省略
link链接連結,鏈結
linked-list链表鏈結串列
linker链接器鏈結器,連結器
lint(不译)(不譯)lint 英文本义是“纱布,绒毛”,此词在
计算机领域中表示程序代码中可疑和
不具结构性的片段,参见 Wikipedia
list列表清單
listener监听器監聽器
literal数据,常量数据,字面值,字面量,
字面常量,字面上的
資料,常量資料,字面值,字面量,
字面常量,字面上的
英文意思:字面意义的(内容)
LLVM(不译)(不譯)Low Level Virtual Machine 的缩写,
是构建编译器的系统
loop循环迴圈作关键字时不译
low-level code底层代码底層代碼
low-level language底层语言底層語言
l-value左值左值
M
main functionmain 函数,主函数main 函式,主函式
macro巨集
map映射映射一般不译
match guard匹配守卫匹配守護
memory内存記憶體
memory address内存地址記憶體位址
memory leak内存泄露記憶體流失
memory safe内存安全記憶體安全
meta原则,元後設
metadata元数据後設資料,詮釋資料
metaprogramming元编程元程式設計,超程式設計,後設程式設計
metavariable元变量後設變數
method call syntax方法调用语法方法呼叫語法
method chaining方法链方法鏈
method definition方法定义方法定義
mock object模拟对象模擬物件
modifier修饰符修飾符
module模块模組
monomorphization单态單型mono: one, morph: form
move移动,转移移動,轉移按照 Rust 所规定的内容,
英语单词 transfer 的意思
比 move 更贴合实际描述
参考:Rust by Example
move semantics移动语义移動語義
mutability可变性可變性
mutable可变可變
mutable reference可变引用可變參考,可變參照
multiple bounds多重约束多重約束
mutiple patterns多重模式多重模式
N
namespace命名空间命名空間
nest嵌套嵌套
Nightly RustRust 开发版Rust 開發版nightly本意是“每夜,每天晚上”,
指代码每天都更新
NLL非词法生命周期非詞法生命週期non lexical lifetime 的缩写,
一般不译
non-copy type非复制类型非複製型別
non-generic非泛型非泛型
no-op空操作,空运算空操作,空運算(此词出现在类型转换章节中)
non-commutative非交换的非交換的
non-scalar cast非标量转换非標量轉換
notation符号,记号符號,記號
numeric数值,数字數值,數字
O
object对象物件
object code目标代码目的碼
object file目标文件目的檔
object-oriented programming面向对象程序设计物件導向程式設計常缩写成 OOP
optimization优化最佳化
out-of-bounds accessing越界访问越界存取
orphan rule孤儿规则孤兒規則
overflow(向上)溢出,(向上)越界(向上)溢出,(向上)越界
own占有,拥有占有,擁有
owner所有者,拥有者所有者,擁有者
ownership所有权所有權
P
package manager包管理器,软件包管理器套件管理員,軟體包管理器
panic(不译)(不譯)此单词直接翻译是“恐慌”,
在 Rust 中用于不可恢复的错误处理
parameter参量,参数,形参,形式参量(数)參數,形參,形式參數,形式引數不严格区分的话, argument(参数)和
parameter(参量)可以互换地使用
parametric polymorphism参数多态參數多型
parent scope父级作用域父級作用域
parentheses小括号,包括“(”和“)”小括號,包括“(”和“)”
parse分析,解析分析,解析
parser(语法)分析器,解析器(語法)分析器,解析器
pattern模式模式
pattern match模式匹配模式匹配
phantom type虚类型,虚位类型虛擬型別,虛位型別phantom 相关的专有名词:
phantom bug 幻影指令
phantom power 幻象电源
参见:HaskellHaskell/Phantom_type
Rust/Phantomstdlib/PhantomData
platform平台平台
polymorphism多态多型
powershell(不译)(不譯)Windows 系统的一种命令行外壳程序
和脚本环境
possibility of absence不存在的可能性不存在的可能性
precede预先?,在…发生(或出现)在…之前
prelude(不译)(不譯)预先导入模块,英文本意:序曲,前奏
primitive types原生类型,基本类型,简单类型原始型別,基本型別,簡單型別
print打印列印
process进程處理程序,行程
procedural macros过程宏,程序宏过程巨集,程序巨集
project项目,工程專案,工程
prototype原型原型
R
race condition竞态条件競態條件
RAII资源获取即初始化(一般不译)資源獲取即初始化(一般不譯)resource acquisition is initialization 的缩写
range区间,范围區間,範圍
range expression区间表达式區間表達式
raw identifier原始标识符原始識別符
raw pointer原始指针,裸指针原始指標,裸指標
RC引用计数參考計數,參照計數reference counted
Reader读取器讀取器
recursive macro递归宏遞迴巨集
reference引用參考,參照
reference cycle引用循环參考循環,參照循環
release发布發佈
resource资源資源
resource leak资源泄露資源漏失
return返回回傳,傳回
return value返回值回傳值,傳回值
RHS右操作数右操作數right-hand side 的非正式缩写,
与 LHS 相对
root directory根目录根目錄
runtime运行时執行時期
runtime behavior运行时行为執行時行為
runtime overhead运行时开销執行時開銷
Rust(不译)(不譯)一种编程语言
Rustacean(不译)(不譯)编写 Rust 的程序员或爱好者的通称
rustc(不译)(不譯)Rust 语言编译器
r-value右值右值
S
saturate截断至边界
scalar标量,数量純量,數量
schedule调度排程
scope作用域作用域
screen屏幕螢幕
script脚本腳本
segmenatation fault存储器段错误,存储器区段错误記憶體段錯誤,記憶體區段錯誤
semicolon分号,“;”分號,“;”
self自身,作关键字时不译自身,作關鍵字時不譯
shadow遮蔽,隐蔽,隐藏,覆盖遮蔽,隱蔽,隱藏,覆蓋
shallow copy浅拷贝,浅复制浅複製,淺拷貝
signature标记簽名,簽章
slice切片切片
smart pointer智能指针智慧指標
snake case蛇形命名蛇形命名参见:Snake case
sound可靠可靠
soundness可靠性可靠性参见:Soundness
source file源文件源檔案
source code源代码原始碼
specialization泛型特化泛型特化
square平方,二次方,二次幂平方,二次方,二次冪
square brackets中括号,“[”和“]”方括號,“[”和“]”
src(不译)(不譯)source 的缩写,指源代码
stack堆疊
stack unwind栈解开、栈展开堆疊展開,堆疊回溯
statement语句陳述
statically allocated静态分配靜態分配
statically allocated string静态分配的字符串靜態分配的字串
statically dispatch静态分发靜態派發
static method静态方法靜態方法
string字符串字串
string literal字符串常量字串常數
string slice字符串切片字串切片
stringify字符串化字串化
subscript notation下标次序,下標表示法
sugar
super父级,作关键字时不译親類,作關鍵字時不譯
syntax context语法上下文語法上下文
systems programming language系统级编程语言系統程式設計語言
T
tagged union标记联合標記聯集
target triple多层次指标,三层/重 指标/目标三元目標描述triple 本义是“三”,但此处虚指“多”,
此词翻译需要更多讨论
terminal终端終端機
testing测试測試
testsuit测试套件測試套件
test double测试替代測試替身
the least significant bit (LSB)最低有效位最低有效位
the most significant bit (MSB)最高有效位最高有效位
thread线程執行緒
TOML(不译)(不譯)Tom’s Obvious, Minimal Language
的缩写,一种配置语言
token tree令牌树標記樹待进一步斟酌
trait特质特質其字面上有“特性,特征”之意
trait bound特质约束特質約束bound 有“约束,限制,限定”之意
trait object特质对象特質物件
transmute(不译)(不譯)其字面上有“变化,变形,变异”之意,
不作翻译
trivial平凡的平凡的
troubleshooting疑难解答,故障诊断,
故障排除,故障分析
問題排除,故障診斷,
故障處理,故障分析
tuple元组元組
two’s complement补码,二补数二補數,二補碼
two-word object双字对象雙字物件
type annotation类型标注,类型注明/标记/标识型別註解,型別標示/標註/標識
type erasure类型擦除型別擦除
type inference类型推导型別推論
type inference engine类型推导引擎型別推論引擎
type parameter类型参量型別參數
type placeholder类型占位符型別預留位置
type signature类型标记型別標誌
U
undefined behavior未定义行为未定義行為
underflow(向下)溢出,(向下)越界(向下)溢出,(向下)越界
uninstall卸载卸載
unit-like struct类单元结构体類單元結構體
unit struct单元结构体單元結構體
“unit-style” tests单元测试單元測試
unit test单元测试單元測試
unit type单元类型單元型別
universal function call syntax
(UFCS)
通用函数调用语法通用函式呼叫語法
unsized types不定长类型不定長型別
unwind展开展開
unwrap解包解包
V
variable变量變數
variable binding变量绑定變數綁定
variable shadowing变量遮蔽,变量隐蔽,
变量隐藏,变量覆盖
變數遮蔽,變數隱蔽,
變數隱藏,變數覆蓋
variable capture变量捕获變數捕獲
variant可变类型,变体變体型,變體
vector(动态数组,一般不译)(動態陣列,一般不譯)vector 本义是“向量”
visibility可见性可見性
vtable虚表虛表
W
where clausewhere 子句,where 从句,where 分句where 子句,where 從句,where 分句在数据库的官方手册中多翻译成“子句”,英语语法中翻译成“从句”
wildcard通配符萬用字元
wrap包裹, (数字溢出时)回绕包裹
wrapped装包裝包
wrapper装包裝飾器,包裹器
Y
yield产生(收益、效益等),产出,提供產生(收益、效益等),產出,提供
Z
zero-cost abstractions零开销抽象零開銷抽象
zero-width space(ZWSP)零宽空格零寬空格

This Week in Rust (TWiR) Rust 语言周刊中文翻译计划, 第 579 期

本文翻译自 Barrett Ray 的博客文章 https://barretts.club/posts/rust_review_2024/, 已获许可, 英文原文版权由原作者所有, 中文翻译版权遵照 CC BY-NC-SA 协议开放.

相关术语翻译依照 Rust 语言术语中英文对照表.

囿于译者自身水平, 译文虽已力求准确, 但仍可能词不达意, 欢迎批评指正.

2024 年 12 月 30 日凌晨, 于北京.

GitHub last commit

A Review of Rust in 2024: What Next?

Rust 2024: 总结与展望

Rust is a programming language with a highly active community. Contributors are constantly adding new features and working toward new goals. This article summarizes my favorite features added in 2024, and also addresses my hopes for the future!

Rust 是一门有着高度活跃的社区的编程语言, 无数贡献者正持续为其添加新功能、达成新目标. 本文总结了 2024 年添加到 Rust 的我最喜欢的语言特性, 同时也展望了我未来的期许.

If you’re here to see me complain about what we don’t have yet, please head to the Wishlist for 2025 section.

如果您只是想听听我对 Rust 尚未有的特性的 “抱怨”, 请移步 Wishlist for 2025.

Table of contents

Review of 2024 | 2024 年度回顾

The Rust project has made countless improvements to the language this year. Let’s review and see what might come next!

Rust 项目今年对该语言有无数改进. 首先让我们回顾一下.

&raw Reference Syntax | &raw 原始引用语法

We now support creating &raw const and &raw mut references as distinct types. These let you safely refer to fields without a well-defined alignment, much like the long-time workarounds (addr_of! and addr_of_mut! macros) did:

我们现在支持将 &raw const&raw mut 引用创建为不同的类型. 这些可以让您安全地引用没有明确定义对齐的字段, 就像以往的解决方法(addr_of!addr_of_mut! 宏)所做的那样:

(译者注: 详见 https://blog.rust-lang.org/2024/10/17/Rust-1.82.0.html)

#![allow(unused)]
fn main() {
/// These fields will be "packed", so there won't be extra padding.
/// 
/// 这些字段将是 "packed" 的, 没有额外的填充.
///
/// This can reduce memory usage, but screws with everything else. It's
/// sometimes used in low-level contexts.
///
/// 这种做法有助于减少内存占用, 但在其他方面都不尽人意, (仅)有时应用于底层代码.
///
/// See: https://doc.rust-lang.org/nomicon/other-reprs.html#reprpacked
#[repr(packed)]
struct MyPackedStruct {
    field_a: i32,
    field_b: i8,
    field_c: u16,
}

let mps: MyPackedStruct = MyPackedStruct {
    field_a: 582,
    field_b: -4,
    field_c: 989,
};

// scary: this will probably cause undefined behavior (UB)!
//
// 可怕: 这可能导致未定义行为(undefined behavior, UB)!
//
// the compiler now gives you an error here.
//
// 编译器会在这里报告错误.
let bad: *const i32 = &mps.field_a as *const i32;

// happy: no problems here.
//
// 高兴: 这里没问题
let good: *const i32 = &raw const mps.field_a;

// we'll want to read the value out using this method.
//
// 我们将使用此方法读取其值.
//
// note: it's only unsafe because `read_unaligned` doesn't care if the
// type is `Copy`. so you can do some nonsense with it
// ...kinda like `core::mem::replace`
//
// 注意: 这里之所以 unsafe, 是因为 `read_unaligned` 不关心类型是否实现 `Copy`, 所以您能做一些
// 没什么实际意义的事情, 例如 `core::mem::replace`.
//
// (译者注: 参见 https://doc.rust-lang.org/std/ptr/index.html#safety)
let value: i32 = unsafe { good.read_unaligned() }; // this is how you'd read the value
}

Again, though, avoid Packed representations if you can. They’re a bit of a footgun. If you do need to use them, though, &raw is vital!

再一次指出, 应尽量避免 Packed. 它们有点像七伤拳(译者注: footgun, 伤自己的脚的枪). 但是, 如果您确实需要使用它们, 那么 &raw 就至关重要!

At first, their usage might seem unclear. What is the difference if the syntax just spits out a *const or *mut… just like as casting would? In short, Rust usually requires you to first create a reference (&MyType) before you can cast to a raw pointer (*const MyType and *mut MyType).

它们(原始引用)的用法可能看起来不甚明了. 如果只是个简单获得 *const*mut… 的语法, 就像 as 转换一样, 那有什么区别? 简而言之, Rust 通常要求您首先创建一个引用 (&MyType), 然后才能转换为原始指针(*const MyType*mut MyType).

However, Rust’s references have certain guarantees that raw references don’t have. In particular, they must be both aligned and dereferenceable.1 When these aren’t true, you immediately create opportunities for undefined behavior (UB) by even compiling the thing. Miscompilations are likely due to LLVM’s reliance on those two invariants.

然而, Rust 意义下的引用具有原始引用所没有的某些保证(invariant), 特别是, 它们必须既对齐可被取消引用1. 如果这些保证不成立, 虽然可以通过编译, 但可能存在未定义行为(UB). 错误的编译结果可能是由于 LLVM 默认已满足这两个保证.

Raw reference (&raw) syntax addresses these problems by telling LLVM that those invariants might not be true. Certain optimizations (and other reliant invariants) are now turned off or adjusted.

原始引用(&raw)语法通过告诉 LLVM 这些保证可能不满足来解决这些问题. 某些优化(以及其他一些相关的保证)将被关闭或调整.

Floating-Point Types in const fn | const fn 中的浮点类型

In the past, you may have tried to use floating-point (FP) numbers within const functions. However, before Rust 1.82, the compiler would stop you. This limitation stemmed from platform differences in FP numbers.

过去, 您可能尝试过在 const 函数中使用浮点(FP)数. 然而, 在 Rust 1.82 之前, 编译器会阻止您. 这种限制源于浮点数的平台差异.

To understand why, you need to know a bit of context. In Rust, const refers to more than something that won’t change - it’s a block that can be computed at compile-time! This system spares many runtime operations, making programs faster. However, since FP numbers have platform differences, it’s harder to compute that stuff at compile-time. If you do, your program’s behavior will change depending on what machine compiled it, even if the cross-compilation is expected to be deterministic!

要理解原因, 您需要了解一些背景知识. 在 Rust 中, const 不仅仅指的是不会改变的东西——它是一个可以在编译时计算的块! 这节省了许多运行时开销, 使程序更快. 然而, 由于浮点数存在平台差异, 因此在编译时计算这些内容会更加困难, 如果那么干, 程序的行为将根据编译它的机器而改变, 但是如交叉编译, 其结果应当是不受编译机器条件变化而改变的!

There’s also another problem. If you want to avoid those cross-compilation flaws, you have to write rules for floats to follow at compile-time. They should be very close to runtime behavior, and ideally, exactly the same. Notably, Go fell into this trap, causing major differences in behavior depending on when floats are evaluated. Every time you use floats in Go, you have to ensure all your code agrees.

还有另一个问题. 如果您想避免这些交叉编译中可能出现的问题, 就必须指出要在编译时遵循的浮点数规则, 它们应该非常接近运行时行为, 并且理想情况下应当完全相同. 值得注意的是, Go 陷入了这个陷阱, 导致行为发生重大差异, 具体取决于浮点数计算是编译时抑或运行时. 每次在 Go 中使用浮点数时, 都必须确保所有代码都一致(译者注: 即要么都 0.1 + 0.2 == 0.3, 要么都 a := 0.1, b := 0.2a + b, 前者使用常量, 编译时计算使用精确算法, 后者使用变量, 运行时计算, 结果损失精度为 0.30000000000000004. 参见博文.).

With these requirements in mind, and a lot of hard work, Rust has introduced floats in const fn! It uses many custom rules to specify exactly how they should work. These are given in RFC 3514: Float Semantics, which specifies how floating-point numbers should work in the language.

考虑到这些要求, 并经过大量的努力, Rust 在 const fn 中引入了浮点数! 它使用许多自定义规则来准确指定它们应该如何工作. RFC 3514: 浮点语义 中给出了这些内容, 它指定了浮点数在该语言中的工作方式.

#![allow(unused)]
fn main() {
struct Maybe {
    pub float: f32,
}

/// As you can see, we're allowed to use floats in `const`!
///
/// 如您所见, 我们现在允许在 `const` 中使用浮点数!
const fn float_in_const(call_me: &Maybe) -> (bool, f32) {
    let f: f32 = call_me.float; // also in your data structures :)

    let new = f / 1.1;
    (new.is_finite(), new)
}
}

Note that most methods on the f32/f64 primitives don’t yet use this. For example, f32::powf and f32::powi aren’t yet const. Using #![feature(const_float_methods)] on Nightly can get you some of the way there, though these power functions don’t seem to be included yet.

请注意, f32/f64 基本类型上的大多数方法尚未(在 stable 版本中)提供 const, 例如 f32::powff32::powi. 在 Nightly 上使用 #![feature(const_float_methods)] 可启用将部分方法标识为 const, 虽然例如前面那两个强大的方法似乎尚未包含在内.

#[expect(lint)]

These attributes are just like #[allow(lint)], but they also give an error when the “expectation” isn’t satisfied.

类似 #[allow(lint)], 但当不满足 “期望”(expectation) 时也会给出错误.

For example, if you put #[allow(unused)] onto a function, but later start calling it somewhere, you typically wouldn’t notice the change. You may forget the function is used in your API. The #[expect] attribute doesn’t let this happen - it’ll show an error if you violate its expectation.

例如, 如果您标识一个未使用的函数 #[allow(unused)], 但后来开始在某个地方调用它. 您通常不会注意到这种变化, 您可能会忘记您的 API 中使用了该函数, 使用 #[expect] 就不会让这种情况发生: 如果您违反了 “期望”, 它就会明确给出错误.

#![allow(unused)]
fn main() {
// you can just replace `#[allow(lint)]` with `#[expect(lint)]`
//
// 您可以简单地将 `#[allow(lint)]` 换成 `#[expect(lint)]`
//
// #[allow(unused)]
#[expect(unused)]
type SomeUnusedItem = i32;
}

This has already fixed some bugs in my code, so I wholeheartedly suggest giving it a try!

这已经修复了我的代码中的一些错误, 所以我衷心建议尝试一下!

(译者注: 搜了一下, 如 https://github.com/onkoe/liboptic/blob/c3a4ea057315797cc9518d652533434fb00a6aae/edid/src/structures/desc/display_range_limits.rs#L154)

core::error::Error Trait Stabilization (error in core) | core::error::Error 特质已稳定 (error in core)

If you’ve been in the embedded trenches before 1.81, you’ve seen Issue #103765: Tracking Issue for Error in core.

如果您在 Rust 1.81 之前涉足过嵌入式领域, 那么您已经看到过 Issue #103765: Tracking Issue for Error in core

Everyone and their mother was using the (now defunct) #![feature(error_in_core)] attribute on their crate - and they all had to use Nightly to boot.

为此, 每个人和上游都不得不在他们的 crate 里使用 #![feature(error_in_core)] 属性(现已不复存在): 这需要 Nightly Rust.

This is no longer a problem! anyhow, thiserror, and my rip-off crate, pisserror all support embedded usage of Error now, at least through no_std! Note that anyhow still requires some form of allocator.

自此以后, 这不再是问题! anyhow, thiserror, 以及我的 “盗版” crate, pisserror, 都已经支持嵌入式领域在 no_std 下使用 Error 特质! 请注意, anyhow 仍然需要某种形式的分配器(allocator).

Anyways… I feel like framing this link on my wall. https://doc.rust-lang.org/stable/core/error/index.html

不管怎样… 我想把这个链接挂在这 https://doc.rust-lang.org/stable/core/error/index.html.

LazyCell and LazyLock

These two types are upstreamed from the well-known once_cell crate, but the standard library is finally catching up!

这两种类型来自著名的 once_cell crate , 标准库终于迎头赶上了!

LazyCell is the standard library’s version of the once_cell::unsync::Lazy type. It can’t be used across threads or in statics, but it’s made for something else: initializing a variable only when it’s needed! They’re typically used when you need to run a large computation once, then use the cached results.

LazyCellonce_cell::unsync::Lazy 类型的标准库版本. 它不能跨线程或在静态中使用, 但它是为其他目的而设计的: 仅在需要时初始化变量! 当您需要运行一次大型计算, 然后使用缓存的结果时, 通常会使用它们.

In comparison to OnceCell, LazyCell is used when the computation is always the same. You can only specify the “creation function” in the constructor.

OnceCell 相比, 当计算结果始终相同时使用 LazyCell. 您只能在构造函数中指定 “创建函数”.

#![allow(unused)]
fn main() {
/// A huge type that we need for our app!
///
/// 我们的 APP 需要的一个巨大的类型!
struct BigType {
    creation_time: Instant,
    // lots of other fields...
    // 许多别的字段
}

impl BigType {
    /// pretend this takes forever. we'll use `sleep` to get the point across :)
    ///
    /// 假设需要很长时间, 我们使用 `sleep` 模拟这点.
    fn new(creation_time: Instant) -> Self {
        std::thread::sleep(Duration::from_millis(500));
        Self { creation_time }
    }
}

/// A type that needs to provide a cached value to callers.
///
/// 需要向调用者提供缓存值的类型.
struct SomethingWithCache {
    cache: LazyCell<BigType>,
}

impl SomethingWithCache {
    pub fn new() -> Self {
        Self {
            cache: LazyCell::new(|| BigType::new(Instant::now())),
        }
    }

    fn big_type(&self) -> &BigType {
        // this deref will initialize the type if not done already!
        //
        // 这里解引用在底层数据没有初始化时会触发初始化.
        //
        // otherwise, we'll just use the cached value...
        //
        // 否则, 直接用缓存了的数据.
        &*self.cache
    }
}
}

On the other hand, LazyLock (once_cell::sync::Lazy) is often used on servers and in other high-performance scenarios. They work with concurrency and threading, and you’ll also tend to find them inside static variables. These are a bit slower than LazyCell, but offer greater flexibility.

另一方面, LazyLock (once_cell::sync::Lazy) 经常用于服务器和其他高性能场景, 允许多线程并发访问, 常见于静态变量. 比 LazyCell 慢一些, 但提供了更大的灵活性.

(译者注: 个人最爱这个, 结合 dashmap 当高性能无锁 kv 缓存. LazyLock 区分于 LazyCell 就是前者是后者的线程安全版本.)

#![allow(unused)]
fn main() {
/// Here's a static, which is accessible throughout the program.
///
/// Let's pretend that creating it takes a looooong time...
static BIG_SCARY_VARIABLE: LazyLock<BigType> = LazyLock::new(|| BigType::new(Instant::now()));

struct BigType {
    creation_time: Instant,
}

impl BigType {
    fn new(creation_time: Instant) -> Self { /* ... */ }
}
}

By the way, you may have noticed that you don’t need to have any mutability to initialize these types. You can mutate them from behind a shared reference, as they use unsafe behind the scenes to mutate themselves.

顺便说一句, 您可能已经注意到, 初始化这些类型不需要任何可变性. 您可以从共享引用后面改变它们, 因为它们在幕后使用 unsafe 来改变自己.

When it lands on Stable, the lazy_get Nightly feature will also allow you to replace the Lazy types’ internal values with your own.

当它登陆稳定版时, lazy_get 这个 Nightly feature 还允许您用自己的内部值替换 Lazy 类型的内部值.

(译者注: 我没留意到这个欸, 超实用的 feature)

Anyways, these types have always been around in one way or another. But now, you don’t need to use an external crate!

不管怎么说, 这些类型一直以这样或那样的方式存在, 但现在, 您不需要使用第三方的 crate 了!

The #[diagnostic::on_unimplemented] Attribute

This simple attribute is extremely influential - it lets you create your own compile errors for the user to see, all without a proc macro! Here’s how it works:

这个简单的属性(attribute)非常有影响力: 它允许您创建自己的编译错误供用户查看, 所有这些都无需 proc 宏! 以下给出一个例子:

#[diagnostic::on_unimplemented(
    message = "tell the user what's going on",
    label = "oh hey im pointing at the failed code",
    note = "You may wish to add `#[derive(Cool)] on the affected item.",
    note = "If that's not an option, consider using `PartialCool` instead." // in my bevy era
)]
trait MyCoolTrait<'a> {
    fn buf(&self) -> &'a [u8];
}

struct CoolType<'data>(&'data [u8]);

impl<'data> MyCoolTrait<'data> for CoolType<'data> {
    fn buf(&self) -> &'data [u8] {
        self.0
    }
}

/// I wish that I could be like the cool kids
//
// 我希望我能像帅孩子~
struct UncoolType;

/// generic to types that impl `MyCoolTrait`
///
/// 泛型参数, 需要满足 `MyCoolTrait` 特质
fn func_with_cool_bounds<'data, Cool: MyCoolTrait<'data>>(cool_type: Cool) {
    println!("dang look at all this data: {:#?}", cool_type.buf())
}

fn main() {
    let cool_type: CoolType = CoolType(&[1, 2, 3]);
    let uncool_type: UncoolType = UncoolType;

    func_with_cool_bounds(cool_type); // all good. compiler is happy
    func_with_cool_bounds(uncool_type); // uh oh! but hey, a custom err message...
}

That last line there gives you the following error:

最后一行给出了以下错误:

#![allow(unused)]
fn main() {
    Checking rs_2024_article_codeblocks v0.1.0 (/Users/barrett/Downloads/rs_2024_article_codeblocks)
error[E0277]: tell the user what's going on
  --> src/diagnostics_on_unimpl.rs:32:27
   |
32 |     func_with_cool_bounds(uncool_type); // uh oh! but hey, a custom err ...
   |     --------------------- ^^^^^^^^^^^ oh hey im pointing at the failed code
   |     |
   |     required by a bound introduced by this call
   |
   = help: the trait `MyCoolTrait<'_>` is not implemented for `UncoolType`
   = note: You may wish to add `#[derive(Cool)] on the affected item.
   = note: If that's not an option, consider using `PartialCool` instead.
   = help: the trait `MyCoolTrait<'data>` is implemented for `CoolType<'data>`
}

This attribute is vital for making certain types of derive macros. Please give it a try if you maintain a crate relying heavily on traits, as this technique can seriously help to inform your users!

此属性对于制作某些类型的派生宏至关重要: 如果您维护一个严重依赖特质(trait)的 crate, 请尝试一下, 因为这种技术可以极大地帮助通知您的用户!

ABI Documentation | ABI 文档

It’s in a weird module, but under the primitive fn (function pointer, NOT Fn trait) module documentation, there is now a section on ABI compatibility!

它位于一个奇怪的模块中, 但在原始 fn (函数指针, 而不是 Fn 特质) 模块文档下, 现在有一个关于 ABI 兼容性的部分!

These can help a lot when relying on #[repr(Rust)] types. These docs seem most useful when writing alternative compilers (like mrustc, gccrs, or Dozer), helping folks to start on the advanced intricacies of rustc instead of getting stuck on small ABI differences.

当依赖 #[repr(Rust)] 类型时, 这些可以有很大帮助. 这些文档在编写替代编译器(如 mrustc, gccrs, 或 Dozer)时似乎最有用, 可以帮助人们开始了解 rustc 的高级复杂性, 而不是陷入小的 ABI 差异.

(as a note, please support those projects I listed. alternative compilers are essential to the Rust ecosystem’s continued development!)

(一个小提醒, 请支持我列出的那些项目. 替代编译器对于 Rust 生态系统的持续发展至关重要!)

Option::inspect, Result::inspect, and Result::inspect_err

I’m in love with these methods. The two inspect methods are great for logging parsing progression, and Result::inspect_err feels almost vital at this point for logging on errors:

我爱上了这些方法(译者注: 我也是). 这两个 inspect 方法对于记录解析进度非常有用, 而 Result::inspect_err 在这一点上对于记录错误几乎至关重要:

#![allow(unused)]
fn main() {
let json: String = serde_json::to_string_pretty(report).inspect_err(|e| {
    tracing::warn!("Failed to make report into a pretty JSON string. (err: {e})")
})?;
}

I enjoy these so much that, in a few projects, I bumped up my MSRV just to use them. They make your code so nice to read…

我非常喜欢这些, 以至于在一些项目中, 我为了使用它们而提高了我的 MSRV(Minimum Supported Rust Version, 最低要求 Rust 版本). 它们让您的代码读起来非常好…

core::ptr::from_ref::<T> and core::ptr::from_mut::<T>

These types, tracked in Issue #106116, are a great way to create raw pointers in the general case. They protect from the usual annoyances of as casting, where you can slightly bend the type system if not careful.

它们在 Issue #106116 中进行跟踪, 是在一般情况下创建原始指针的好方法. 它们可以防止 as 带来的常见问题: 如果不小心, 您可能会 “掰弯” 类型系统(译者注: 如一个不小心 i32 as u32, 这种操作常见于 FFI 边界).

If you use these types, please consider linting for an accidental swap of shared (&) and exclusive (&mut) references. See clippy::as_ptr_cast_mut for more info.

如果您使用它们, 请考虑对共享 (&) 和独占 (&mut) 引用的意外交换进行 linting, 参阅 clippy::as_ptr_cast_mut.

Return-Position impl Trait… in Traits (RPITIT) | 在特质方法中返回 impl Trait…(RPITIT)

It feels like those acronyms get longer each time I look. In any case, with Rust 1.75, traits can now use RPIT like any other function/method item.

感觉每次我看这些缩写词都会变得更长(译者注: 我也是). 不管怎么样, 在 Rust 1.75 中, 特质(trait)现在可以像任何其他函数/方法一样使用 RPIT (译者注: 暂且理解为返回一个 opaque type, 返回值能直接写成 impl Trait 吧).

These work just like you’d expect, so please see the announcement blog post for additional information.

这些工作正如您所期望的那样, 因此请参阅公告博客文章以获取更多信息.

Async Functions in Traits (AFIT) | 特质(trait)支持异步方法 (AFIT)

The last PR also added async functions to traits, though they’re a little knee-capped. Here’s what that can look like:

上一个 PR 还为特质(trait)添加了异步函数, 尽管它们有点限制. 看起来是这样的:

pub trait Fart {
    async fn fart(&self) {
        tokio::time::sleep(std::time::Duration::from_millis(self.get_fart_time().await)).await;
        println!("<fart>");
    }

    async fn get_fart_time(&self) -> u64;
}

struct Bob;

impl Bob {
    const FART_TIME_MS: u64 = 300_u64;
}

impl Fart for Bob {
    async fn get_fart_time(&self) -> u64 {
        Self::FART_TIME_MS
    }
}

struct Sam;

impl Sam {
    const FART_TIME_MS: u64 = 600_u64; // much longer
}

impl Fart for Sam {
    async fn get_fart_time(&self) -> u64 {
        Self::FART_TIME_MS
    }
}

async fn main() {
    let bob = Bob;
    let sam = Sam;

    tokio::join! {
        bob.fart(),
        sam.fart()
    };
}

Note that these aren’t yet fully functional, as traits that use it are no longer dyn compatible (new term for “object safe”).

请注意, 这些尚未完全可用, 因为使用它的特质(trait)不再 dyn 兼容(“对象安全“的新术语):

#![allow(unused)]
fn main() {
fn take_farter(farter: &dyn Fart) {}
}

leads to this error:

导致这个错误:

#![allow(unused)]
fn main() {
error[E0038]: the trait `afit::Fart` cannot be made into an object
  --> src/afit.rs:45:25
   |
45 | fn take_farter(farter: &dyn Fart) {}
   |                         ^^^^^^^^ `afit::Fart` cannot be made into an object
   |
note: for a trait to be "dyn-compatible" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
  --> src/afit.rs:4:14
   |
3  | pub trait Fart {
   |           ---- this trait cannot be made into an object...
4  |     async fn fart(&self) {
   |              ^^^^ ...because method `fart` is `async`
...
9  |     async fn get_fart_time(&self) -> u64;
   |              ^^^^^^^^^^^^^ ...because method `get_fart_time` is `async`
   = help: consider moving `fart` to another trait
   = help: consider moving `get_fart_time` to another trait
   = help: the following types implement the trait, consider defining an enum where each variant holds one of these types, implementing `afit::Fart` for this new enum and using it instead:
             afit::Bob
             afit::Sam
}

So, if you need to use trait objects (dyn Farts syntax), you’ll want to add a helper crate: async_trait!

因此, 如果您需要使用特质(trait)对象 (dyn Farts 这种语法), 您需要添加一个 辅助 crate: async_trait!

(译者注: 例如 axum 里面重度使用了该 crate.)

#![allow(unused)]
fn main() {
use async_trait::async_trait;

#[async_trait]
pub trait Fart { /* ... */ }

#[async_trait]
impl Fart for Bob { /* ... */ }

#[async_trait]
impl Fart for Sam { /* ... */ }
}

Now, take_farter compiles just fine! :D

现在, take_farter 编译得很好! :D

Behind the scenes, though, this proc macro is doing a lot of work:

不过, 在幕后, 这个过程宏做了很多工作:

#![allow(unused)]
fn main() {
impl Fart for Bob {
    fn get_fart_time<'life0, 'async_trait>(
        &'life0 self,
    ) -> Pin<Box<dyn Future<Output = u64> + Send + 'async_trait>>
    where
        'life0: 'async_trait,
        Self: 'async_trait,
    {
        Box::pin(async move {
            if let Some(__ret) = None::<u64> {
                #[allow(unreachable_code)]
                return __ret;
            }
            let __self = self;
            let __ret: u64 = { Self::FART_TIME_MS };
            #[allow(unreachable_code)]
            __ret
        })
    }
}
    // ...
}

See that Box right there? That’s a peek into the embedded trenches…

看到那里的 Box 吗? 这是对嵌入战壕的一瞥…

(译者注: 确实过于 “不优雅”, 不过还好 async_trait 为我们处理好了…)

Nonetheless, this option is useful for binaries, but be careful when doing this stuff in your libraries. Additional changes are needed for semantic versioning to be consistent here.

尽管如此, 此选项对于二进制文件很有用, 但在库中使用时要小心, 需要注意语义版本控制在此处保持一致. (译者注: 即需要支持 Rust 1.75 以下.)

const Blocks

When you need these, you need them. const evaluation has historically been a little difficult to control, governed by the internal (opaque) rules of the compiler as it pursues const promotion. In libraries operating in low-level spaces, const eval can significantly impact performance, so many folks pursue it aggressively: if a maintainer has any doubt, they’ll const-ify any parameter into a const PARAM just to encourage the compiler.

当您需要这些的时候, 您就需要它们. const 计算历来有点难以控制, 受编译器内部(不透明的)规则的控制, 因为它追求尽可能 const(const promotion). 底层库应用 const 计算将显著影响性能, 因此许多人积极追求它: 如果维护者有任何疑问, 他们会将任何参数常量化为 const PARAM, 只是为了鼓励编译器.

With const blocks, you can directly tell the compiler that it should simplify the given expression at compile-time.

使用 const 块, 您可以直接告诉编译器应该在编译时简化给定的表达式.

Here’s a short example of how this looks:

简单的例子:

#![allow(unused)]
fn main() {
// probably not realistic but shhh pretend we're talking to an allocator
let m = allocate(const { 1024 * 8 });
}

If there was any doubt whether that would be evaluated by the compiler, it’s gone now. Our troubles were dealt with at compile-time.

如果对编译器是否会对其进行计算有任何疑问, 那么现在就没了: 我们的麻烦在编译时就得到了解决.

Some Extras | 一些额外的内容

Here are some other things I liked:

以下是我喜欢的其他一些东西:

Wishlist for 2025 | 2025 愿望单

Ok, 2024 was great for Rust! But, there are still some things that are missing. Let’s discuss my wishlist for Rust in 2025:

好吧, 2024 年对 Rust 来说是伟大的一年! 但是, 仍然缺少一些东西. 让我们讨论一下我对 2025 年 Rust 的愿望清单:

Compile-Time Reflection | 编译时反射

Compile-time reflection is a construct to analyze source code at compile time. In short, it replaces small code generation tasks (think serde, thiserror, and bevy_reflect) with normal Rust source code.

编译时反射是在编译时分析源代码的构造. 简而言之, 它用普通的 Rust 源代码替换了小型代码生成任务(例如 serde, thiserrorbevy_reflect).

In my view, this is one of the few large-scale optimizations on compile time we’ve got left (you know… ignoring the whole batch compiler thing). It would vastly reduce compile times for the largest Rust binaries, especially for large applications like web servers.

在我看来, 这是我们剩下的少数几个尚未实现的大规模的编译时间优化手段之一, 它将大大减少 Rust 大型二进制文件的编译时间, 特别是对于 Web 服务器等大型应用程序.

Reflection would lessen the amount of syn we’d see slowly compiling alone, allowing Rust developers to iteratively make changes as if we hand-rolled all our serde::De/Serialize implementations, without giving up on our high-level constructs. It is my #1 prospect for the language - after this, everyone could go home until 2026. I would still be happy. (please don’t though!)

反射将减少我们需要单独缓慢编译的 syn 数量, 允许 Rust 开发人员迭代地进行更改, 就像我们手动滚动所有 serde::De/Serialize 实现一样, 而不放弃我们的高级构造. 这是我对这门语言的第一大期望. 实现这个, 我们每个人都可以直接回家待到 2026 年, 我仍然会很高兴.(但请不要这样做!)

Modern Allocator Trait | 现代 Allocator 特质

Let’s take a look at my favorite thing ever - the new Allocator trait’s allocate() method:

让我们来看看我最喜欢的东西: 新的 Allocator 特质(trait)中的 allocate() 方法:

#![allow(unused)]
fn main() {
pub unsafe trait Allocator {
    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError>;

    // ...
}
}

Ok… do you see that? The Result in its return type?

好… 您看到了吗? 返回类型中的 Result?

This new allocator interface lets you fallibly manage your memory without checking for null pointers at every stage! Or, in other words, sane human beings can manage the memory in their applications without immediately resorting to unsafe. Drop is Rust’s comfy free, but allocate is finally giving Rust a comfy malloc.

这个新的 allocator 接口可以让您有效管理内存分配, 而无需在每个阶段检查空指针! 或者, 换句话说, 理智的人可以管理应用程序中的内存, 而无需诉诸不安全的方法. 如 Drop 是 Rust 中舒适版本的 free 般, allocate 最终给了 Rust 一个舒适的 malloc.

This new allocator will be very impactful! I’ll list a few benefits here:

这个新的 allocator 接口将非常有影响力! 我在这里列出一些好处:

  • The Linux kernel can use Rusty memory management (i.e. unite kernel::alloc with… everyone else)

    Linux 内核可以使用 Rusty 内存管理(即将 kernel::alloc 与其他联合起来).

  • Embedded developers won’t have to fight demons to manage their allocators

    嵌入式开发人员不必与恶魔作斗争来管理他们的分配器.

  • Crates using custom allocators for performance won’t have to use global state

    使用自定义分配器来提高性能的 crate 不必强制改变全局分配器.

  • Stuff will get faster in general :)

    一般来说, 事情会变得更快:)

Unfortunately, it’s not done yet. If you have any ideas or needs that seem unfulfilled, please reach out to the Allocators WG (working group) on Zulip!

不幸的是, 它还没有完成. 如果您有任何未满足的想法或需求, 请联系 the Allocators WG (working group) on Zulip!

Enum Variant Types | 枚举变体类型

When you write an enum, you sometimes want to pass around a variant for various reasons. Maybe it avoids dozens of newtypes, powers your state machine, or helps in reducing boilerplate.

当您编写枚举时, 有时您会出于各种原因想要传递变体. 它也许可以避免数十种新类型, 为您的状态机提供强大助力, 或者有助于减少样板代码.

Unfortunately, Rust’s enums are not currently capable of these, as variants are not types. The workaround isn’t pretty. I linked it above, but often, you’ll end up using the newtype pattern on all of your enum variants:

不幸的是, Rust 的 enum 目前无法实现这些, 因为变体不是类型. 解决方法并不漂亮. 我在上面链接了它, 但通常, 您最终会在所有枚举变体上使用 newtype 模式:

#![allow(unused)]
fn main() {
pub enum ComponentDescription {
    CpuDescription(CpuDescription),
    RamDescription(RamDescription),
    // ...
}

That’s because, without it, you can’t share each variant as a type. For example, if I know that this component has a RamDescription, then there’s no use in pattern matching it out. A lot of Rust code would become significantly easier to read with variant types.

这是因为, 如果没有它, 您就无法将每个变体作为类型共享. 例如, 如果我知道这个组件有一个 RamDescription, 那么对其进行模式匹配就没有用了. 使用变体类型, 许多 Rust 代码将变得更容易阅读.

Stabilization of #[feature(let_chains)]

I really love let_chains! With these, you can combine verbose instances of pattern matching into just a few lines.

我真的很喜欢 let_chains! 有了这些, 您可以将模式匹配的详细实例合并为几行:

(译者注: 我也很喜欢!)

#![allow(unused)]
fn main() {
let my_result: Result<u32, MyError> = Result::Ok(2025_u32);

if let Ok(res) = my_result
    && res > 2024_u32
{
    println!("ayo it's 2025!");
}
}

They’re not currently stable, but I use them in all my Nightly projects! :)

它们目前不稳定, 但我在所有的 Nightly 项目中都使用它们! :)

ABI

I hope that #[repr(Rust)] never becomes stable. Read these for more info:

我希望 #[repr(Rust)] 永远不会进入稳定 阶段. 阅读以下内容以获取更多信息:

Oh… you came back! I didn’t expect that!

哦… 您回来了! 我没想到会这样!

So anyways, Rust is considering its own stable ABI called crabi, with its own repr tag: #[repr(crabi)]. In short, this means you’d be able to write languages that “spoke” Rust. I think we’d start seeing more high-level systems languages (similar to the now-defunct, and wonderful, June Language or Go) based on the crabi ABI model.

所以无论如何, Rust 正在考虑自己的稳定 ABI, 称为 crabi, 具有自己的 repr 标签: #[repr(crabi)]. 简而言之, 这意味着您将能够编写 “讲” Rust 的语言. 我认为我们会开始看到更多基于 crabi ABI 模型的高级系统语言(类似于现已不复存在的精彩的 June Language 或 Go).

Python would likely gain support for crabi, so I can imagine a world where the two languages have a large overlap in ecosystems.

Python 可能会获得对 crabi 的支持, 我可以想象这两种语言生态交叉的世界.

adt_const_params feature - Use Custom Types in Your const Generics

This one is nice. In essence, you can now share important info at compile-time without using const functions and parameters. These can encourage the compiler to evaluate related expressions at compile-time and avoid passing parameters around. Instead, it’s engrained into the type system!

这个不错. 本质上,您现在可以在编译时共享重要信息, 而无需使用 const 函数和参数. 这些可以鼓励编译器在编译时评估相关表达式并避免传递参数. 相反, 它已根植于类型系统中!

Option::inspect_none

This one sounds kinda funny, but I want a way to log when there’s no value.

这听起来有点有趣, 但我想要一种在 Option::None 时记录日志的方法:

Like so:

例如:

#![allow(unused)]
fn main() {
let username: Option<String> = account.username().inspect_none(|| {
    tracing::error!("User does not have a username! (id: `{}`)", account.id())
});
}

Currently, we have to use if account.username().is_none(), which is a bit verbose for a logging construct.

目前, 我们必须使用 if account.username().is_none(), 这对于日志记录来说有点冗长.

Closing Thoughts | 结束语

These are some of my favorite changes from 2024, and my hopes for 2025! 这些是 2024 年以来我最喜欢的一些变化, 以及我对 2025 年的希望!

Rust is doing its Annual Community Survey until December 23rd, 2024, so please fill out the form if you want to share your thoughts! (but blog posts work too)

Rust 正在进行年度社区调查, 截止日期为 2024 年 12 月 23 日(译者注: 不幸, 截至译稿截稿时已是 30 日), 因此如果您想分享您的想法, 请填写表格! (博客文章也可以)


  1. These invariants on references are mentioned here in the RFC. Note that they’re incomplete, so additional invariants may exist. RFC 中提到了这些引用的前提保证. 请注意, 它们是不完整的. ↩2

This Week in Rust (TWiR) Rust 语言周刊中文翻译计划, 第 585 期

本文翻译自 Oleksandr Prokhorenko 的博客文章 https://minikin.me/blog/computed-properties-in-rust, 英文原文版权由原作者所有, 中文翻译版权遵照 CC BY-NC-SA 协议开放. 如原作者有异议请邮箱联系.

相关术语翻译依照 Rust 语言术语中英文对照表.

囿于译者自身水平, 译文虽已力求准确, 但仍可能词不达意, 欢迎批评指正.

2025 年 2 月 7 日晚, 于广州.

GitHub last commit

(译者注: 本文适合 Rust 新手阅读, Rust 熟手可跳过.)

Computed Properties in Rust

Rust 计算属性 (computed properties) 最佳实践

Introduction | 前言

Computed properties dynamically calculate values when accessed instead of storing them. While languages like Swift and JavaScript support them natively, Rust requires explicit patterns. This guide covers five approaches to replicate computed properties in Rust, including thread-safe solutions for concurrent code.

所谓计算属性 (computed properties), 即需要根据已有数据计算的属性. SwiftJavaScript 之类的语言原生支持计算属性, 但 Rust 里需要明确的模式. 本指南涵盖了五种在 Rust 中实现计算属性的方法, 包括适用于并发代码的线程安全的解决方案.

In Swift, a computed property recalculates its value on access:

Swift 中, 计算属性将在访问时重新计算:

struct Rectangle {
    var width: Double
    var height: Double

    var area: Double { // 计算属性
        width * height
    }
}

let rect = Rectangle(width: 10, height: 20)
print(rect.area) // 200

Rust doesn’t support this syntax, but we can achieve similar results with methods and caching strategies.

Rust 不支持此语法, 但是我们可以通过关联方法和缓存策略获得类似的结果.

Using Getter Methods (No Caching) | 使用 Getter 方法 (无缓存)

📌 Best for: Simple calculations or frequently changing values.

📌 最适用于: 计算简便或经常变化的值.

🦀 Rust Implementation | Rust 实现

#[derive(Debug)]
struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
}

fn main() {
    let rect = Rectangle { width: 10.0, height: 20.0 };
    println!("Area: {}", rect.area()); // 200.0
}

👍 Pros | 优点:

  • Always up-to-date. 总是最新的.
  • No dependencies. 没有依赖性.
  • Zero overhead for caching or locking. 没有缓存或锁定的额外开销.

👎 Cons | 缺点:

  • Recomputed on every call (no caching). 每次调用都会重新计算 (无缓存).

Using Lazy Computation with OnceLock (Efficient Caching) 使用 OnceLock 进行惰性计算 (积极缓存)

📌 Best for: Immutable data with expensive computations.

📌 最适用于: 计算极其耗费资源的不变的数据.

Rust’s OnceLock lets you lazily compute a value one time. Once written, you cannot reset or invalidate it — perfect for data that never changes.

RustOnceLock 允许您惰性计算并存储结果(译者注: 调用 OnceLock::get_or_init 传入初始化方法在未初始化时初始化, 或者调用 OnceLock::set 直接存储一个结果), 往后您就无法修改了 — 非常适合(初始化后)永不更改的数据.

🦀 Rust Implementation | Rust 实现

use std::sync::{Arc, OnceLock};
use std::thread;

#[derive(Debug)]
struct Rectangle {
    width: f64,
    height: f64,
    cached_area: OnceLock<f64>,
}

impl Rectangle {
    fn new(width: f64, height: f64) -> Self {
        Self { width, height, cached_area: OnceLock::new() }
    }

    fn area(&self) -> f64 {
        *self.cached_area.get_or_init(|| {
            println!("Computing area...");
            self.width * self.height
        })
    }
}

fn main() {
    // Create the Rectangle in a single-threaded context.
    let mut rect = Rectangle::new(10.0, 20.0);

    // Compute area (first time, triggers computation).
    println!("First call: {}", rect.area()); // Computes and caches
    // Use cached value
    println!("Second call: {}", rect.area()); // Uses cached value

    // Modify width but does NOT invalidate the cache.
    rect.width = 30.0; // Has no effect on cached area

    // Prove that area() is still the cached value.
    println!("After modifying width: {}", rect.area()); // Still 200, not 600

    // Move rect into an Arc when we need multi-threading.
    let rect = Arc::new(rect);

    // Proving Thread-Safety
    let rect_clone = Arc::clone(&rect);
    let handle = thread::spawn(move || {
        println!("Thread call: {}", rect_clone.area());
    });

    handle.join().unwrap();

    println!("Final call: {}", rect.area());
}

🖨️ Expected Output | 预期输出

Computing area...
First call: 200
Second call: 200
After modifying width: 200
Thread call: 200
Final call: 200

👍 Pros | 优点:

  • Thread-safe once enclosed in Arc. 线程安全 (译者注: 参见 OnceLock 文档, 线程不安全版本为 OnceCell).
  • Zero overhead after first initialization. 首次初始化后零开销 (译者注: 还是有检查是否初始化完成的开销的).

👎 Cons | 缺点:

  • No invalidation: once set, remains forever. 不会失效:一旦设置, 永久保留.
  • Only for immutable data (or if you never need to re-compute). 仅用于不变的数据 (或者您永远不需要重新计算).

Mutable Caching with RefCell | RefCell 实现可变缓存

📌 Best for: Single-threaded mutable data, where the computed value can be invalidated or re-computed multiple times.

📌 最适用于: 单线程数据, 需要可变性.

Rust’s interior mutability pattern allows us to store a cache (such as an Option<f64>) behind an immutable reference. RefCell<T> enforces borrowing rules at runtime rather than compile time.

RefCell<T> 具备内部可变性, 将编译时借用检查挪到运行时.

🦀 Rust Implementation | Rust 实现

use std::cell::RefCell;
use std::sync::atomic::{AtomicUsize, Ordering};

static COMPUTE_COUNT: AtomicUsize = AtomicUsize::new(0);

#[derive(Debug)]
struct Rectangle {
    width: f64,
    height: f64,
    // Cache stored in RefCell for interior mutability
    cached_area: RefCell<Option<f64>>,
}

impl Rectangle {
    fn new(width: f64, height: f64) -> Self {
        Self { width, height, cached_area: RefCell::new(None) }
    }

    fn area(&self) -> f64 {
        let mut cache = self.cached_area.borrow_mut();
        match *cache {
            Some(area) => {
                println!("Returning cached area: {}", area);
                area
            }
            None => {
                println!("Computing area...");
                let area = self.width * self.height;
                // Only for debugging purposes to track how many times the area is actually computed.
                COMPUTE_COUNT.fetch_add(1, Ordering::SeqCst);
                *cache = Some(area);
                area
            }
        }
    }

    fn set_size(&mut self, width: f64, height: f64) {
        println!("Updating dimensions and clearing cache...");
        self.width = width;
        self.height = height;
        self.cached_area.replace(None); // Invalidate the cache
    }

    fn invalidate_cache(&self) {
        println!("Invalidating cache...");
        self.cached_area.replace(None);
    }
}

fn main() {
    let mut rect = Rectangle::new(10.0, 20.0);

    println!("First call: {}", rect.area()); // Computes
    println!("Second call: {}", rect.area()); // Cached

    rect.set_size(15.0, 25.0); // Mutates and invalidates cache
    println!("After resize: {}", rect.area()); // Recomputes

    rect.invalidate_cache(); // Manually invalidate cache
    println!("After cache invalidation: {}", rect.area()); // Recomputes

    println!("Times computed: {}", COMPUTE_COUNT.load(Ordering::SeqCst)); // Should be 3
}
译者注

注意到示例大量使用 Ordering::SeqCst, 实际上在业务中不推荐, 推荐阅读 The Rustonomicon’s Github repo, issue 166 获取更多信息.

至于推荐用法, 简单总结如下:

  • 对于 fetch_xxx 一类先读后写的, 应当使用 AcqRel
  • 读取 (load) 用 Acquire
  • 写入 (store) 用 Release
  • 对原子性没多大需求, 例如只是简单计数的场景, 用 Relax 即可

🖨️ Expected Output | 预期输出

Computing area...
First call: 200
Returning cached area: 200
Second call: 200
Updating dimensions and clearing cache...
Computing area...
After resize: 375
Invalidating cache...
Computing area...
After cache invalidation: 375
Times computed: 3

👍 Pros | 优点:

  • Handles mutable data. 数据可变.
  • Explicit invalidation available. (译者注: 即可让缓存失效然后刷新)

👎 Cons | 缺点:

  • Not thread-safe. (译者注: 都用 RefCell 了, 自然线程不安全, 更 Rust 的说法就是 not Sync)
  • Runtime borrow checks add overhead. 运行时借用检查添加开销(译者注: 运行时检查也让 BUG 更难找, 把借用检查推到运行时也丧失 Rust 编译时阻止大部分内存不安全操作优势了. 除非是 cpp 熟练应用者转 Rust, 否则慎用, 也无多大优势.)

Thread-Safe Caching with Mutex | 带有 Mutex 的线程安全缓存

📌 Best for: Shared data across threads, when updates or caching need exclusive access.

📌 最适用于: 跨线程共享数据, 读取或写入是独占性的 (译者注: 即 Mutex 的特性)

For multi-threaded scenarios, we can wrap our cache in a Mutex<Option<f64>>. The Mutex enforces mutual exclusion, meaning only one thread can compute or update the cache at a time.

对于多线程场景, 我们可以将缓存包裹在 Mutex 内, 如 Mutex<Option<f64>>, 限制只有一个线程可以进行操作.

🦀 Rust Implementation | Rust 实现

use std::sync::{Arc, Mutex};
use std::thread;

struct Rectangle {
    width: f64,
    height: f64,
    cached_area: Mutex<Option<f64>>,
}

impl Rectangle {
    fn new(width: f64, height: f64) -> Self {
        Self { width, height, cached_area: Mutex::new(None) }
    }

    fn area(&self) -> f64 {
        let mut cache = self.cached_area.lock().unwrap();
        match *cache {
            Some(area) => area,
            None => {
                println!("Computing area...");
                let area = self.width * self.height;
                *cache = Some(area);
                area
            }
        }
    }
}

fn main() {
    let rect = Arc::new(Rectangle::new(10.0, 20.0));
    let mut handles = vec![];

    // Spawn 4 threads
    for _ in 0..4 {
        let rect = Arc::clone(&rect);
        handles.push(thread::spawn(move || {
            println!("Area: {}", rect.area());
        }));
    }

    for handle in handles {
        handle.join().unwrap();
    }
}
译者注

对于 Mutex, 标准库中 Mutex 在部分线程 panic 情况下会导致 “中毒” (poisoned) 的问题, 在生产应用中, 常使用来自第三方库的 Mutex 实现, 例如:

  • parking_lot::Mutex 参阅其官方文档.

  • antidote::Mutex 只是标准库实现的简单包装, 但是方法都不是 const 的, 在全局变量的场景下用不了, 我的 PR 也没见官方合并…

  • tokio::sync::Mutex 一般用不着, 除非你确信你需要跨线程共享 MutexGuard, 但是很不推荐这么干, 最佳实践应该是即锁即用, 用完立即释放 (即 drop 掉 MutexGuard, 可以说离开作用域自动 Drop, 或者手动 Drop).

更多地, 还需要指出一个常见问题 (cargo clippy 应该也会提示).

一个 示例:

use std::sync::Mutex;

fn main() {
    let data: Mutex<Option<i32>> = Mutex::new(None);
    
    // try uncomment the following line?
    *data.lock().unwrap() = Some(1);
    
    // test code
    {
        if let data @ Some(_) = data.lock().unwrap().as_ref() {
            println!("Get: {data:?}");
        } else {
            // 问: 此时前面 `data.lock()` 上的锁解除了吗?
            *data.lock().unwrap() = Some(1);
        }
    }
    
    println!("After: {:?}", data.lock().unwrap());
}

答案是没有.

不信? 第一次遇到这种情况, 直觉肯定是已经离开作用域了, else 里面再锁没问题. 但是实际上整块 if else 都在一个作用域内, if let 只是个特殊的写法罢了.

你可以在 playground 里面注释掉首个 *data.lock().unwrap() = Some(1);, 点击 Run 看看会发生什么(会卡很久没反应, 直到超出官方 Playground 对于单次运行时间的限制).

当然, 要善于利用 cargo clippy, 聪明的 clippy 会明确阻止你那么干(虽然编译器是能编译通过的):

    Checking playground v0.0.1 (/playground)
error: calling `Mutex::lock` inside the scope of another `Mutex::lock` causes a deadlock
  --> src/main.rs:11:9
   |
11 |           if let data @ Some(_) = data.lock().unwrap().as_ref() {
   |           ^                       ---- this Mutex will remain locked for the entire `if let`-block...
   |  _________|
   | |
12 | |             println!("Get: {data:?}");
13 | |         } else {
14 | |             *data.lock().unwrap() = Some(1);
   | |              ---- ... and is tried to lock again here, which will always deadlock.
15 | |         }
   | |_________^
   |
   = help: move the lock call outside of the `if let ...` expression
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#if_let_mutex
   = note: `#[deny(clippy::if_let_mutex)]` on by default

error: could not compile `playground` (bin "playground") due to 1 previous error

Rust 2024 Edition 以后, 打了个 if_let_rescope 的补丁, 参见 https://github.com/rust-lang/rust/issues/131154. 相同的代码 就能正常跑了:

2024 Edition

此前你只能老老实实用这种写法:

use std::sync::Mutex;

fn main() {
    let data: Mutex<Option<i32>> = Mutex::new(None);
    
    // try uncomment the following line?
    // *data.lock().unwrap() = Some(1);
    
    // test code
    {
        let guard = data.lock().unwrap();
        if let data @ Some(_) = guard.as_ref() {
            println!("Get: {data:?}");
        } else {
            drop(guard); // 显式 drop 掉 MutexGuard
            *data.lock().unwrap() = Some(1);
        }
    }
    
    println!("After: {:?}", data.lock().unwrap());
}

🖨️ Expected Output | 预期输出

Computing area...
Area: 200
Area: 200
Area: 200
Area: 200

👍 Pros | 优点:

  • Thread-safe. 线程安全.
  • Computes once across threads. 跨再多线程都只需要计算一次.

👎 Cons | 缺点:

  • Locking overhead (all threads block during the write). 有锁.

Optimized Reads with RwLock | 读优化的 RwLock

译者注:

内容和 Mutex 类似, 只不过换成 std::sync::RwLock 了, 不再翻译.

但需要指出: 除非你确信并发读远多于写, 否则 Mutex 速度反而可能更快, 不要被迷惑. 个中原因应归咎于 RustRwLock 实际上依赖于操作系统实现, 而 Mutex 是纯 Rust 实现.

遇事不决就多 bench, 对于本文所述作计算属性用, Mutex 在大部分情况下足矣.

Comparison Table | 比较表

ApproachUse CaseThread-SafeOverheadInvalidation
方法使用场景线程安全否?开销
Getter MethodSimple, non-cached valuesNoneAlways recomputed
OnceCellImmutable, expensive computationsLowNot possible (one-and-done)
RefCellSingle-threaded mutable dataModerateManual (replace(None))
MutexThread-safe, shared dataHighManual (lock & reset Option)
RwLockRead-heavy concurrent accessHighManual (write lock & reset)

Final Thoughts | 后话

Rust might not have Swift-like computed properties built into the language syntax, but it more than compensates with low-level control and flexible lazy/cached patterns. Whether you pick a simple method, an interior-mutability cache, or a multi-threading–friendly lock-based approach, Rust gives you a safe, explicit way to manage when and how expensive computations run.

Rust 没有内置的类似于 Swift 中的计算属性, 但可以通过低级控制和灵活的惰性执行/缓存模式替代实现类似功能. 无论您选择简单的 Getter 方法, 内部可变性缓存还是上锁, Rust 都可以为您提供安全明确的方法决定何时进行昂贵的计算.

  • Getter methods for no caching. Getter 方法, 没有缓存.
  • OnceLock (or OnceCell) for one-time lazy initialization on immutable data. 用于不变数据的一次性的惰性初始化.
  • RefCell for single-threaded mutable caching with manual invalidation. 用于手动使失效的的单线程环境下的内部可变性缓存.
  • Mutex / RwLock for multi-threaded caching, balancing read concurrency and write locking. 用于多线程缓存, 平衡并发读和写锁定.

Choose the pattern that aligns with your data’s mutability, concurrency, and performance needs. Rust’s explicit nature means you’re always in control of exactly when and how a property is computed, updated, or shared across threads.

选择与您的数据可变性、并发性和性能需求相匹配的模式. Rust 的显式特性意味着您始终可以精确控制属性何时以及如何被计算、更新或在线程间共享.

(译者注: 计算完成后不需要可变选 OnceLock, 否则 Mutex)

This Week in Rust (TWiR) Rust 语言周刊中文翻译计划, 第 585 期

本文翻译自 Evan Schwartz 的博客文章 https://emschwartz.me/pinning-down-future-is-not-send-errors, 英文原文版权由原作者所有, 中文翻译版权遵照 CC BY-NC-SA 协议开放. 如原作者有异议请邮箱联系.

相关术语翻译依照 Rust 语言术语中英文对照表.

囿于译者自身水平, 译文虽已力求准确, 但仍可能词不达意, 欢迎批评指正.

2025 年 2 月 8 日晚, 于广州.

GitHub last commit

来自译者的前言:

Rust 的难度有目共睹, 异步 Rust 更是难上加难, 毕竟异步本来就不是件简单的事情, 有的语言一开始压根没有异步的概念(例如 Python 直到 3.4 才引入 asyncio), 有的语言异步从一而终(无 goroutine 无 Go), 它们大多数都将异步那些复杂的实现隐藏, 让新手也能轻松入门, 而 Rust 作为现代系统级编程语言, 选择让你去从底层控制(当然 tokio 一类的库帮你干了很多很多).

本文主要讲述如何理解, 以及如何定位哪导致 Future is not Send 的问题, 个人觉得写得非常好, 适合初学者学习.


Pinning Down “Future Is Not Send” Errors

定位 “Future Is Not Send” 错误

If you use async Rust and Tokio, you are likely to run into some variant of the “future is not Send” compiler error. While transitioning some sequential async code to use streams, a friend suggested a small technique for pinning down the source of the non-Send errors. It helped a lot, so I thought it would be worth writing up in case it saves others some annoying debugging time.

如果您使用异步 Rust 和 Tokio, 则可能会遇到各式各样的 “future is not Send” 编译器错误. 在试图将同步代码异步化(译者注: 大部分情况下简单加上 async 关键字就可以啦) 以使用流(stream)时, 一个朋友建议一种小型技术来定位 non-Send 错误的来源. 它有很大帮助, 所以我认为值得在此分享, 让后来者节省一些令人讨厌的调试时间.

I’ll give a bit of background on Futures and Send bounds first, but if you want to skip past that you can jump to The DX Problem with Non-Send Futures or Pinning Down the Source of Non-Send Errors.

我会先介绍一些有关 Future 或者 Send 的背景知识, 当然您也可以跳到后文.

Table of contents

Why Futures Must Be Send | 为什么 Futures 必须(实现) Send

(译者特注: Send 是 Rust 中的一个概念, 语言设计上表示为一个 marker trait, “impl Send” 和 “某个结构体(or else) Send” 的说法是一个意思, 下不再赘述.)

I wrote another blog post about the relationship between async Rust and Send + Sync + 'static so we won’t go into detail about that here. The main thing we’ll focus on here is that if you’re using Tokio, you’re probably going to be spawning some Futures, and if you spawn a Future it must be Send + Sync + 'static.

我写了另一篇博客文章(译者注: 后续视情况翻译), 介绍了异步 Rust 和 Send + Sync +'static 之间的关系, 因此我们在这里不会详细介绍. 我们将重点关注的主要内容是, 如果您使用的是Tokio, 那么您可能会 spawn 一些 Futures, 它们必须是 Send + Sync + 'static 的.

#![allow(unused)]
fn main() {
pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
where
    F: Future + Send + 'static,
    F::Output: Send + 'static,
}

How Futures Lose Their Send Markers | Futures 是怎么就不 Send 了的

Most types are automatically marked as Send, meaning they can be safely moved between threads.

大多数类型都自动实现了 Send, 这标记着它们 可以在线程之间安全地移动.(译者注: marker trait 确实非常贴切, marker -> 标记)

As the The Rustonomicon says:

但如 Rust 死灵书 (Rustomonicon) 所说:

Major exceptions include 主要例外包括:

  • raw pointers are neither Send nor Sync(because they have no safety guards). 原始指针既不 Send 也不 Sync (因为它们没有任何安全保证).
  • UnsafeCell isn’t Sync (and therefore Cell and RefCell aren’t). UnsafeCellSync (因此 Cell 和 RefCell 亦然).
  • Rc isn’t Send or Sync (because the refcount is shared and unsynchronized). Rc 既不 Send 也不 Sync (因为引用计数是共享和不同步的).

Pointers and Rcs cannot be moved between threads and nor can anything that contains them. (简而言之), 指针和 Rcs 不能在线程之间移动, 包含它们的任何东西自然也如此.

Futures are structs that represent the state machine for each step of the asynchronous operation. When a value is used across an await point, that value must be stored in the Future. As a result, using a non-Send value across an await point makes the whole Future non-Send.

Futures 是代表异步操作每个步骤的状态机的结构体. 当跨 .await 边界使用一个值时, 该值必须存储在 Future 中. 因此, 跨 .await 边界使用的值是非 Send 的, 将导致产生的或从属的 Future 也是非 Send 的.

译者补充(必读):

async fn 算个语法糖, 本质 (解糖, de-sugar) 是返回一个匿名结构体, 该结构体实现了 Future 这个 trait (Return Position impl Trait, RPIT). 在后续让 trait 里面支持写 async fn (async fn In Traits, AFIT) 本质也是如此 (Return Position impl Trait In Traits, RPITIT).

#![allow(unused)]
fn main() {
trait TestT {
    // AFIT 写法
    async fn hello() -> Result<String, Error>;
    // RPITIT 的写法
    fn hello() -> impl Future<Output = Result<String, Error>>;
}
}

目前 AFIT 并未非常成熟, 还是推荐 RPITIT 的写法, 官方对于 pub trait 也是如此推荐的.

这篇博文更深入一点, 有兴趣可以阅读: https://nihil.cc/posts/rust_rpitit_afit/

The DX Problem with Non-Send Futures | 非 Send Futures 的 DX 问题

(译者注: 原文并没有指出 DX 是什么的缩写, 不译, 不影响理解)

To illustrate the problem in the simplest way possible, let’s take an extremely simplified example.

为了以最简单的方式说明问题, 让我们看一个极为简化的示例.

Below, we have an async noop function and an async not_send function. The not_send function holds an Rc across an await point and thus loses its Send bound – but shhh! let’s pretend we don’t know that yet. We then have an async_chain that calls both methods and a function that spawns that Future.

下面的示例中, 我们有一个异步的 noop 方法和一个异步的 not_send 方法. not_send 方法中 Rc 的生命周期跨越 .await 边界 (或者说在 Rc 还 “活着” 的时候 .await 了其他异步方法), 因此不再 Send. 但是! 让我们假装我们还不知道 (毕竟代码行数一多起来就很容易忽略). 然后, async_chain 调用了这两个方法, 还有一个 spawns Future 的方法.

(译者特注: Future 语言设计上表示为一个 trait, “返回一个匿名结构体, 这个结构体 impl Future” 的说法和 “返回一个 Future” 的说法是一个意思, 下不再赘述.)

#![allow(unused)]
fn main() {
use tokio;
   
async fn noop() {}
   
async fn not_send() -> usize {
    let ret = std::rc::Rc::new(2); // <-- this value is used across the await point 这个值生命周期跨越了 await 点
    noop().await;
    *ret
}
   
async fn async_chain() -> usize {
    noop().await;
    not_send().await
}
   
fn spawn_async_chain() {
    tokio::spawn(async move {
        let result = async_chain().await;
        println!("{}", result);
    }); // <-- compiler points here 编译器(错误信息)指向这里
}
}

This code doesn’t compile (playground link). But where does the compiler direct our attention? If we only take a quick look at the error message, it seems like the error is coming from the tokio::spawn call:

此代码不能编译通过 (来这里试一试, 如果是 mdbook 可以直接点击运行看看). 但是编译器在哪里指出了问题? 如果我们只粗略查看错误消息, 似乎错误来自 tokio::spawn 调用:

error: future cannot be sent between threads safely
   --> src/lib.rs:17:5
    |
17  | /     tokio::spawn(async move {
18  | |         let result = async_chain().await;
19  | |         println!("{}", result);
20  | |     });
    | |______^ future created by async block is not `Send`
    |
    = help: within `{async block@src/lib.rs:17:18: 17:28}`, the trait `Send` is not implemented for `Rc<usize>`
note: future is not `Send` as this value is used across an await
   --> src/lib.rs:7:12
    |
6   |     let ret = std::rc::Rc::new(2);
    |         --- has type `Rc<usize>` which is not `Send`
7   |     noop().await;
    |            ^^^^^ await occurs here, with `ret` maybe used later
note: required by a bound in `tokio::spawn`
   --> /playground/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.43.0/src/task/spawn.rs:168:21
    |
166 |     pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
    |            ----- required by a bound in this function
167 |     where
168 |         F: Future + Send + 'static,
    |                     ^^^^ required by this bound in `spawn`

In this example, it’s easy to spot the mention of the Rc not being Send – but we know what we’re looking for! Also, our async chain is pretty short so that types and error messages are still somewhat readable. The longer that chain grows, the harder it is to spot the actual source of the problem.

在此示例中, 很容易发现非 SendRc<usize> 的存在; 另外, 我们的异步调用链非常短, 因此类型和错误消息可阅性尚佳. 调用链越长, 发现问题的实际来源就越难(译者注: 确实, 写实际项目经常是好几十层, 看着就头疼).

The crux of the issue is that the compiler draws our attention first to the place where the bounds check fails. In this case, it fails when we try to spawn a non-Send Future – rather than where the Future loses its Send bound.

问题的关键是: 编译器首先将我们的注意力吸引到检查到失败的边界. 在这种情况下, 当我们尝试 spawn 一个非 SendFuture 时, 它会失败, 而不是让这个 Future (或者说这个实现 Future 的匿名结构体)不再 Send 的地方.

Pinning Down the Source of Not-Send Errors | 定位非 Send 错误的来源

There are a number of different ways we could pin down the source of these errors, but here are two:

我们可以通过多种不同的方式来定位这些错误的来源, 这里给出两个:

Replacing async fn with an impl Future Return Type | 用 RPIT 代替 async fn

Instead of using an async fn, we can instead use a normal fn that returns a Future. (This is what the async keyword does under the hood, so we can just forego that bit of syntactic sugar.)

用返回一个 impl Future 匿名结构体的普通方法代替 async fn (这就是 async 关键字作用于 fn 的本质, 因此我们可以手动放弃这个语法糖).

We can transform our example above into something that looks like the code below using an async block, or alternatively using Future combinators.

我们可以将上面的示例转换为使用 async 块的看起来像代码的东西, 或使用 Future 组合器的代码.

Neither of these will compile (playground link), but this time the compiler errors will point to the Futures returned by async_chain or combinator_chain not fulfilling the Send bound that we are specifying.

这些 都无法编译通过, 但是这次编译器错误将明确指出 async_chaincombinator_chain 返回的 Futures 不符合我们指定的 Send 限定.

#![allow(unused)]
fn main() {
use tokio;
use std::future::Future;
use futures::FutureExt;

async fn noop() {}

async fn not_send() -> usize {
    let ret = std::rc::Rc::new(2);
    noop().await;
    *ret
}

fn async_chain() -> impl Future<Output = usize> + Send + 'static { // note the return type 明确写出返回类型 (虽然是 RPIT)
    async move {
        noop().await;
        not_send().await
    } // <-- now the compiler points here
}

fn spawn_async_chain() {
    tokio::spawn(async move {
        let result = async_chain().await;
        println!("{}", result);
    });
}

fn combinator_chain() -> impl Future<Output = usize> + Send + 'static { // <-- the compiler will also point here
    noop().then(|_| not_send()) // 来自三方库(?) futures 的方法
}

fn spawn_combinator_chain() {
    tokio::spawn(async move {
        let result = combinator_chain().await;
        println!("{}", result);
    });
}
}

The idea here is that we are foregoing the async fn syntax to explicitly state that the Future our functions return must be Send + ’static.

这里的精髓是, 我们正在剔除 async fn 语法, 明确指出我们的方法的返回的匿名结构体必须是 impl Future<Output = ***> + Send + 'static 的.

Helper Function to Enforce Send + 'static | 辅助方法以强制保证 Send + 'static

In the code below (playground link), we’ll keep our original async fns but this time we’ll use a helper function send_static_future to ensure that the value we pass to it implements Send. Here, the compiler will also point us to the right place.

在下面的代码 (playground) 中, 我们将保留我们的原始的 async fns, 但是这次我们将使用一个辅助方法 send_static_future 来确保 tSend 的. 在这里, 编译器报错还将指向正确的位置.

use tokio;
use std::future::Future;
use futures::FutureExt;

fn send_static_future<T: Future + Send + 'static>(t: T) -> T {
    t
}

async fn noop() {}

async fn not_send() -> usize {
    let ret = std::rc::Rc::new(2);
    noop().await;
    *ret
}

async fn async_chain() -> usize {
    send_static_future(async move {
        noop().await;
        not_send().await
    }).await
}

fn spawn_async_chain() {
    tokio::spawn(async move {
        let result = async_chain().await;
        println!("{}", result);
    });
}

async fn combinator_chain() -> usize {
    send_static_future(noop().then(|_| not_send())).await
}

fn spawn_combinator_chain() {
    tokio::spawn(async move {
        let result = combinator_chain().await;
        println!("{}", result);
    });
}

#[tokio::main]
async fn main() {
    spawn_combinator_chain();
}

While debugging, you could wrap any part of the async chain with the send_static_future function call until you’ve pinpointed the non-Send part.

在调试时, 您可以使用 send_static_future 方法将异步调用链的任何部分包裹起来, 直到您确定了非 Send 部分.

(This is similar what the static_assertions::assert_impl_all macro creates under the hood – and using that crate is another option.)

(这与 static_assertions::assert_impl_all 宏进行的操作类似, 使用该 crate 是另一个选择)

Identifying Non-Send Stream Combinators | 识别非 SendStream 组合器

译者注: Future 处理单个异步事件. 而 Stream 处理多个异步事件的序列, 通俗地, Stream 即流式处理的一大堆 Futures. 一个极为常见的情形是服务器流式处理客户端传过来的编码过的 HTTP Body, 接收一个数据帧处理一个数据帧.

Since the introduction of async/await, I have mostly stopped using Future combinators. However, combinators still seem like the way to go when working with Streams.

自从引入 async/await 以来, 我基本已不再使用 Future 组合器. 但是, 在处理 Streams 时, 组合器似乎仍然必要的.

Streams present the same DX problems we’ve seen above when you have a combinator that produces a non-Send result.

Streams 显示了前述相同的 DX 问题, 当您有一个产生非 Send 结果的组合器时.

Here’s a simple example (playground link) that demonstrates the same issue we had with Futures above:

这是一个简单的示例, 演示了我们上面 Futures 遇到的相同问题:

#![allow(unused)]
fn main() {
use futures::{pin_mut, stream, Stream, StreamExt};
use std::sync::{Arc, Mutex};

async fn noop() {}

fn stream_processing() -> impl Stream<Item = usize> {
    let state = Arc::new(Mutex::new(0));
    stream::iter(0..100).filter_map(move |i| {
        let state = state.clone();
        async move {
	          // This is contrived but we're intentionally keeping the MutexGuard across the await to make the Future non-Send
            // 这是人为的问题, 我们让 `MutexGuard` 跨越了 .await 界限, 导致这个 `Future` 不再 `Send`.
            // (译者注: `MutexGuard` 活着的时候, 就不能让别的线程上锁, 如果实现 `Send` 随意发送到别的线程就乱套了)
            // (译者注: 当然, tokio::sync::Mutex 通过额外的保证允许你那么干, 代价是性能, 可以看看我上一篇博文的译者注)
            let mut state = state.lock().unwrap();
            noop().await;
            *state += i;
            if *state % 2 == 0 {
                Some(*state)
            } else {
                None
            }
        }
    })
    // (Imagine we had a more complicated stream processing pipeline)
    // (想象我们还有一大堆复杂的流式处理管线/流程)
}

fn spawn_stream_processing() {
    tokio::spawn(async move {
        let stream = stream_processing();
        pin_mut!(stream);
        while let Some(number) = stream.next().await {
            println!("{number}");
        }
    }); // <-- the compiler error points us here
}
}

As with the Futures examples above, we can use the same type of helper function to identify which of our closures is returning a non-Send Future (playground link):

类似地, 我们可以使用相同类型的辅助方法来识别我们的哪些闭包正在返回非 SendFuture (playground):

#![allow(unused)]
fn main() {
use futures::{pin_mut, stream, Future, Stream, StreamExt};
use std::sync::{Arc, Mutex};

async fn noop() {}

fn send_static_future<T: Future + Send + 'static>(t: T) -> T {
    t
}

fn stream_processing() -> impl Stream<Item = usize> {
    let state = Arc::new(Mutex::new(0));
    stream::iter(0..100).filter_map(move |i| {
        send_static_future({
            let state = state.clone();
            async move {
                let mut state = state.lock().unwrap();
                noop().await;
                *state += i;
                if *state % 2 == 0 {
                    Some(*state)
                } else {
                    None
                }
            }
        }) // <-- now the compiler points us here
    })
    // (Imagine we had a more complicated stream processing pipeline)
}

fn spawn_stream_processing() {
    tokio::spawn(async move {
        let stream = stream_processing();
        pin_mut!(stream);
        while let Some(number) = stream.next().await {
            println!("{number}");
        }
    });
}
}

Conclusion | 总结

Async Rust is powerful, but it sometimes comes with the frustrating experience of hunting down the source of trait implementation errors.

异步 Rust 是强大的, 但有时会带来令人沮丧的经历, 即寻找 trait 实现错误的来源.

I ran into this while working on Scour, a personalized content feed. The MVP used a set of sequential async steps to scrape and process feeds. However, that became too slow when the number of feeds grew to the thousands.

我在开发 Scour 时遇到了这个问题, 这是一个个性化内容推送服务. 最初的最小可行产品(MVP)使用了一系列顺序的异步步骤来抓取和处理推送内容. 然而, 当推送内容的数量增长到数千时, 这种方法变得太慢了.

(译者注: 一大堆 .await 连着来和顺序执行没差别, 理论上一个 Future 应尽可能快地返回, 作者的应用场景显然不是, 作者的 MVP 应该是逐个 await 特定方法获取特定内容再组合起来返回, 自然效率不高, 改成 Streams, 让抓取和处理过程流式化即可, 反正不在意获取到各类内容的先后.)

Transitioning to using Streams allows me to take advantage of combinators like flat_map_unordered, which polls nested streams with a configurable level of concurrency. This works well for my use case, but writing the code initially involved plenty of non-Send-Future hunting.

转向使用 Streams 让我可以利用像 flat_map_unordered 这样的组合器. 它可以以可配置的并发级别轮询嵌套流. 这对我的使用场景很有效. 但最初编写代码时涉及大量寻找非 SendFuture.

(译者注: 基本符合我的猜测.)

The techniques described above helped me narrow down why my Stream combinator chains were becoming non-Send. I hope you find them useful as well!

上面描述的技术帮助我缩小了可能是是哪个闭包让我的 Stream 组合器变得非 Send 的范围. 希望您也发现它们也有用!

Thanks to Alex Kesling for mentioning this technique and saving me a couple hours of fighting with rustc.

感谢 Alex Kesling 提到了这项技术, 并为我节省了与 rustc 的数小时搏斗.

(译者注: 太真实了…)

See Also | 参见

If you’re working with Rust streams, you might also want to check out:

如果您正在使用 Rust streams, 您可能需要:

  • async-fn-stream is a nice crate for creating streams using a simpler closure syntax.

    async-fn-stream 是允许您使用更简单的闭合语法创建流的好 crate.

  • pumps is an interesting and different take on Rust streams.

  • argus is an experimental VS Code extension that uses Rust’s New Trait Solver to help you identify why some struct, Future, or Stream does not implement the traits it should.

    argus 是一个实验性的 VSCode 拓展. 它使用 Rust 的新的 trait 求解器来帮助您确定某些结构体/FutureStream 没实现其应有的 traits.


Discuss on r/rust, Lobsters, or Hacker News.

This Week in Rust (TWiR) Rust 语言周刊中文翻译计划, 第 586 期

本文翻译自 Yoshua Wuyts 的博客文章 https://blog.yoshuawuyts.com/a-survey-of-every-iterator-variant, 英文原文版权由原作者所有, 中文翻译版权遵照 CC BY-NC-SA 协议开放. 如原作者有异议请邮箱联系.

相关术语翻译依照 Rust 语言术语中英文对照表.

囿于译者自身水平, 译文虽已力求准确, 但仍可能词不达意, 欢迎批评指正.

2025 年 2 月 17 日晚, 于广州.

GitHub last commit

译者前言: 本文作者梳理了一些标准库中存在或作者希望存在的迭代器, 迭代器作为 Rust 中的重要概念, 值得认真了解. 作者给出的部分设想其实也能以子 trait 的方式自行实现, 不妨将其作为练习; 当然, 大部分设想都需要改动标准库甚至编译器实现, 所以了解其思想即可! 援引作者的一句话: “Iterator is probably the single-most complex trait in the language. It is a junction in the language where every effect, auto-trait, and lifetime feature ends up intersecting.”


A survey of every iterator variant

细数 Rust 那些迭代器 (Iterator)

Introduction

Oh yeah, you like iterators? Name all of them.
哦, 我的老伙计, 你喜欢迭代器吗? 尝试把它们的名字都列出来…

I’m increasingly of the belief that before we can meaningfully discuss the solution space, we have to first make an effort to build consensus on the problem space. It’s hard to plan for a journey together if we don’t know what the trip will be like along the way. Or worse: if we don’t agree in advance where we want the journey to take us.

探讨问题的各种解决方案前, 让我们先探讨问题所在.

In Rust the Iterator trait is the single-most complex trait in the stdlib. It provides 76 methods, and by my estimate (I stopped counting at 120) has around 150 trait implementations in the stdlib alone. It also features a broad range of extension traits like FusedIterator and ExactSizeIterator that provide additional capabilities. And it is itself a trait-based expression of one of Rust’s core control-flow effects; meaning it is at the heart of a lot of interesting questions about how we combine such effects.

在 Rust 中, Iterator trait 是标准库中最为复杂的 trait. 它提供了 76 个方法, 据我估算(数到 120 时我就不数了), 仅在标准库中就有大约 150 个 trait 实现. 它还包含一系列扩展 trait (如 FusedIteratorExactSizeIterator), 这些扩展 trait 提供了额外的方法. 同时, 它本身作为基于 trait 的表达方式, 体现了 Rust 核心控制流机制之一——这意味着它处于许多关于如何组合这些控制流机制的有趣问题的核心位置.

Despite all that we know the Iterator trait falls short today and we would like to extend it to do more. Async iteration is one popular capability people are missing as a built-in. Another is the ability to author address-sensitive (self-referential) iterators. Less in the zeitgeist but equally important are iterators that can lend items with a lifetime, conditionally terminate early, and take external arguments on each call to next.

尽管 Iterator trait 已经如此强大, 但我们知道它目前仍有不足, 并希望扩展其功能. 人们普遍期待 Rust 能内置支持异步迭代器, 以及地址敏感(自引用)迭代器. 此外, 虽然关注度较低但同样重要的功能包括: 能够以一定生命周期借出元素的迭代器、条件提前终止的迭代器, 以及在每次调用 next 方法时接收外部参数的迭代器.

I’m writing this post to enumerate all of the iterator variants in use in Rust today. So that we can scope the problem space to account for all of them. I’m also doing this because I’ve started hearing talk about potentially (soft-)deprecating Iterator. I believe we only get one shot at running such a deprecation1, and if we are going to do one we need to make sure it’s a solution that will plausibly get us through all of our known limitations. Not just one or two.

我撰写此文的目的, 是希望系统梳理当前 Rust 中使用的所有迭代器变体, 从而明确问题范围以涵盖所有可能性. 之所以进行这项梳理, 是因为我近期开始听到关于可能(软性)弃用 Iterator 的讨论. 我相信这类弃用操作只有一次机会1, 并且如果决定这么做, 就必须确保提出的解决方案能够合理突破目前已知的所有限制, 而不仅仅是解决其中一两个问题.

So without further ado: let’s enumerate every variation of the existing Iterator trait we know we want to write, and discuss how those variants interact with each other to create interesting new kinds of iterators.

因此, 事不宜迟: 让我们细数 Iterator trait 现有的各种变体, 并讨论它们如何相互交互以创建有趣的新类型的迭代器.

Base Iterator | 基础的迭代器

Iterator is a trait representing the stateful component of iteration; IntoIterator represents the capability for a type to be iterated over. Here is the Iterator trait as found in the stdlib today. The Iterator and IntoIterator traits are closely linked, so throughout this post we will show both to paint a more complete picture.

Iterator 是一个代表迭代状态组成部分的 trait; IntoIterator 则代表类型具备被迭代的能力. IteratorIntoIterator 是紧密相连的, 本文将充分结合二者叙述.

Throughout this post we will be covering variations on Iterator which provide new capabilities. This trait is represents the absence of those capabilities: it is blocking, cannot be evaluated during compilation, is strictly sequential, and so on. Here is what the foundation of the core::iter submodule looks like today:

在本文中, 我们将探讨 Iterator 的多种变体, 这些变体能够提供新的能力. 当前的 Iterator trait 并不具备这些特性: 它是阻塞式的、无法在编译时求值、严格按顺序执行等. 以下是当前 core::iter 子模块的基础概览:

#![allow(unused)]
fn main() {
/// An iterable type.
/// 能被迭代的类型
pub trait IntoIterator {
    /// The type of elements yielded from the iterator.
    /// 转化为迭代器后, 迭代元素的类型
    type Item;
    /// Which kind of iterator are we turning this into?
    /// 这个类型转化为迭代器后, 迭代器自身的类型
    type IntoIter: Iterator<Item = Self::Item>;
    /// Returns an iterator over the elements in this type.
    /// 将自身转化并返回一个迭代器
    fn into_iter(self) -> Self::IntoIter;
}

/// A type that yields values, one at the time.
/// 一种每次迭代产生一个值的类型
pub trait Iterator {
    /// The type of elements yielded from the iterator.
    /// 迭代元素的类型
    type Item;
    /// Advances the iterator and yields the next value.
    /// 迭代并返回下一个值
    fn next(&mut self) -> Option<Self::Item>;
    /// Returns the bounds on the remaining length of the iterator.
    /// 返回迭代器剩下的长度的范围
    fn size_hint(&self) -> (usize, Option<usize>) { .. }
}
}

While not strictly necessary: the associated IntoIterator::Item type exists for convenience. That way people using the trait can directly specify the Item using impl IntoIterator<Item = Foo>, which is a lot more convenient than I: <IntoIterator::IntoIter as Iterator>::Item, etc.

虽然并非严格必要: 方便起见, 存在关联类型 IntoIterator::Item, 以避免 I: <IntoIterator::IntoIter as Iterator>::Item 的繁杂写法.

Bounded Iterator | 有界迭代器

The base Iterator trait represents a potentially infinite (unbounded) sequence of items. It has a size_hint method that returns how many items the iterator still expects to yield. That value is however not guaranteed to be correct, and is intended to be used for optimizations only. From the docs:

基础 Iterator trait 实际上隐含无限序列的意思. 它具有 size_hint 方法, 返回当前迭代器还期望(但不精确)产生多少元素, 旨在仅用于优化. 文档有言:

Implementation notes It is not enforced that an iterator implementation yields the declared number of elements. A buggy iterator may yield less than the lower bound or more than the upper bound of elements.
实现(本 trait 的)温馨提示 不强制迭代器实现需要产生声明的元素数量, 错误的迭代器实现可能产生数量小于下限或高于上限的元素.
size_hint() is primarily intended to be used for optimizations such as reserving space for the elements of the iterator, but must not be trusted to e.g., omit bounds checks in unsafe code. An incorrect implementation of size_hint() should not lead to memory safety violations.
size_hint() 主要意图为优化, 例如为迭代器的元素保留空间, 但绝对不能盲目信任, 例如在不安全的代码中直接省略界限检查. size_hint() 的不正确实现不应导致内存安全问题.

The Rust stdlib provides two Iterator subtraits that allow it to guarantee it is bounded: ExactSizeIterator (stable) and TrustedLen (unstable). Both traits take Iterator as its super-trait bound and on its surface seem nearly identical. But there is one key difference: TrustedLen is unsafe to implement allowing it to be used to guarantee safety invariants.

Rust 标准库提供了 Iterator 的两个子 trait, 使可以保证其有界: ExactSizeIterator (stable) 和 TrustedLen (nightly). 这两个特征都将 Iterator 作为父 trait, 并且表面上似乎几乎相同. 但是它们有个关键区别: TrustedLen 是 unsafe 的.

#![allow(unused)]
fn main() {
/// An iterator that knows its exact length.
pub trait ExactSizeIterator: Iterator {
    /// Returns the exact remaining length of the iterator.
    fn len(&self) -> usize { .. }
    /// Returns `true` if the iterator is empty.
    fn is_empty(&self) -> bool { .. }
}

/// An iterator that reports an accurate length using `size_hint`.
#[unstable(feature = "trusted_len")]
pub unsafe trait TrustedLen: Iterator {}
}

ExactSizeIterator has the same memory-safety guarantees as Iterator::size_hint meaning: you can’t rely on it to be correct. That means that if you’re say, collecting items from an iterator into a vec, you can’t omit bounds checks and use ExactSizeIterator::len as the input to Vec::set_len. However if TrustedLen is implemented bounds checks can be omitted, since the value returned by size_hint is now a safety invariant of the iterator.

ExactSizeIterator 具有与 Iterator::size_hint 含义相同的内存安全保证: 您不能信任它是正确的. 这意味着如果您将元素从迭代器收集到 Vec 中, 不能省略边界检查而使用 ExactSizeIterator::len 作为 Vec::set_len 的输入, 但还实现了 TrustedLen 的除外(译者注: 这也是为什么这是个 unsafe trait).

Fused Iterator | Fused 迭代器

(译者注: fused 这个概念实在不知道怎么翻译合适, 保留不译. 大意即完成迭代后保证不再有新元素可供迭代. 这个概念在 future -> Stream 里面也有见到.)

When working with an iterator, we typically iterate over items until the iterator yields None at which point we treat the iterator as “done”. However the documentation for Iterator::next includes the following:

使用迭代器时, 我们通常会迭代直到产生 None, 此时我们将迭代器视为 “完成”. 但是, Iterator::next 的文档明确说道:

Returns None when iteration is finished. Individual iterator implementations may choose to resume iteration, and so calling next() again may or may not eventually start returning Some(Item) again at some point.
迭代完成后返回 None. 单个迭代器实现可以选择恢复迭代, 因此再次调用 next() 可能会在某个时候再次开始返回 Some(Item).

It’s rare to work with iterators which yield None and then resume again afterwards, but the iterator trait explicitly allows it. Just like it allows for an iterator to panic if next is called again after None has been yielded once. Luckily the FusedIterator subtrait exists, which guarantees that once None has been yielded once from an iterator, all future calls to next will continue to yield None.

很少见这种情况, 但是确实是允许的. 就像如果 next 返回 None 后再次调用一次 next 时, panic 也是允许的. 幸运的是, 存在 FusedIterator 子 trait, 它可以保证一旦 None 从迭代器中产生一次, 将来对 next 的调用都将继续产生 None.

#![allow(unused)]
fn main() {
/// An iterator that always continues to yield `None` when exhausted.
pub trait FusedIterator: Iterator {}
}

Most iterators in the stdlib implement FusedIterator. For iterators which aren’t fused, it’s possible to call the Iterator::fuse combinator. The stdlib docs recommend never taking a FusedIterator bound, instead favoring Iterator in bounds and calling fuse to guarantee the fused behavior. For iterators that already implement FusedIterator this is considered a no-op.

标准库中的大多数迭代器都实现了 FusedIterator. 对于非 Fused 的迭代器, 可以调用 Iterator::fuse 组合器. 标准库文档不建议引入 FusedIterator 的约束, 而应该对 Iterator 调用 fuse 方法来保证迭代是 fused 的(返回 None 后就再也不会返回 Some(Item) 了). 对于已经实现 FusedIterator 的迭代器而言, 这被认为是一个空操作.

The FusedIterator design works because Option::None is idempotent: there is never a scenario where we can’t create a new instance of None. Contrast this with enums such as Poll that lack a “done” state - and you see ecosystem traits such as FusedFuture attempting to add this lack of expressivity back through other means. The need for an idempotent “done” state will be important to keep in mind as we explore other iterator variants throughout this post.

FusedIterator 的设计之所以起作用, 是因为 Option::None 是幂等的(译者注: 大意即不管 Option::<T>::NoneT 是什么, None 就是 None): 无论调用多少次 next 方法, 一旦迭代器终止, 我们永远不可能遇到无法创建新的 None 实例的情况. 与此形成对比的是像 Poll 这样的枚举类型——它们缺乏明确的 “完成” 状态, 因此生态系统需要通过 FusedFuture 等 trait 以其他方式补足这种表达能力. 幂等的 “完成” 状态需求在本文后续探讨其他迭代器变体时尤为重要, 需要时刻牢记.

Thread-Safe Iterator | 线程安全的迭代器

Where bounded and fused iterators can be obtained by refining the Iterator trait using subtraits, thread-safe iterators are obtained by composing the Send and Sync auto-traits with the Iterator trait. That means there is no need for dedicated SendIterator or SyncIterator traits. A “thread-safe iterator” becomes a composition of Iterator and Send / Sync:

区别于需要使用子 trait 完善 Iterator trait 来获得有界或 fused 的迭代器, SendSync 作为 auto trait, 意味着不需要专门的如 SendIteratorSyncIterator 般的子 trait, “线程安全的迭代器” 即 Iterator + Send / Sync:

#![allow(unused)]
fn main() {
struct Cat;

// Manual `Iterator` impl
impl Iterator for Cat { .. }

// These impls are implied by `Send`
// and `Sync` being auto-traits
unsafe impl Send for Cat {}
unsafe impl Sync for Cat {}
}

And when taking impls in bounds, we can again express our intent by composing traits in bounds. I’ve made the argument before that taking Iterator directly in bounds is rarely what people actually want, so the bound would end up looking like this:

落实到应用上, trait 约束看起来像这样:

#![allow(unused)]
fn main() {
fn thread_safe_sink(iter: impl IntoIterator + Send) { .. }
}

If we also want the individual items yielded by the iterator to be marked thread-safe, we have to add additional bounds:

如果我们还希望迭代器所产生的元素是线程安全的, 还需要对 Item 约束:

#![allow(unused)]
fn main() {
fn thread_safe_sink<I>(iter: I)
where
    I: IntoIterator<Item: Send> + Send,
{ .. }
}

While there is no lack of expressivity here, at times bounds like these can get rather verbose. That should not be taken as indictment of the system itself, but rather as a challenge to improve the ergonomics for the most common cases.

尽管不缺乏表现力, 这里的 trait 约束偶尔会相当繁杂. 当然这不应将其视为对实现本身的控诉, 而应将其作为改善最常见情况的人体工程学的挑战.

(译者注: 遇到线程安全的实现, Send + Sync 的约束确实会满天飞. 引入它们也是为了内存安全着想, 我认为是比较优秀的设计, 复杂一点可以接受.)

Dyn-Compatible Iterator | dyn 兼容的迭代器

(译者注: 之前叫 object safe, 但容易引起误解, 现在改说法了, 参见 https://github.com/rust-lang/lang-team/issues/286)

Dyn-compatibility is another axis that traits are divided on. Unlike e.g. thread-safety, dyn-compatibility is an inherent part of the trait and is governed by Sized bounds. Luckily both the Iterator and IntoIterator traits are inherently dyn-compatible. That means they can be used to create trait objects using the dyn keyword:

dyn 兼容性是 trait 的又一特征. 不像线程安全性, dyn 兼容性是 trait 的固有部分, 遵循 Sized 约束. 幸运的是, IteratorIntoIterator 的特征本质上都是 dyn 兼容的. 这意味着它们可以使用 dyn 关键字来创建 trait 对象:

#![allow(unused)]
fn main() {
struct Cat;
impl Iterator for Cat { .. }

let cat = Cat {};
let dyn_cat: &dyn Iterator = &cat; // ok
}

There are some iterator combinators such as count that take an additional Self: Sized bound2. But because trait objects are themselves sized, it all mostly works as expected:

有一些方法(例如 count) 需要额外的 Self: Sized 约束2, 但多数都满足.

#![allow(unused)]
fn main() {
let mut cat = Cat {};
let dyn_cat: &mut dyn Iterator = &mut cat;
assert_eq!(dyn_cat.count(), 1); // ok
}

Double-Ended Iterator | 双端迭代器

(译者注: 也可译作 双向迭代器, 但 VecDeque 常译作双端队列, 故此处译为 双端迭代器)

Often times iterators over collections hold all data in memory, and can be traversed in either direction. For this purpose Rust provides the DoubleEndedIterator trait. Where Iterator builds on the next method, DoubleEndedIterator builds on a next_back method. This allows items to be taken from both the logical start and end of the iterator. And once both cursors meet, iteration is considered over.

通常, 对集合 (collection) 的迭代器将所有数据保存在内存中, 并且可以沿任一方向遍历. 为此, Rust 提供了 DoubleEndedIterator trait. 其中 Iterator 提供 next 方法, DoubleEndedIterator 提供 next_back 方法. 这允许从迭代器的逻辑始端和末端获取元素. 一旦两个光标相遇, 迭代就会结束.

#![allow(unused)]
fn main() {
/// An iterator able to yield elements from both ends.
pub trait DoubleEndedIterator: Iterator {
    /// Removes and  an element from the end of the iterator.
    fn next_back(&mut self) -> Option<Self::Item>;
}
}

While you’d expect this trait to be implemented for e.g. VecDeque, it’s interesting to note that it’s also implemented for Vec, String, and other collections that only grow in one direction. Also unlike some of the other iterator refinements we’ve seen, DoubleEndedIterator has a required method that is used as the basis for several new methods such as rfold (reverse fold) and rfind (reverse find).

VecDeque 是意料之中的实现此 trait 的类型, 但有趣的是, Vec, String 或其他本仅在一个方向增长的集合也实现了此 trait. 与我们前面所述的迭代器不同, DoubleEndedIterator 有一必需的方法 (译者注: required method, 即需要你实现没有默认实现的方法, 后者称 provided method), 该方法被用作几种新方法的基础, 例如 rfold (反向 fold, reverse fold) 和 rfind (反向寻找, reverse find).

Seeking Iterator | 定位迭代器

Both the Iterator and Read traits in Rust provide abstractions for streaming iteration. The main difference is that Iterator works by returning arbitrary owned types when calling next, where Read is limited to reading bytes into buffers. But both abstractions keep a cursor that keeps track of which data has been processed already, and which data still needs processing.

Rust 中的 IteratorRead 特征都为流的迭代提供了抽象. 主要区别在于, Iterator 在调用 next 方法返回的是 owned 的类型 (译者注: owned / borrowed 是 Rust 所有权特有的概念, 不译), 而 Read 仅限于从缓冲区读取字节. 但是, 这两个抽象都保持了一个光标 (cursor), 以跟踪已经处理了哪些数据, 并且哪些数据仍需要处理.

But not all streams are created equally. When we read data from a regular file3 on disk we can be sure that, as long as no writes occurred, we can read the same file again and get the same output. That same guarantee is however not true for sockets, where once we’ve read data from it we can’t read that same data again. In Rust this distinction is surfaced via the Seek trait, which gives control over the Read cursor in types that support it.

但是, 各类流的特性并不是一致的. 当我们读取磁盘上的常规文件时3, 我们可以断言, 只要没有写入, 再次读取会获得相同的输出. 但从网络套接字读取字节流时并没有类似的保证, 一旦我们从中读取了数据, 我们就无法再次读取相同的数据 (译者注: 数据会从网络栈的底层缓冲区移出. 当然你也可以使用 MSG_PEEK 操作, 这就是后话了). 在 Rust 中, 这种区别通过 Seek trait 表现出来, 其提供了一个可以在字节流中移动的光标.

In Rust the Iterator trait provides no mechanism to control the underlying cursor, despite its similarities to Read. A language that does provide an abstraction for this is C++ in the form of random_access_iterator. This is a C++ concept (trait-alike) that further refines bidirectional_iterator. My C++ knowledge is limited, so I’d rather quote the docs directly than attempt to paraphrase:

在 Rust 中, 尽管 Iterator trait 与 Read 相似, 但没有提供控制光标的机制. 确实为此提供抽象的语言是 C++, 以 random_access_iterator 的形式. 这是一个 C++ 中进一步完善 bidirectional_iterator 的 concept (类似 trait). 我的 C++ 知识是有限的, 因此我宁愿直接引用文档而不是试图解释:

[…] random_access_iterator refines bidirectional_iterator by adding support for constant time advancement with the +=, +, -=, and - operators, constant time computation of distance with -, and array notation with subscripting [].
[…] random_access_iterator (随机访问迭代器) 在 bidirectional_iterator (双向迭代器) 的基础上进一步改进, 新增了对以下功能的支持: 通过 +=+-=- 运算符实现常数时间内的迭代器(光标)移动; 通过 - 运算符以常数时间计算迭代器间的距离; 通过下标运算符 [] 实现类似数组的随机访问.

Being able to directly control the cursor in Iterator implementations could prove useful when working with in-memory collection types like Vec, as well as when working with remote objects such as paginated API endpoints. The obvious starting point for such a trait would be to mirror the existing io::Seek trait and adapt it to be a subtrait of Iterator:

能够直接控制 Iterator 中的光标在处理数据保存于内存的集合 (如 Vec) 以及远程对象(例如可分页的 API endpoints)时相当实用. 实现这个 trait 的起点明显是镜像现有的 io::Seek trait, 并将其改造为为 Iterator 的子 trait:

#![allow(unused)]
fn main() {
/// Enumeration of possible methods to seek within an iterator.
pub enum SeekFrom {
    /// Sets the offset to the specified index.
    Start(usize),
    /// Sets the offset to the size of this object plus the specified index.
    End(isize),
    /// Sets the offset to the current position plus the specified index.
    Current(isize),
}

/// An iterator with a cursor which can be moved.
pub trait SeekingIterator: Iterator {
    /// Seek to an offset in an iterator.
    fn seek(&mut self, pos: SeekFrom) -> Result<usize>;
}
}

Compile-Time Iterator | 编译时迭代器

In Rust we can use const {} blocks to execute code during compilation. Only const fn functions can be called from const {} blocks. const fn free functions and methods are stable, but const fn trait methods are not. This means that traits like Iterator are not yet callable from const {} blocks, and so neither are for..in expressions.

在 Rust 中, 我们可以使用 const {} 块在编译时执行代码. const {} 块只能调用 const 方法, 然而 const_trait_impl feature 仍不稳定. 这意味着 Iterator 之类的 trait 的方法不能在 const {} 块中调用, 自然 for..in 表达式也不行 (译者注: 不过有个 const_for 的三方库).

We know we want to support iteration in const {} blocks, but we don’t yet know how we want to spell both the trait declaration and trait bounds. The most interesting open question here is how we will end up passing the Destruct trait around, which is necessary to enable types to be droppable in const contexts. This leads to additional questions around whether const trait bounds should imply const Destruct. And whether the const annotation should be part of the trait declaration, the individual methods, or perhaps both.

我们需要支持在 const {} 块中进行迭代操作, 但目前尚未确定如何设计 trait 声明 (trait declaration) 和 trait 约束 (trait bounds) 的具体语法. 当前最关键的开放性问题在于如何传递 Destruct trait, 这是确保类型在常量上下文中可被丢弃 (droppable) 的必要条件. 这进一步引发了以下讨论:

  • const trait 约束 (const trait bounds) 是否应隐式包含 const Destruct.
  • const 是应作为 trait 声明的一部分、单个方法的修饰, 还是需要同时出现在两者中.

This post is not the right venue to discuss all tradeoffs. But to give a sense of what a compile-time-compatible Iterator trait might look like: here is a variant where both the trait and individual methods are annotated with const:

这篇文章不讨论各类权衡. 但是我们还是给出 const compatible 的 Iterator 可能的样子, 特征和单个方法都是 const 的:

(译者注: 相当破坏性了, 那我们手动实现的 Iterator 没办法让方法 const 怎么办… 但是最关键的还是那几个方法能不能 const… 总不能 optional const 吧, 或者妥协一点, ConstIterator 的玩意…)

#![allow(unused)]
fn main() {
pub const trait IntoIterator {                        // ← `const`
    type Item;
    type IntoIter: const Iterator<Item = Self::Item>; // ← `const`
    const fn into_iter(self) -> Self::IntoIter;      // ← `const`
}

pub const trait Iterator {                                     // ← `const`
    type Item;
    const fn next(&mut self) -> Option<Self::Item>;           // ← `const`
    const fn size_hint(&self) -> (usize, Option<usize>) { .. } // ← `const`
}
}

Lending Iterator | 借用迭代器

While there are plenty of iterators in the stdlib today that yield references to items, the values that are being referenced are never owned by the iterator itself. In order to write iterators which owns the items it yields and yields them by-reference, the associated item type in iterator needs a lifetime:

尽管当今标准库中有很多迭代器会引起对父体元素的引用, 但所引用的元素从未由迭代器本身所有. 为了编写拥有一定量元素并返回其引用的迭代器, Iterator trait 中的关联类型需要生命周期:

#![allow(unused)]
fn main() {
pub trait IntoIterator {
    type Item<'a>                                               // ← Lifetime
    where
        Self: 'a;                                              // ← Bound
    type IntoIter: for<'a> Iterator<Item<'a> = Self::Item<'a>>; // ← Lifetime
    fn into_iter(self) -> Self::IntoIter;
}

pub trait Iterator {
    type Item<'a>                                 // ← Lifetime
    where
        Self: 'a;                                // ← Bound
    fn next(&mut self) -> Option<Self::Item<'_>>; // ← Lifetime
    fn size_hint(&self) -> (usize, Option<usize>) { .. }
}
}

There has been talk about adding a ’self lifetime to the language as a shorthand for where Self:’a. But even with that addition this set of signatures is not for the faint of heart. The reason for that is that it makes use of GATs, which are both useful and powerful — but are for all intents and purposes an expert-level type-system feature, and can be a little tricky to reason about.

关于在语言中添加 'self 生命周期 (作为 where Self: 'a 的简写形式) 已有过一些讨论. 但即使这样, 其函数签名也并非轻易可以驾驭, 原因在于它们使用了泛型关联类型 (GATs): 虽然功能强大且实用, 但从本质上说属于专家级的类型系统功能, 其逻辑推理可能需要一定的技巧才能掌握.

Lending iteration is also going to be important when we add dedicated syntax to construct iterators using the yield keyword. The following example shows a gen {} block which creates a string and then yields a reference to it. This perfectly4 matches onto the Iterator trait we’ve defined:

当我们使用 yield 关键字构建迭代器时, 借用迭代也将很重要. 下面的示例给出一个 gen {} 块(译者注: 产生迭代器的新语法, 暂且称生成器), 该块创建了一个字符串, 然后返回其引用, 完美4匹配我们定义的 Iterator trait:

#![allow(unused)]
fn main() {
let iter = gen {
    let name = String::from("Chashu");
    yield &name; // ← Borrows a local value stored on the heap.
};
}

Iterator with a Return Value | 带返回值的迭代器

The current Iterator trait has an associated type Item, which maps to the yield keyword in Rust. But it has no associated type that maps to the return keyword. A way to think about iterators today is that their return type is hard-coded to unit. If we want to enable generator functions and blocks to be written which can not just yield, but also return, we’ll need some way to express that.

当前 Iterator trait 有个关联类型 Item, 该类型映射到 yield 关键字. 但是没有能映射到 return 关键字的关联类型. 目前可以认为, 它们的返回类型被硬编码为 unit. 如果我们需要在生成器内不仅可以 yield 还可以 return, 我们将需要某种方法来表达这一点:

#![allow(unused)]
fn main() {
let counting_iter = gen {
    let mut total = 0;
    for item in iter {
        total += 1;
        yield item;     // ← Yields one type.
    }
    total                // ← Returns another type.
};
}

The obvious way to write a trait for “an iterator which can return” is to give Iterator an additional associated item Output which maps to the logical return value. In order to be able to express fuse semantics, the function next needs to be able to return three different states:

为实现 “可以返回的迭代器”, 显然可以给 Iterator 添加一个叫 Output 的关联类型(译者注: 而且并不是破坏性的操作, 给定默认就是 unit 类型就可以), 映射到逻辑返回值. 为了能够表达融合语义, next 方法需要能够返回三个不同的状态:

  • Yielding the associated type Item

    产生 Item

  • Returning the associated type Output

    返回 Output

  • The iterator has been exhausted (done)

    迭代器已经用尽(完成)

One way to do this would be for next to return Option<ControlFlow>, where Some(Continue) maps to yield, Some(Break) maps to return, and None maps to done. Without a final “done” state, calling next again on an iterator after it’s finished yielding values would likely always have to panic. This is what most futures do in async Rust, and is going to be a problem if we ever want to guarantee panic-freedom.

做到这一点的一种方法是返回 Option<ControlFlow>, 其中 Some(Continue) 对应 yield, Some(Break) 对应 return, None 对应完成. 如果没有最终的 “完成” 状态, 在完成后再次调用 next 存在总是会 panic 的情况. 这就是多数异步 Rust 实现中所做的事情, 如果我们想保证尽量不要 panic, 这将是一个问题.

#![allow(unused)]
fn main() {
pub trait IntoIterator {
    type Output;                                           // ← Output
    type Item;
    type IntoIter:
        Iterator<Output = Self::Output, Item = Self::Item>; // ← Output
    fn into_iter(self) -> Self::IntoIter;
}

pub trait Iterator {
    type Output;                                          // ← Output
    type Item;
    fn next(&mut self)
        -> Option<ControlFlow<Self::Output, Self::Item>>; // ← Output
    fn size_hint(&self) -> (usize, Option<usize>) { .. }
}
}

The way this would work on the caller side and in combinators is quite interesting. Starting with the provided for_each method: it will want to return Iterator::Output rather than () once the iterator has completed. Crucially the closure F provided to for_each only operates on Self::Output, and has no knowledge of Self::Output. Because if the closure did have direct knowledge of Output, it could short-circuit by returning Output sooner than expected which is a different kind of iterator than having a return value.

这种方法在调用方和组合器中的工作方式非常有趣. 从 for_each (provided method)开始: 它希望在迭代器完成后返回 Iterator::Output 而不是 (). 关键是, 提供给 for_each 的闭包 F 仅对 Self::Output 进行操作, 并且对 Self::Output 没有直接的关系. 因为如果闭包确实对 Output 有直接关系, 它可能会通过比预期更早地返回 Output 来短路返回, 这与具有返回值的迭代器是不同类型的迭代器.

#![allow(unused)]
fn main() {
fn for_each<F>(self, f: F) -> Self::Output // ← Output
where
    Self: Sized,
    F: FnMut(Self::Item),
{ .. }
}

If we were to transpose “iterator with return value” to for..in things get even more interesting still. In Rust loop expressions can themselves evaluate to non-unit types by calling break with some value. for..in expressions cannot do this in the general case yet, except for the handling of errors using ?. But it’s not hard to see how this could be made to work, conceptually this is equivalent to calling Iterator::try_for_each and returning Some(Break(value)):

如果我们要将 “带有返回值的迭代器” 转换为 for...in, 那么事情就会变得更加有趣. 在 Rust loop 中, 可以通过以某种方式调用 break, 藉由推断返回值类型. 但在一般情况下, for..in 表达式无法做到这一点, 除了使用 ? 处理错误. 但是, 不难看出如何处理这种情况, 从概念上讲, 这等同于调用 Iterator::try_for_each 和返回 Some(Break(value)):

#![allow(unused)]
fn main() {
let ty: u32 = for item in iter {  // ← Evaluates to a `u32`
    break 12u32                   // ← Calls `break` from the loop body
};
}

Assuming we have an Iterator with its own return value, that would mean for..in expressions would be able to evaluate to non-unit return types without calling break from within the loop’s body:

假设我们有一个具有明确返回值的 Iterator, 那意味着 for..in 表达式将能够推断非 Unit 的返回类型, 而无需从循环体中调用 break:

#![allow(unused)]
fn main() {
let ty: u32 = for item in iter {  // ← Evaluates to a `u32`
    dbg!(item);                  // ← Doesn't `break` from the loop body
};
}

(译者注: 那如上面的例子, 返回啥玩意呢…)

This of course leads to questions about how to combine an “iterator with a return value” and “using break from inside a for..in expression”. I’ll leave that as an exercise to the reader on how to spell out (I’m certain it can be done, I just think it’s a fun one). Generalizing all modes of early returns from for..in expressions to invocations of for_each combinators is an interesting challenge that we’ll cover in more detail later on when we discuss short-circuiting (fallible) iterators.

当然, 这会导致有关如何将 “具有返回值的迭代器” “和 “从 for...in 表达式内部使用 break” 相结合的问题. 我将把它作为一个练习, 以使读者了解如何实现这点(我敢肯定可以做到这一点, 我只是认为这是一个有趣的事情). 将 for...in 表达式的所有早期返回模式概括为 for_each 组合器的调用是一个有趣的挑战, 我们稍后将在讨论可短路(可失败)迭代器时更详细地介绍.

Iterator with a Next Argument | 带有下一个参数的迭代器

In generator blocks the yield keyword can be used to repeatedly yield values from an iterator. But what if the caller doesn’t just want to obtain values, but also pass new values back into the iterator? That would require for yield to be able evaluate to a non-unit type. Iterators that have this functionality are often referred to as “coroutines”, and they are being particularly useful when implementing I/O-Free Protocols.

在生成器块中, yield 关键字可用于从迭代器中反复产生值. 但是, 如果调用者不仅想获得值, 还想将新值传递回迭代器怎么办? 这需要 yield 能够推断为非单元类型. 具有此功能的迭代器通常被称为 “coroutines”, 在实现 I/O 无关的协议时, 它们特别有用.

#![allow(unused)]
fn main() {
/// Some RPC protocol
enum Proto {
    /// Some protocol state
    MsgLen(u32),
}

let rpc_handler = gen {
    let len = message.len();
    let next_state = yield Proto::MsgLen(len); // ← `yield` evaluates to a value
    ..
};
}

In order to support this, Iterator::next needs to be able to take an additional argument in the form of a new associated type Args. This associated type has the same name as the input arguments to Fn and AsyncFn. If “iterator with a next argument” can be thought of as representing a “coroutine”, the Fn family of traits can be thought of as representing a regular “routine” (function).

为了支持这一点, 需要引入新的关联类型 Args 作为 Iterator::next 方法的参数. 此关联类型的名称与 FnAsyncFn 的参数相同. 如果可以将 “具有下一个参数的迭代器” 视为代表了 “coroutine”, 则可以将 Fn trait 家族视为代表常规的 “routine” (函数).

#![allow(unused)]
fn main() {
pub trait IntoIterator {
    type Item;
    type Args;                                         // ← Args
    type IntoIter:
        Iterator<Item = Self::Item, Args = Self::Args>; // ← Args
    fn into_iter(self) -> Self::IntoIter;
}

pub trait Iterator {
    type Item;
    type Args;                                                 // ← Args
    fn next(&mut self, args: Self::Args) -> Option<Self::Item>; // ← Args
    fn size_hint(&self) -> (usize, Option<usize>) { .. }
}
}

Here too it’s interesting to consider the caller side. Starting with a hypothetical for_each function: it would need to be able to take an initial value passed to next, and then from the closure be able to return additional values passed to subsequent invocations of next. The signature for that would look like this:

在这里, 考虑调用者方面也很有趣. 从假设的 for_each 函数开始: 它需要能够将初始值传递到 next, 然后从闭包中可以返回传递给 next 的后续调用的其他值. 签名看起来像这样:

#![allow(unused)]
fn main() {
fn for_each<F>(self, args: Self::Args, f: F) // ← Args
where
    Self: Sized,
    F: FnMut(Self::Item) -> Self::Args,      // ← Args
{ .. }
}

To better illustrate how this would work and build up some intuition, consider manual calls to next inside of a loop. We would start by constructing some initial state that is passed to the first invocation of next. This will produce an item that can be used to construct the next argument to next. And so on, until the iterator has no more items left to yield:

为了更好地说明工作原理并建立一些直觉, 假设我们将在循环内部手动调用 next 的例子, 我们将构建第一次调用 next 时传递的初始参数, 并处理 next 返回的 item 作为下一次调用 next 的参数. 依此类推, 直到迭代器没有更多的物品可以产生:

#![allow(unused)]
fn main() {
let mut iter = some_value.into_iter();
let mut arg = some_initial_value;

// loop as long as there are items to yield
while let Some(item) = iter.next(arg) {
    // use `item` and compute the next `arg`
    arg = process_item(item);
}
}

If we were to iterate over an iterator with a next function using a for..in expression, the equivalent of returning a value from a closure would be either to continue with a value. This could potentially also be the final expression in the loop body, which you can think of itself being an implied continue today. The only question remaining is how to pass initial values on loop construction, but that mainly seems like an exercise in syntax design:

如果我们要使用 for...in 表达式来遍历带有 next 方法的迭代器, 从闭包返回值的等价操作可能是通过 continue 携带值来实现. 这也可能成为循环体中的最终表达式, 你可以将其视为当前隐含的 continue 操作. 剩下的唯一问题是如何在循环构造时传递初始值, 但这似乎主要是语法设计的范畴:

#![allow(unused)]
fn main() {
// Passing a value to the `next` function seems like
// it would logically map to `continue`-expressions.
// (passing initial state to the loop intentionally omitted)
for item in iter {
    continue process_item(item);  // ← `continue` with value
};

// You can think of `for..in` expressions as having
// an implied `continue ()` at the end. Like functions
// have an implied `return ()`. What if that could take
// a value?
// (passing initial state to the loop intentionally omitted)
for item in iter {
    process_item(item)             // ← `continue` with value
};
}

“iterator with return value” and “iterator with next argument” feel like they map particularly well to break and continue. I think of them as duals, enabling both expressions to carry non-unit types. This feels like it might be an important insight that I haven’t seen anyone bring up before.

感觉 “带有返回值的迭代器” 和 “带有下一个参数的迭代器” 相当能映射到 break 和 continue. 我认为它们是伴生的, 使两种表达式都可以携带非 Unit 类型. 这感觉可能是我以前从未见过的重要见解.

Being able to pass a value to next is one of the hallmark features of the Coroutine trait in the stdlib. However unlike the trait sketch we provided here, in Coroutine the type of the value passed to next is defined as a generic param on the trait rather than an associated type. Presumably that is so that Coroutine can have multiple implementations on the same type that depend on the input type. I searched whether this was actually being used today, and it doesn’t seem to be. Which is why I suspect it is likely fine to use an associated type for the input arguments.

能够向 next 方法传递值是标准库中 Coroutine trait 的标志性特征之一. 但与本文提出的特性设计草案不同, 在标准库的 Coroutine 中, 传递给 next 方法的值的类型被定义为特性上的泛型参数 (generic param), 而非关联类型 (associated type). 推测这样设计是为了允许在同一个类型上根据输入类型的不同, 实现多个不同的协程变体. 但经过调研, 目前实际应用中似乎并没有这样的使用场景. 基于此, 我们有理由认为将输入参数的类型定义为关联类型是可行的方案.

Short-Circuiting Iterator | 可短路迭代器

While it is possible to return Try-types such as Result and Option from an iterator today, it isn’t yet possible is to immediately stop execution in the case of an error 5. This behavior is typically referred to as “short-circuiting”: halting normal operation and triggering an exceptional state (like an electrical breaker would in a building).

虽然现在可以从迭代器中返回 Try 类型, 例如 ResultOption, 但在发生错误5的情况下, 不可能立即停止执行. 这种行为通常称为 “短路”: 停止正常操作并触发特殊状态(就像建筑物中的电气断路器一样).

In unstable Rust we have try {} blocks that rely on the unstable Try trait, but we don’t yet have try fn functions. If we want these to function in traits the way we’d want them to, they will have to desugar to impl Try. Rather than speculate about what a potential try fn syntax might look like in the future, we’ll be writing our samples using -> impl Try directly. Here is what the Try (and FromResidual) traits look like today:

在 unstable Rust 中, 我们有 try {} 块, 其依赖于尚未稳定的 Try trait, 但我们还没有 try fn 功能. 如果我们希望这些以我们希望它们的方式在 trait 上发挥作用, 则将不得不将其解糖为 impl Try. 我们将直接使用 -> impl Try 撰写例子, 而不是推测潜在的 try fn 语法可能是什么样的.

这是 Try (和 FromResidual) trait 现在的样子:

#![allow(unused)]
fn main() {
/// The `?` operator and `try {}` blocks.
pub trait Try: FromResidual {
    /// The type of the value produced by `?`
    /// when _not_ short-circuiting.
    type Output;
    
    /// The type of the value passed to `FromResidual::from_residual`
    /// as part of ? when short-circuiting.
    type Residual;
    
    /// Constructs the type from its `Output` type.
    fn from_output(output: Self::Output) -> Self;

    /// Used in ? to decide whether the operator should produce
    /// a value ( or propagate a value back to the caller.    
    fn branch(self) -> ControlFlow<Self::Residual, Self::Output>;
}

/// Used to specify which residuals can be converted
/// into which `Try` types.
pub trait FromResidual<R = <Self as Try>::Residual> {
    /// Constructs the type from a compatible `Residual` type.
    fn from_residual(residual: R) -> Self;
}
}

You can think of a short-circuiting iterator as a special case of an “iterator with return value”. In its base form, it will only return early in case of an exception while its logical return type remains hard-coded to unit. The return type of fn next should be an impl Try returning an Option, with the value of Residual set to the associated Residual type. This allows all combinators to share the same Residual, enabling types to flow.

您可以将可短路迭代器视为 “带有返回值的迭代器” 的特殊情况: 只有在错误的情况下才能提早返回, 而其逻辑返回类型仍将硬编码为 Unit. next 的返回类型应当 impl Try, 实际返回 Option, 并设置关联类型为 Residual. 这允许组合器共享相同的 Residual:

#![allow(unused)]
fn main() {
pub trait IntoIterator {
    type Item;
    type Residual;
    type IntoIter: Iterator<
        Item = Self::Item,
        Residual = Residual   // ← Residual
    >;
    fn into_iter(self) -> Self::IntoIter;
}

pub trait Iterator {
    type Item;
    type Residual;                  // ← Residual
    fn next(&mut self) -> impl Try<  // ← impl Try
        Output = Option<Self::Item>,
        Residual = Self::Residual,   // ← Residual
    >;
}
}

If we once again consider the caller, we’ll want to provide a way for for..in expressions to short-circuit. What’s interesting here is that the base iterator trait already provides a try_for_each, method. The difference between that method and the for_each we’re about to see is how the type of Residual is obtained. In try_for_each the value is local to the method, while if the trait itself is “short-circuiting” the type of Residual is determined by the associated Self::Residual type. Or put differently: in a short-circuiting iterator the type we short-circuit with is a property of the trait rather than a property of the method.

如果我们再次考虑调用者, 我们需要为 for...in 表达式提供一种短路机制. 这里有趣的是, 基础的 Iterator trait已经提供了 try_for_each 方法. 该方法与我们即将看到的 for_each 之间的区别在于 Residual 类型的获取方式:

  • try_for_each 中, 该值的类型是方法的局部变量
  • 而当 trait 本身具有 “短路” 特性时, Residual 的类型由关联的 Self::Residual 类型决定

换句话说: 在短路迭代器中, 我们用于短路的类型是 trait 本身的属性, 而不是方法的属性. 这体现了迭代器 trait 设计中控制流抽象的层级差异——将短路类型提升为 trait的关联类型, 使得短路行为成为迭代器的固有特性而非临时方法参数.

#![allow(unused)]
fn main() {
fn for_each<F, R>(self, f: F) -> R                  // ← Return type
where
    Self: Sized,
    F: FnMut(Self::Item) -> R,                      // ← Return type
    R: Try<Output = (), Residual = Self::Residual>, // ← `impl Try`
{ .. }
}

As mentioned earlier on in this post: the interaction between “iterator with a return type” and “short-circuiting iterator” is an interesting one. Returning Option<ControlFlow> from fn next is able to encode three distinct states, but this combination of capabilities requires us to encode four states:

如本文中前面提到的那样: “带返回值的迭代器” 和 “短路迭代器” 之间的相互关系相当有趣. 从 next 返回 Option<ControlFlow> 可以编码三个不同的状态, 但是此时要求我们编码四个状态:

  • yield a next item 产生 下一个值
  • break with a residual 带残值 中止
  • return a final output 返回 最终输出
  • iterator done (idempotent) 迭代器完成 (幂等的)

The reason why we want to be able to encode a signature like this is because when writing gen fn functions it is entirely reasonable to want to have a return type, short-circuit on error using ?, and also yield values. This works like regular functions today, but with the added capability to invoke yield. The naive way of encoding this would be to return an impl Try of Option<ControlFlow<_>> with distinct associated types for Item, Output, and Residual. This does however feel like it is starting to get a little out of hand, though perhaps a first-class try fn notation might provide some relief.

我们希望支持此类签名的原因在于, 当编写生成器时, 完全有理由需要同时满足以下三个需求:

  1. 定义返回类型
  2. 使用 ? 操作符进行错误短路
  3. 通过 yield 产生值

这与普通函数的工作方式类似, 但额外增加了调用 yield 的能力. 最直观的实现方式可能是返回一个 impl Try<Option<ControlFlow<_>>>, 并为ItemOutputResidual 分别定义不同的关联类型. 然而这种方式开始显得过于复杂, 或许引入原生支持的 try fn 语法能有效简化这种设计.

#![allow(unused)]
fn main() {
pub trait IntoIterator {
    type Item;
    type Output;                          // ← `Output` 
    type Residual;                        // ← `Residual` 
    type IntoIterator: Iter<
        Item = Self::Item,
        Residual = Self::Residual,         // ← `Residual` 
        Output = Self::Output,             // ← `Output` 
    >;
    fn into_iter(self) -> Self::IntoIter;
}

pub trait Iterator {
    type Item;
    type Output;                    // ← `Output`
    type Residual;                  // ← `Residual`
    fn next(&mut self) -> impl Try<  // ← `impl Try` 
        Output = Option<ControlFlow< // ← `ControlFlow
            Self::Output,            // ← `Output
            Self::Item,
        >>,
        Residual = Self::Residual,   // ← `Residual` 
    >;
}
}

Address-Sensitive Iterator | 地址敏感的迭代器

Rust’s generator transformation may create self-referential types. That is: types which have fields that borrow from other fields on the same type. We call these types “address-sensitive” because once the type has been constructed, its address in memory must remain stable. This comes up when writing gen {} blocks that have stack-allocated locals 6 that are kept live across yield-expressions. What is or isn’t a “stack-allocated local” can get a little complicated. But it’s important to highlight that for example calling IntoIterator::into_iter on a type and re-yielding all items is something that just works (playground):

Rust 生成器可能会产生自引用的类型, 即具有借用自身其他字段的字段的类型, 我们称之地址敏感, 即一旦构造, 其内存地址必须保持稳定 (译者注: 不能 move 或者以任何形式譬如 mem::swap 替换为新值, 否则此时旧值仍持有对原字段内存位置的引用, 会导致安全问题). 当编写带有于堆分配的局部变量6gen {} 块时, 会出现这种情况. 关于是或不是 “堆分配的局部变量” 的问题会有些复杂. 但是, 重要的是要强调, 例如, 在类型上调用 IntoIterator::into_iter 并 yield 其元素是可行的(playground):

#![allow(unused)]
fn main() {
// This example works today
let iter = gen {
    let cat_iter = cats.into_iter();
    for cat in cat_iter {
        yield cat;
    }
};
}

And to give a sense of what for example does not work, here is one of the samples Tmandry (T-Lang) collected. This creates an intermediate borrow, which results in the error: “Borrow may still be in use when gen fn body yields” (playground):

什么情况下不可以呢? 这里有一示例. 这会产生一个中间借用, 这导致错误: “borrow may still be in use when gen fn body yields” (playground):

(译者注: 原文链接有错, 已自行更正了)

#![allow(unused)]
fn main() {
gen fn iter_set_rc<T: Clone>(xs: Rc<RefCell<HashSet<T>>>) -> T {
    for x in xs.borrow().iter() {
        yield x.clone();
    }
}
}

In order to enable examples like the last one to work, Rust needs to be able to express some form of “address-sensitive iterator”. The obvious starting point would be to mint a new trait PinnedIterator which changes the self-type of next to take a Pin<&mut Self> rather than &mut self:

为了处理如示例所示的情况, Rust 需要能够表达某种形式的 “地址敏感迭代器”. 显而易见的起点是引入一个新的 trait PinnedIterator, 让 next 方法接受 Pin<&mut Self> 而不是 &mut self:

#![allow(unused)]
fn main() {
trait IntoIterator {
    type Item;
    type IntoIter: Iterator<Item = Self::Item>;
    fn into_iter(self) -> Self::IntoIter;
}

trait Iterator {
    type Item;
    fn next(self: Pin<&mut Self>) -> Option<Self::Item>; // ← `Pin<&mut Self>`
    fn size_hint(&self) -> (usize, Option<usize>) { .. }
}
}

Enumerating all the problems of Pin is worth its own blog post. But still, it seems important enough to point out that this definition has what Rust for Linux calls The Safe Pinned Initialization Problem. IntoIterator::into_iter cannot return a type that is address-sensitive at time of construction, instead address-sensitivity is something that can only be guaranteed at a later point once the type is pin!ned in place.

细数 Pin 的各类问题是值得单开一篇新的博客文章的. 但是还是在此指出, Rust 中 Pin 的概念即 Linux 中的 The Safe Pinned Initialization Problem. IntoIterator::into_iter 无法返回构造时对地址敏感的类型, 确保地址不变是后面就地 pin! 的事情.

At the start of this post I used the phrase: “(soft-)deprecation of the Iterator trait”. With that I was referring to one proposed idea to enable gen {} to return a new trait Generator with the same signature as our example. As well as some bridging impls for the purposes of compatibility. The core of the compat system would be as follows:

在这篇文章的开头, 我提到 “(软性)弃用 Iterator trait”. 我的想法是, 使 gen {} 返回具有与我们的示例相同的签名的新 trait Generator, 以及某些兼容实现. 核心代码如下:

#![allow(unused)]
fn main() {
/// All `Iterator`s are `Generator`s
impl<I: IntoIterator> IntoGenerator for I {
    type Item = I::Item;
    type IntoGen = IteratorGenerator<I::IntoIter>;
    fn into_gen(self) -> Self::IntoGen {
        IteratorGenerator(self.into_iter())
    }
}

/// Only pinned `Generator`s are `Iterator`s
impl<G> Iterator for Pin<G>
where
    G: DerefMut,
    G::Target: Generator,
{
    type Item = <<G as Deref>::Target as Generator>::Item;
    fn next(&mut self) -> Option<Self::Item> {
        Generator::next(self.as_mut())
    }
}
}

This creates a situation that I’ve been describing as “one-and-a-half-way compat”, as opposed to the usual two-way-compat. And we need two-way-compat to not be a breaking change. This leads to a situation where changing a bound from taking Iterator to Generator is backwards-compatible. But changing an impl from returning an Iterator to returning a Generator is not backwards-compatible. The obvious solution then would be to migrate the entire ecosystem to take Generator bounds everywhere. Coupled with gen {} only ever returning Generator and not Iterator: that is a deprecation of Iterator in all but name.

这种情况我称之为 “单向半兼容” (one-and-a-half-way compat), 区分于传统的双向兼容 (two-way-compat). 我们需要确保 双向兼容性不会成为破坏性变更. 这导致以下现象:

  • 将约束从 Iterator 改为 Generator 是向下兼容的
  • 但将实现从返回 Iterator 改为返回 Generator不向下兼容

最直接的解决方案是推动整个生态逐步迁移到全面采用 Generator 约束. 结合 gen {} 块始终返回 Generator 而非 Iterator, 这实质上构成了对 Iterator变相弃用, 尽管名义上仍保留其存在.

At first sight it might seem like we’re being forced into deprecating Iterator because of the limitations of Pin. The obvious answer to that would be to solve the issues with Pin by replacing it with something better. But that creates a false dichotomy: there is nothing forcing us to make a decision on this today. As we established at the start of this section: a surprising amount of use cases already work without the need for address-sensitive iterators. And as we’ve seen throughout this post: address-sensitive iteration is far from the only feature that gen {} blocks will not be able to support on day one.

乍一看, 由于 Pin 的局限性, 我们似乎被迫弃用 Iterator. 显而易见的解决方案是直接踢掉 Pin 换成更好的方案. 但实际上, 没有什么迫使我们必须今天对此做出决定. 正如我们在本节开头所示: 令人惊讶的写法已经有效, 无需使用地址敏感的迭代器. 正如我们在这篇文章中看到的那样: 地址敏感的迭代远非 gen {} 块无法首先支持的唯一功能.

Iterator Guaranteeing Destruct | 保证析构的迭代器

The current formulation of thread::scope requires that the thread it’s called on remains blocked until all threads have been joined. This requires stepping into a closure and executing all code within that. Contrast this with something like FutureGroup which logically owns computations and can be freely moved around. The values of the futures resolved within can in turn be yielded out. But unlike thread::scope it can’t guarantee that all computations will complete, and so a parallel version of FutureGroup can’t mutably hold onto mutable borrows the way thread::scope can.

当前 thread::scope 的实现机制要求调用该方法的线程必须保持阻塞状态, 直到所有子线程完成. 这一特性要求必须进入闭包环境并执行其中的所有代码.

与之形成对比的是类似 FutureGroup 的结构:

  • FutureGroup逻辑层面拥有计算任务, 可以自由移动.
  • 其内部解析完成的 Future 值可以 yield 产出.
  • 无法保证所有计算任务都会完成

因此, FutureGroup 的并行版本不能像 thread::scope 那样, 以可变方式持有可变借用. 这种根本性的差异源于两者不同的生命周期管理策略.

#![allow(unused)]
fn main() {
// A usage example of `thread::scope`,
// the ability to spawn new threads
// is only available inside the closure.
thread::scope(|s| {
    s.spawn(|| ..);
    s.spawn(|| ..);
                    // ← All threads are joined here.
});

// A usage example of `FutureGroup`,
// no closures required.
let mut group = FutureGroup::new();
group.insert(future::ready(2));
group.insert(future::ready(4));
group.for_each(|_| ()).await;
}

If we want to write a type similar to FutureGroup with the same guarantees as thread::scope, we’d either need to guarantee that FutureGroup can never be dropped or guarantee that FutureGroup’s Drop impl is always run. It turns out that it’s rather impractical to have types that can’t be dropped in a language where every function may panic. So the only real option here are to have types whose destructors are guaranteed to run.

如果我们要编写类似于 FutureGroup 的类型, 并具有与 thread::scope 相同的保证, 我们要么需要保证 FutureGroup 永远不会被 drop, 要么保证 FutureGroupDrop 实现永不返回. 但这是不切实际的. 我们唯一真正的选择是拥有保证必然被析构的类型.

The most plausible way we know of to do this would be by introducing a new auto trait Leak, disallowing types from being passed to mem::forget, Box::leak, and so on. For more on the design, read Linear Types One-Pager. Because Leak is an auto-trait, we could compose it with the existing Iterator and IntoIterator traits, similar to Send and Move:

我们知道这样做的最合理的方法是引入一个新的自动特征Leak, 将类型从传递给mem::forget, Box::leak等等. 有关设计的更多信息, 请读取线性类型的单选. 因为Leak是一种自动特征, 所以我们可以将其与现有的Iterator和IntoIterator特征组成, 类似于Send和Move:

fn linear_sink(iter: impl IntoIterator<IntoIter: ?Leak>) { .. }

Async Iterator | 异步迭代器

In Rust the async keyword can transform imperative function bodies into state machines that can be manually advanced by calling the Future::poll method. Under the hood this is done using what is called a coroutine transform, which is the same transform we use to desugar gen {} blocks with. But that’s just the mechanics of it; the async keyword in Rust also introduces two new capabilities: ad-hoc concurrency and ad-hoc cancellation. Together these capabilities can be combined to create new control-flow operations, like Future::race and Future::timeout.

在 Rust 中, async 关键字能够将命令式函数体转换为可手动推进的状态机, 这种推进通过调用 Future::poll 方法实现. 其底层实现依赖于coroutine 转换机制. 该机制同样用于 gen {} 代码块的脱糖处理. 但这仅仅是其实现机制, async 关键字引入了两项新能力: 临时并发, 以及临时取消. 这些能力可组合使用, 创造新的控制流操作(如 Future::raceFuture::timeout).

Async Functions in Traits were stabilized one year ago in Rust 1.75, enabling the use of async fn in traits. Making the Iteratortrait work with async is mostly a matter of adding an async prefix to next:

一年前, 在 Rust 1.75 中, AFIT 终于稳定 (关于 AFIT, 参见 这里 的译者注), 从而允许在 Iterator trait 内引入异步方法:

#![allow(unused)]
fn main() {
trait IntoIterator {
    type Item;
    type IntoIter: Iterator<Item = Self::Item>;
    fn into_iter(self) -> Self::IntoIter;
}

trait Iterator {
    type Item;
    async fn next(&mut self) -> Option<Self::Item>;     // ← async
    fn size_hint(&self) -> (usize, Option<usize>) { .. }
}
}

While the next method here would be annotated with the async keyword, the size_hint method should probably not be. The reason for that is that it acts as a simple getter, and it really shouldn’t be performing any asynchronous computation. It’s also unclear whether into_iter should be an async fn or not. There is probably a pattern to be established here, and it might very well be.

next 方法将是 async 的, 但 size_hint 方法很可能不应该是, 它只是一个简单的 getter, 不应该执行任何异步计算. 目前尚不清楚 into_iter 是否应该是async fn. 这里可能有一个模式可以建立, 而且很可能是.

An combination of iterator variants that’s been of some interest recently is an address-sensitive async iterator. We could imagine writing an address-sensitive async iterator by making next take self: Pin<&mut Self>:

最近, 地址敏感的异步迭代器引起了我们的兴趣. 我们可以想象通过使 next 接受 self: Pin<&mut Self> 来编写一个地址敏感的异步迭代器:

#![allow(unused)]
fn main() {
trait IntoIterator {
    type Item;
    type IntoIter: Iterator<Item = Self::Item>;
    async fn into_iter(self) -> Self::IntoIter;
}

trait Iterator {
    type Item;
    async fn next(self: Pin<&mut Self>) -> Option<Self::Item>; // ← async + `Pin<&mut Self>`
    fn size_hint(&self) -> (usize, Option<usize>) { .. }
}
}

This signature is likely to confuse some people. async fn next return an impl Future which itself must be pinned prior to being polled. In this example we are separately requiring that Self is also pinned. That is because “the state of the iterator” and “the state of the future” are not the same state. We intuitively understand this when working with non-async address-sensitive iterators: the locals created within the next are not captured by the enclosing iterator, and are free to be stack-pinned for the duration of the call to next. But when working with an asynchronous address-sensitive iterator, for some reason people seem to assume that all locals defined in fn next now need to be owned by the iterator and not the future.

(译者注: 此处原文非常晦涩, 意译.)

同步代码场景中我们知道, 方法内部创建的局部变量不是迭代器本身持有, 但在异步场景中, 人们却容易误认为 async fn next 中定义的所有局部变量由迭代器持有, 实际上一个道理. 这里额外使用 Pin<&mut Self> 只是为了确保可能的自引用有效.

In the async Rust ecosystem there exists a popular variation of an async iterator trait called Stream. Rather than keeping the state of the iterator (self) and the next function separate, it combines both into a single state. The trait has a single method poll_next which acts as a mix between Future::poll and Iterator::next. With a provided convenience function async fn next that is a thin wrapper around poll_next.

在异步 Rust 生态系统中, 存在一种称为 Stream 的异步迭代器变体: 与其分别处理迭代器状态(self)和 next 方法, 不如将两个函数组合到单个状态中. 该 trait 提供 poll_next 方法, 充当 Future::pollIterator::next 的组合. 我们可以提供 async fn next, 对 poll_next 的简单封装.

#![allow(unused)]
fn main() {
trait IntoStream {
    type Item;
    type IntoStream: Stream<Item = Self::Item>;
    fn into_stream(self) -> Self::IntoStream;
}

pub trait Stream {
    type Item;
    fn poll_next(                            // ← `fn poll_next`
        self: Pin<&mut Self>,                // ← `Pin<&mut Self>`
        cx: &mut Context<'_>,                // ← `task::Context`
    ) -> Poll<Option<Self::Item>>;          // ← `task::Poll` 
    async fn next(&mut self) -> Self::Item   // ← `async`
    where
        Self: Unpin                          // ← `Self: Unpin` (译者注: 即对非自引用的 `Iterator`, 不是 pinned 也行)
    { .. }
    fn size_hint(&self) -> (usize, Option<usize>) { ... }
}
}

By combining both states into a single state, this trait violates one of the core tenets of async Rust’s design: the ability to uniformly communicate cancellation by dropping futures. Here if the future by fn next is dropped, that is a no-op and cancellation will not occur. This causes compositional async control-flow operators like Future::race to not work despite compiling.

实际上结合这两个状态违反了异步 Rust 设计的一个核心原则: 允许通过丢弃 impl Future 的匿名结构体取消异步任务. 在这里, drop 掉 fn next 产生的 impl Future, 并不会取消 Future. 这会导致诸如 Future::race 之类的异步控制流操作尽管能通过编译, 但仍无法正常工作.

To instead cancel the current call to next you are forced to either drop the entire stream, or use some bespoke method to cancel just the future’s state. Cancellation in async Rust is infamous for being hard to get right, which is understandable when (among other things) core traits in the ecosystem do not correctly handle it.

相反, 要取消当前对 next 的调用, 您将需要丢弃整个 Stream, 或使用一些定制方法来仅仅是取消 Future 的状态. 本来异步 Rust 中的取消操作就因难以正确进行而饱受诟病, 所以上面的现象还是可以理解的.

Concurrent Iterator | 并发迭代器

As we’re approaching the end of our exposition here, let’s talk about the most elaborate variations on Iterator. First in line: the rayon crate and the ParallelIterator trait. rayon provides what are called “parallel iterators” which process items concurrently rather than sequentially, using operating system threads. This tends to significantly improve throughput compared to sequential processing, but have the caveat that all consumed items must implement Send. To see just how familiar parallel iterators can be: the following example looks almost identical to a sequential iterator except for the call to into_par_iter instead of into_iter.

博文最后, 让我们来看看迭代器最缜密的一个变体: rayon crate 的 ParallelIterator trait. rayon 提供了所谓的 “并行迭代器”, 它使用操作系统线程并发地而不是顺序地处理项目. 与顺序处理相比, 这往往会显着提高吞吐量, 但需要注意的是, 元素必须实现 Send. 其 API 和同步版本很像, 如以下示例, 看起来几乎与顺序迭代器相同, 除了调用 into_par_iter 而不是 into_iter:

#![allow(unused)]
fn main() {
use rayon::prelude::*;

(0..100)
    .into_par_iter()   // ← Instead of calling `into_iter`.
    .for_each(|x| println!("{:?}", x));
}

The ParallelIterator trait however comes as a pair with the Consumer trait. It can be a little mind-boggling but the way rayon works is that combinators can be chained to create a handler, which at the end of the chain is copied to each thread and used there to handle items. This is of course a simplified explanation; I’ll defer to rayon maintainers to provide a detailed explanation. To give you a sense how different these traits are from the regular Iterator traits, here they are (simplified):

ParallelIterator trait 实际上和 Consumer trait 搭配. 它可能有点令人难以置信, 但 rayon 的工作方式是, 将组合器链接起来创建一个 handler, 在链的末尾将其复制到每个线程并用来处理项目. 当然, 这是一个简化的解释; 我将给出 rayon 维护者给出的详细解释. 为了让您理解其于常规 Iterator trait有多大的不同, 在这里给出(简化版本):

#![allow(unused)]
fn main() {
/// A consumer is effectively a generalized "fold" operation.
pub trait Consumer<Item>: Send + Sized {
    /// The type of folder that this consumer can be converted into.
    type Folder: Folder<Item, Result = Self::Result>;
    /// The type of reducer that is produced if this consumer is split.
    type Reducer: Reducer<Self::Result>;
    /// The type of result that this consumer will ultimately produce.
    type Result: Send;
}

/// A type that can be iterated over in parallel
pub trait IntoParallelIterator {
    /// What kind of iterator are we returning?
    type Iter: ParallelIterator<Item = Self::Item>;
    /// What type of item are we yielding?
    type Item: Send;
        /// Return a stateful parallel iterator.
    fn into_par_iter(self) -> Self::Iter;
}

/// Parallel version of the standard iterator trait.
pub trait ParallelIterator: Sized + Send {
    /// The type of item that this parallel iterator produces.
    type Item: Send;
    /// Internal method used to define the behavior of this
    /// parallel iterator. You should not need to call this
    /// directly.
    fn drive_unindexed<C>(self, consumer: C) -> C::Result
    where
        C: UnindexedConsumer<Self::Item>;
}
}

What matters most here is that using the ParallelIterator trait feels similar to a regular iterator. All you need to do is call into_par_iter instead of into_iter and you’re off to the races. On the consuming side it seems like we should be able to author some variation of for..in to consume parallel iterators. Rather than speculate about syntax, we can look at the signature of ParallelIterator::for_each to see which guarantees this would need to make.

这里最重要的是, 使用 ParallelIterator 与常规迭代器相似. 您仅需调用 into_par_iter 而不是 into_iter. 在消费端, 似乎我们能够写 for...in 的一些变体来消耗并行迭代器. 为此我们可以先看看 ParallelIterator::for_each 的签名:

#![allow(unused)]
fn main() {
fn for_each<F>(self, f: F)
where
    F: Fn(Self::Item) + Sync + Send
{ .. }
}

We can observe three changes here from the base iterator trait:

我们可以在这里观察到相较于基础的 Iterator trait, 存在三个变动:

  • Self no longer needs to be Sized.

    Self 不再需要 Sized.

  • Somewhat predictably the closure F needs to be thread-safe.

    可以预见的是, 闭包 F 需要是线程安全的.

  • The closure F needs to implement Fn rather than FnMut to prevent data races.

    闭包 F 需要实现 Fn 而不是 FnMut 以防止数据竞争.

We can then infer that in the case of a parallel for..in expression, the loop body would not be able to close over any mutable references. This is an addition to the existing limitation that loop bodies already can’t express FnOnce semantics and move values (e.g. “Warning: this value was moved in a previous iteration of the loop”.)

我们可以推断出在并行 for...in 的情况下, 循环体将无法闭包可变借用. 这是现有限制的补充: 循环体已经无法表达 FnOnce 语义和移动值(例如 “警告: 此值在循环的先前迭代中被移动”).

An interesting combination of are “parallel iteration” and “async iteration”. An interesting aspect of the async keyword in Rust is that it allows for ad-hoc concurrent execution of futures without needing to rely on special syscalls or OS threads. This means that concurrency and parallelism can be detached from one another. While we haven’t yet seen a “parallel async iterator” trait in the ecosystem, the futures-concurrency crate does encode a “concurrent async iterator”7. Just like ParallelIterator, ConcurrentAsyncIterator comes in a pair with a Consumer trait.

一个有趣的组合是 “并行迭代” 和 “异步迭代”. Rust 中 async 关键字的一个有趣之处在于, 它允许对 future 进行临时的并发执行, 而无需依赖特殊的系统调用或操作系统线程. 这意味着并发性和并行性可以彼此分离. 虽然我们还没有在生态系统中看到 “并行异步迭代器” trait, 但 futures-concurrency crate 确实编码了一个 “并发异步迭代器” 7. 就像 ParallelIterator 一样, ConcurrentAsyncIterator 也与一个 Consumer trait 配对.

#![allow(unused)]
fn main() {
/// Describes a type which can receive data.
pub trait Consumer<Item, Fut>
where
    Fut: Future<Output = Item>,
{
    /// What is the type of the item we’re returning when completed?
    type Output;
    /// Send an item down to the next step in the processing queue.
    async fn send(self: Pin<&mut Self>, fut: Fut) -> ConsumerState;
    /// Make progress on the consumer while doing something else.
    async fn progress(self: Pin<&mut Self>) -> ConsumerState;
    /// We have no more data left to send to the `Consumer`;
    /// wait for its output.
    async fn flush(self: Pin<&mut Self>) -> Self::Output;
}

pub trait IntoConcurrentAsyncIterator {
    type Item;
    type IntoConcurrentAsyncIter: ConcurrentAsyncIterator<Item = Self::Item>;
    fn into_co_iter(self) -> Self::IntoConcurrentAsyncIter;
}

pub trait ConcurrentAsyncIterator {
    type Item;
    type Future: Future<Output = Self::Item>;

    /// Internal method used to define the behavior
    /// of this concurrent iterator. You should not
    /// need to call this directly.
    async fn drive<C>(self, consumer: C) -> C::Output
    where
        C: Consumer<Self::Item, Self::Future>;
}
}

While ParallelIterator and ConcurrentAsyncIterator have similarities in both usage and design, they are different enough that we cant quite think as one being the async, non-thread-safe version of the other. Perhaps it is possible to bring both traits closer to one another, so that the only difference are a few strategically placed async keywords, but more research is needed to validate whether that is possible.

尽管 ParallelIteratorConcurrentAsyncIterator 在用法和设计上都有相似之处, 但它们的不同之处在于我们不能完全认为一个是异步的, 非线程安全版本的另一个版本. 也许可以使这两个 trait 彼此接近, 使其唯一的区别是一些 async 关键字, 但是需要更多的研究来验证是否可能.

Another interesting bit to point out here: concurrent iteration is also mutually exclusive with lending iteration. A lending iterator relies on yielded items having sequential lifetimes, while concurrent iterators rely on yielded items having overlapping lifetimes. Those are fundamentally incompatible concepts.

这里还要指出另一个有趣的细节: 并发迭代与借用迭代也是互斥的. 借用迭代依赖于产生的元素具有连续的生命周期, 而并发迭代依赖于产生的元素具有重叠的生命周期. 这些概念从根本上是不相容的.

Conclusion | 结论

And that’s every iterator variant that I know of, bringing us to 17 different variations. If we take away the variants that we can express using subtraits (4 variants) and auto-traits (5 variants), we are left with 9 different variants. That’s 9 variants, with 76 methods, and approximately 150 trait impls in the stdlib. That is a big API surface, and that’s not even considering all the different combinations of iterators.

我所知道的迭代器变体就这么多了, 总共有 17 种不同的变体. 如果我们去掉可以用子 trait (4 个) 和 auto-trait (5 个) 表示的变体, 那么还剩下 9 个不同的变体. 9 个变体, 76 个方法, 以及标准库中大约 150 个 trait 实现: 相当多的 API, 还没考虑各种变体的组合器.

Iterator is probably the single-most complex trait in the language. It is a junction in the language where every effect, auto-trait, and lifetime feature ends up intersecting. And unlike similar junctions like the Fn-family of traits; the Iterator trait is stable, broadly used, and has a lot of combinators. Meaning it both has a broad scope, and stringent backwards-compatibility guarantees we need to maintain.

Iterator 可能是该语言中最复杂的 trait. 它是语言中的一个交汇点, 每个效应、auto-trait 和生命周期特性最终都会在这里相交. 与 Fn 系列 trait 不同, Iterator trait 早已稳定而被广泛使用, 并且还有很多组合器, 这就给维护向下兼容性带来相当大麻烦.

At the same time Iterator is also not that special either. It’s a pretty easy trait to write by hand after all. The way I think of it is mainly as a canary for language-wide shortcomings. Iterator is for example not unique in its requirement for stable addresses: we want to be able to guarantee this for arbitrary types and to be able to use this with arbitrary interfaces. I believe that the question to ask here should be: what is stopping us from using address-sensitive types with arbitrary types and arbitrary interfaces? If we can answer that, not only will we have an answer for Iterator - we will have solved it for all other interfaces we did not consciously anticipate would want to interact with this 8.

当然, Iterator 也不是那么特别. 毕竟, 手动编写一个 Iterator trait 是相当容易的. 我认为它主要是一个用于检测语言范围缺陷的金丝雀. 例如, 并不只有 Iterator 需要稳定地址的概念: 我们希望能够保证任意类型的稳定地址, 并且能够将其与任意接口一起使用. 我认为这里应该问的问题是: 是什么阻止我们将地址敏感类型与任意类型和任意接口一起使用? 如果我们能够回答这个问题, 不仅能解决 Iterator 的问题, 还能解决我们没有有意识地预料到会与之交互的所有其他接口8.

In this post I’ve done my best to show by-example which limitations the Iterator trait has today, and how each variant can overcome those. And while I believe that we should try and lift those limitations over time, I don’t think anyone is too keen on us minting 9 new variations on the core::iter submodule. Nor the thousand-plus possible combinations of those submodules (yay combinatorics). The only feasible approach I see to navigating the problem space is by extending, rather than replacing, the Iterator trait. Here is my current thinking for how we might be able to extend Iterator to support all iterator variants:

在这篇文章中, 我尽力通过示例展示了 Iterator trait 还有哪些限制, 以及每个变体是如何克服这些限制的. 虽然我认为我们应该努力逐步解决这些限制, 但我认为没有人热衷于在 core::iter 子模块上再引入 9 个新的变体. 也没有人热衷于这些子模块的数千种可能的组合(组合学万岁). 我认为解决这个问题的唯一可行方法是扩展 Iterator trait, 而不是替换它. 这是我目前关于如何扩展 Iterator 以支持所有迭代器变体的想法:

  • base trait: default, already supported

    基本 trait: 默认, 已经支持

  • dyn-compatible: default, already supported

    dyn 兼容: 默认, 已经支持

  • bounded: sub-trait, already supported

    有界: 子 trait, 已经支持

  • fused: sub-trait, already supported

    fused: 子 trait, 已经支持

  • thread-safe: auto-trait, already supported

    线程安全: auto trait, 已经支持

  • seeking: sub-trait

    定位 :子 trait

  • compile-time: effect polymorphism (const) 9

    编译时操作: 效应多态 (const) 9

  • lending: 'move lifetime 10

    借用: 'move 生命周期 10

  • with return value: unsure 11

    带返回值: 不确定 11

  • with next argument: default value + optional/variadic arg

    next 方法接受参数: 默认 + 可选 / 可变参数

  • short-circuiting: effect polymorphism (try)

    短路: 效应多态 (try)

  • address-sensitive: auto-trait

    地址敏感: auto trait

  • guaranteeing destruct: auto-trait

    保证析构: auto trait

  • async: effect polymorphism (async)

    异步: 效应多态 (async)

  • concurrent: new trait variant(s)

    并发: 新的 trait 变体

译者注: 效应多态 并不是 effect polymorphism 的推荐译法, 译者查询资料并没有发现其准确译名. 所谓 “效应多态”, 大意是指一定的函数或代码块能够以多态的方式适应不同的效应需求, 如异步执行、异常处理、状态变更等, 无需绑定到具体实现. 例如所谓 “Replace Conditional with Polymorphism”.

When evolving the language, I believe we entire job is to balance feature development with the management of complexity. If done right, over time the language should not only become more capable, but also simpler and easier to extend. To quote something TC (T-Lang) said in a recent conversation: “We should get better at getting better every year”. 12

在语言演进的过程中, 我认为我们全部的工作就是平衡特性开发和复杂性管理. 如果做得好, 随着时间的推移, 语言不仅应该变得更强大, 而且应该更简单、更容易扩展. 引用TC(T-Lang)最近一次对话中的话: “我们应该每年都变得更擅长变得更好”.12

As we think about how we wish to overcome the challenges presented by this post, it is my sincere hope that we will increasingly start thinking of ways to solve classes of problems that just happen to show up with Iterator first. While at the same time looking for opportunities to ship features sooner, without blocking ourselves on supporting all of the use cases straight away.

当我们思考如何克服这篇文章提出的挑战时, 我真诚地希望我们能越来越多地开始思考如何解决一类问题, 而这类问题恰好首先出现在 Iterator 中. 同时, 寻找机会更快地发布特性, 而无需一开始就阻塞自己, 去支持所有的用例.


  1. I’m basing this off of my experience being a part of the Node.js Streams WG through the mid-2010s. By my count Node.js now features five distinct generations of stream abstractions. Unsurprisingly it’s not particularly pleasant to use, and ideally something we should avoid replicating in Rust. Though I’m not opposed to running deprecations of core traits in Rust either, I believe that in order to do that we want to be approaching 100% certainty that it’ll be the last deprecation we do. If we’re going to embark on a decade plus of migration issues, we have to make absolutely sure it’s worth it.
    我是根据我在 2010 中期参与 Node.js Streams WG 的经验来判断的. 据我统计, Node.js 现在有五个不同的流抽象. 不出所料, 它用起来不是特别愉快, 理想情况下我们应该避免在 Rust 中复制这种灾难. 虽然我也不反对在 Rust 中促成核心 trait 的弃用, 但我希望接近 100% 确定这将是我们做的最后一次弃用. 如果我们要开始长达十多年的迁移问题, 我们必须绝对确定这是值得的. ↩2

  2. Admittedly my knowledge of dyn is spotty at best. Out of everything in this post, the section on dyn was the bit I had the least intuition about.
    诚然, 我对 dyn 的了解充其量也只是略知皮毛. 在这篇文章的所有内容中, 关于 dyn 的部分是我最没有直觉的部分. ↩2

  3. No, files in /proc are not “regular files”. Yes, I know sockets are technically file descriptors.
    不, /proc 中的文件不是 “常规文件”. 是的, 我知道套接字在技术上是文件描述符. ↩2

  4. Remember that strings are heap-allocated so we don’t need to account for address-sensitivity. While the compiler can’t yet perform this desugaring, there is nothing in the language prohibiting it.
    请记住, 字符串是在堆上分配的, 所以我们不需要考虑地址敏感性. 虽然编译器还不能执行这种脱糖操作, 但语言中没有任何东西阻止那么干. ↩2

  5. By that I mean: an error, not a panic.
    我的意思是:一个错误, 而不是一个 panic. ↩2

  6. This affects heap-allocated locals too, but that’s not a limitation of the language, only one of the impl.
    这也会影响堆分配的局部变量, 但这并不是语言的限制, 只是实现上的限制. ↩2

  7. Technically this trait is called ConcurrentStream, but there is little that is Stream-dependent here. I called it that because it is compatible with the futures_core::Stream trait since futures-concurrency is intended to be a production crate.
    从技术上讲, 这个 trait 叫做 ConcurrentStream, 但这里几乎没有依赖 Stream 的东西. 我这样称呼它是因为它与 futures_core::Stream trait 兼容, 因为 futures-concurrency 旨在成为一个可用于生产环境的 crate. ↩2

  8. This is adjacent to “known unknowns” vs “unknown unknowns” - we should not just cater to cases we can anticipate, but also to cases we cannot. And that requires analyzing patterns and thinking in systems.
    这与 “已知的未知” 与 “未知的未知” 相邻 - 我们不应该只迎合我们可以预料到的情况, 还应该迎合我们无法预料到的情况. 这需要分析模式并以系统的方式思考. ↩2

  9. the const effect itself is already polymorphic over the compile-time effect, since const fn means: “a function that can be executed either at comptime or runtime”. Out of all the effect variants, const is most likely to happen in the near-term.
    const 效应本身已经是编译时效应的多态, 因为 const fn 意味着 “一个可以在编译时或运行时执行的函数”. 在所有效应变体中, const 最有可能在短期内发生. ↩2

  10. What we want to express is that we have an associated type which MAY take a lifetime, not MUST take a lifetime. That way we can pass a type by value where a type would otherwise be expected to be passed by-reference. This is different from both ’static lifetimes and &own references.
    我们想要表达的是, 我们有一个关联类型, 它可以可能接受一个生命周期, 而不是必须接受一个生命周期. 这样, 我们可以通过值传递一个类型, 而在其他情况下, 该类型应该通过引用传递. 这与 'static 生命周期和 &own 引用都不同. ↩2

  11. I tried answering how to add return values to the Iterator trait a year ago in my post Iterator as an Alias, but I came up short. As I mentioned earlier in the post: combining “return with value” and “may short-circuit with error” seems tricky. Maybe there is a combination here I’m missing / we can special-case something. But I haven’t seen it yet.
    一年前, 我试图在我的帖子 Iterator as an Alias 中回答如何向 Iterator trait 添加返回值, 但我没有成功. 正如我在帖子前面提到的:结合 “返回值” 和 “可能因错误而短路” 似乎很棘手. 也许这里我遗漏了一种组合/我们可以特殊处理一些事情. 但我还没有看到它. ↩2

  12. I recently remarked to TC that I’ve started to think about governance and language evolution as being about the derivative of the language and project. Rather than measuring the direct outcomes, we measure the process that produces those outcomes. TC remarked that what we should really care about is the double derivative. Not only should we improve our outcomes over time; the processes that produce those outcomes should improve over time as well. Or put differently: we should get better at getting better every year! I love this quote and I wanted y’all to read it too.
    我最近向 TC 评论说, 我已经开始将治理和语言演进视为语言和项目的导数. 我们不是衡量直接结果, 而是衡量产生这些结果的过程. TC 评论说, 我们真正应该关心的是二阶导数. 不仅我们应该随着时间的推移改善我们的结果;产生这些结果的过程也应该随着时间的推移而改善. 或者换句话说:我们应该每年都变得更擅长变得更好! 我喜欢这句话, 我想让你们也读一读. ↩2

This Week in Rust (TWiR) Rust 语言周刊中文翻译计划, 第 594 期

本文翻译自 Matthias Endler 的博客文章 https://corrode.dev/blog/pitfalls-of-safe-rust/, 英文原文版权由原作者所有, 中文翻译版权遵照 CC BY-NC-SA 协议开放. 如原作者有异议请邮箱联系.

相关术语翻译依照 Rust 语言术语中英文对照表.

囿于译者自身水平, 译文虽已力求准确, 但仍可能词不达意, 欢迎批评指正.

2025 年 4 月 13 日晚, 于北京.

GitHub last commit

Pitfalls of Safe Rust | 安全 Rust “不安全”

When people say Rust is a “safe language”, they often mean memory safety. And while memory safety is a great start, it’s far from all it takes to build robust applications.

当人们说 Rust 是一种 “安全的语言” 时, 他们通常指的是内存安全. 虽然内存安全是一个良好的开端, 但它远非构建健壮的应用程序所需的全部.

Memory safety is important but not sufficient for overall reliability.

内存安全很重要, 但不足以支撑起整体意义上的可靠性.

In this article, I want to show you a few common gotchas in safe Rust that the compiler doesn’t detect and how to avoid them.

在本文中, 我想向你展示一些编译器无法检测到的安全 Rust 中的常见问题, 以及如何避免它们.

Why Rust Can’t Always Help | 为什么 Rust 不是万金油

Even in safe Rust code, you still need to handle various risks and edge cases. You need to address aspects like input validation and making sure that your business logic is correct.

即使在安全的 Rust 代码中, 您仍然需要处理各种风险和现实问题. 您需要解决输入验证和确保业务逻辑正确等方面的问题.

Here are just a few categories of bugs that Rust doesn’t protect you from:

以下是 Rust 无法保护您免受的几类错误:

  • Type casting mistakes (e.g. overflows)

    类型转换错误(例如溢出)

  • Logic bugs

    逻辑错误

  • Panics because of using unwrap or expect

    由于使用 unwrapexpect 而出现 panic

  • Malicious or incorrect build.rs scripts in third-party crates

    第三方 crate 中存在恶意或不正确的 build.rs 脚本

  • Incorrect unsafe code in third-party libraries

    第三方库中的不安全代码不正确

  • Race conditions

    竞态条件

Let’s look at ways to avoid some of the more common problems. The tips are roughly ordered by how likely you are to encounter them.

让我们看看避免一些常见问题的方法, 大致按您遇到它们的可能性排序.

Protect Against Integer Overflow | 防止整数溢出

Overflow errors can happen pretty easily:

溢出错误很容易发生:

// DON'T: Use unchecked arithmetic
fn calculate_total(price: u32, quantity: u32) -> u32 {
    price * quantity  // Could overflow!
}

// For DEBUG build, it will panic here
fn main() {
calculate_total(u32::MAX, 2233);
}

If price and quantity are large enough, the result will overflow. Rust will panic in debug mode, but in release mode, it will silently wrap around.

如果 pricequantity 足够大, 则结果将溢出. Rust 在 Debug 模式下会 panic, 但在 Release 模式下, 它会静默地回绕 (wrap, 即会采用二进制补码).

To avoid this, use checked arithmetic operations:

为避免这种情况, 请使用标准库中提供的 “已检查(是否溢出的)” 算术操作 (译者注: 以 checked_ 为前缀):

#[derive(Debug)]
enum ArithmeticError { Overflow }
// DO: Use checked arithmetic operations
fn calculate_total(price: u32, quantity: u32) -> Result<u32, ArithmeticError> {
    price.checked_mul(quantity)
        .ok_or(ArithmeticError::Overflow)
}

fn main() {
calculate_total(u32::MAX, 2233)
.expect("Here should panic, we detect");
}

Static checks are not removed since they don’t affect the performance of generated code. So if the compiler is able to detect the problem at compile time, it will do so:

当然, 静态检查是始终的, 因为它们不会影响生成的代码的性能. 如果编译器能够在编译时检测到问题, 它将这样做:

fn main() {
    let x: u8 = 2;
    let y: u8 = 128;
    let z = x * y;  // Compile-time error!
    let _z = z;   
}

The error message will be:

错误消息将是:

error: this arithmetic operation will overflow
 --> src/main.rs:4:13
  |
4 |     let z = x * y;  // Compile-time error!
  |             ^^^^^ attempt to compute `2_u8 * 128_u8`, which would overflow
  |
  = note: `#[deny(arithmetic_overflow)]` on by default

For all other cases, use checked_add, checked_sub, checked_mul, and checked_div, which return None instead of wrapping around on underflow or overflow.1

此类函数包括 checked_addchecked_subchecked_mulchecked_div, 它们在发生溢出时返回 None, 而不是静默回绕.1

tip

Enable Overflow Checks In Release Mode

在 Release 模式下启用溢出检查

Rust carefully balances performance and safety. In scenarios where a performance hit is acceptable, memory safety takes precedence.

Rust 小心翼翼地平衡了性能和安全性. 在性能影响可以接受的情况下, 内存安全优先.

Integer overflows can lead to unexpected results, but they are not inherently unsafe. On top of that, overflow checks can be expensive, which is why Rust disables them in release mode.

整数溢出可能会导致意外结果, 但它们本身并非不安全. 最重要的是, 溢出检查可能性能代价高昂, 这就是 Rust 在发布模式下禁用它们的原因.

However, you can re-enable them in case your application can trade the last 1% of performance for better overflow detection.

但是, 您可以重新启用它们, 让您的应用程序付出牺牲最后 1% 的性能为代价来获得更好的溢出检测.

Put this into your Cargo.toml:

将它放入你的 Cargo.toml 中:

[profile.release]
overflow-checks = true # Enable integer overflow checks in release mode

This will enable overflow checks in release mode. As a consequence, the code will panic if an overflow occurs.

这将在 Release 模式下启用溢出检查. 因此, 如果发生溢出, 代码将 panic (译者注: 这不还是导致运行时 panic, 所以还是尽量用 checked_ 操作).

See the docs for more details. 有关更多详细信息, 请参阅文档.


One example where Rust accepts a performance cost for safety would be checked array indexing, which prevents buffer overflows at runtime. Another is when the Rust maintainers fixed float casting because the previous implementation could cause undefined behavior when casting certain floating point values to integers.

Rust 为了安全而接受性能成本的一个例子是 checked 数组索引, 它可以防止运行时的缓冲区溢出. 另一个是当 Rust 维护者修复浮点转换时, 因为以前的实现在将某些浮点值转换为整数时可能会导致未定义的行为.


According to some benchmarks, overflow checks cost a few percent of performance on typical integer-heavy workloads. See Dan Luu’s analysis here.

根据一些基准测试, 溢出检查在典型的整数密集型工作负载上消耗的性能只有百分之几. 在此处查看 Dan Luu 的分析.

Avoid as For Numeric Conversions | 避免 as 转换数字类型

While we’re on the topic of integer arithmetic, let’s talk about type conversions. Casting values with as is convenient but risky unless you know exactly what you are doing.

当我们讨论整数运算的话题时, 让我们谈谈类型转换. 使用 as 强制转换值很方便, 但存在风险, 除非你确切知道自己在做什么.

fn main() {
let x: i32 = 42;
let y: i8 = x as i8;  // Can overflow!
}

There are three main ways to convert between numeric types in Rust:

在 Rust 中, 主要有三种方法可以在数字类型之间进行转换:

  • ⚠️ Using the as keyword: This approach works for both lossless and lossy conversions. In cases where data loss might occur (like converting from i64 to i32), it will simply truncate the value.

    ⚠️ 使用 as 关键字: 此方法可用于无损和有损转换. 在可能发生数据丢失的情况下(例如从 i64 转换为 i32), 它只会截断.

  • Using From::from(): This method only allows lossless conversions. For example, you can convert from i32 to i64 since all 32-bit integers can fit within 64 bits. However, you cannot convert from i64 to i32 using this method since it could potentially lose data.

    使用 From::from(): 此方法只允许无损转换. 例如, 您可以从 i32 转换为 i64, 因为所有 32 位整数都可以容纳在 64 位内. 但是, 您不能使用此方法从 i64 转换为 i32, 因为它可能会丢失数据.

  • Using TryFrom: This method is similar to From::from() but returns a Result instead of panicking. This is useful when you want to handle potential data loss gracefully.

    使用 TryFrom: 此方法类似于 From::from(), 但返回 Result 而不是 panic. 当您想要正常处理潜在的数据丢失时, 这非常有用.

tip

Safe Numeric Conversions

安全的数值转换

If in doubt, prefer From::from() and TryFrom over as.

如有选择困难症, 请首选 From::from()TryFrom 而不是 as.

  • use From::from() when you can guarantee no data loss.

    当您可以保证不会丢失数据时, 请使用 From::from().

    译者注: 一定意义上这是取悦类型系统的做法, 例如 i32i64 转换是必然无损的, 才允许 From::from(), 直接 as 即可. 这个方法一个更突出的意义在于处理 i64isize 这种涉及 isize, usize 的转换, 因为在 32 位系统 isize 等价于 i32 而不是 i64, 以此类推.

  • use TryFrom when you need to handle potential data loss gracefully.

    当您需要正常处理潜在的数据丢失时, 请使用 TryFrom.

  • only use as when you’re comfortable with potential truncation or know the values will fit within the target type’s range and when performance is absolutely critical.

    仅当你不在意可能的截断或知道值一定处于目标数字类型的可容纳范围并且性能绝对关键时, 才使用 as.

(Adapted from StackOverflow answer by delnan and additional context.)

(改编自 delnan 的 StackOverflow 回复其他上下文内容.)

The as operator is not safe for narrowing conversions. It will silently truncate the value, leading to unexpected results.

as 运算符对于缩小转换范围是不安全的. 它将静默截断值, 从而导致意外的结果.

What is a narrowing conversion? It’s when you convert a larger type to a smaller type, e.g. i32 to i8.

什么是收缩转换? 当你将一个(可容纳数字范围)较大的类型转换为一个较小的类型时, 例如 i32i8.

For example, see how as chops off the high bits from our value:

例如, 看看 as 如何从我们的值中切掉高位:

fn main() {
    let a: u16 = 0x1234;
    let b: u8 = a as u8;
    println!("0x{:04x}, 0x{:02x}", a, b); // 0x1234, 0x34
}

So, coming back to our first example above, instead of writing

所以, 回到上面的第一个例子, 不要这么写

#![allow(unused)]
fn main() {
let x: i32 = 42;
let y: i8 = x as i8;  // Can overflow!
}

use TryFrom instead and handle the error gracefully:

请改用 TryFrom 并优雅地处理可能的转换错误:

#![allow(unused)]
fn main() {
let y = i8::try_from(x).ok_or("Number is too big to be used here")?;
}

(译者注: 推荐查阅 https://cheats.han.rs/#type-conversions 快速查阅有关 as 关键字的用法说明.)

Use Bounded Types for Numeric Values | 对数值使用有界类型

Bounded types make it easier to express invariants and avoid invalid states.

有界类型可以更轻松地表达不变量并避免无效状态.

E.g. if you have a numeric type and 0 is never a correct value, use std::num::NonZeroUsize instead.

例如, 如果你有一个数字类型, 并且 0 永远不是正确的值, 请改用 std::num::NonZeroUsize.

You can also create your own bounded types:

您还可以创建自己的有界类型, 这里是一个完整示例:

// This example demonstrates how to use bounded types to enforce invariants
// Instead of using raw primitive types that could have invalid values,
// we create a custom type that enforces constraints on construction

// 此示例展示了如何利用有界类型来强制保持不变量的有效性.
// 相较于使用可能包含无效值的原始基本类型, 
// 我们创建了一个自定义类型, 在构造时强制检查约束条件.

// Define our error type
// 定义错误类型
// (译者注: 这种只有一种可能变体的类型不如使用 marker Struct 模式, ZST 会被优化掉)
#[derive(Debug)]
enum DistanceError {
    Invalid,
}

// DON'T: Use raw numeric types for domain values
// 不要为内部数据使用原始的数字类型
struct RawMeasurement {
    distance: f64, // Could be negative or NaN! 可能是负数或 NaN!
}

// DO: Create bounded types with well-defined constraints
/// A distance value that is guaranteed to be non-negative and finite
///
/// This type provides several advantages:
/// - It's impossible to create an invalid distance (negative or NaN)
/// - The validation happens once at creation time
/// - Functions accepting this type don't need to re-validate
/// - Intent is clearly documented in the type system
///
// 务必: 创建具有明确约束定义的边界类型
/// 保证为非负且有限的距离值
///
/// 该类型具有以下优势:
/// - 无法创建无效距离 (负数或NaN)
/// - 验证仅在创建时进行一次
/// - 接受此类型的函数无需重复验证
/// - 意图通过类型系统清晰记录
#[derive(Debug, Clone, Copy, PartialEq)]
struct Distance(f64);

impl Distance {
    /// Creates a new Distance if the value is valid (non-negative and finite)
    ///
    /// # Errors
    ///
    /// Returns `DistanceError::Invalid` if:
    ///
    /// - The value is negative
    /// - The value is NaN or infinity
    ///
    /// 当数值有效 (非负且有限) 时创建一个新的 Distance
    ///
    /// # 错误
    ///
    /// 以下情况返回 `DistanceError::Invalid`:
    ///
    /// - 数值为负数
    /// - 数值为NaN或无限大
    pub fn new(value: f64) -> Result<Self, DistanceError> {
        if value < 0.0 || !value.is_finite() {
            return Err(DistanceError::Invalid);
        }
        Ok(Distance(value))
    }

    /// Returns the underlying distance value
    pub fn value(&self) -> f64 {
        self.0
    }
}

/// A measurement with guaranteed valid distance
#[derive(Debug, Clone, Copy, PartialEq)]
struct Measurement {
    distance: Distance,
}

impl Measurement {
    /// Creates a new measurement with the given distance
    ///
    /// # Errors
    ///
    /// Returns `DistanceError::Invalid` if the distance is invalid
    pub fn new(distance: f64) -> Result<Self, DistanceError> {
        let distance = Distance::new(distance)?;
        Ok(Measurement { distance })
    }
}

fn main() -> Result<(), DistanceError> {
    // Valid distance
    let valid_measurement = Measurement::new(42.0)?;
    println!("Valid measurement: {:?}", valid_measurement.distance);

    // These would fail at creation time, preventing invalid states:

    // Negative distance
    let negative_result = Measurement::new(-5.0);
    println!("Negative distance result: {:?}", negative_result);

    // NaN distance
    let nan_result = Measurement::new(f64::NAN);
    println!("NaN distance result: {:?}", nan_result);

    // Infinite distance
    let inf_result = Measurement::new(f64::INFINITY);
    println!("Infinite distance result: {:?}", inf_result);

    Ok(())
}

Don’t Index Into Arrays Without Bounds Checking | 不要在没有边界检查的情况下索引数组

Whenever I see the following, I get goosebumps😨:

每当我看到以下内容时, 我都会起鸡皮疙瘩😨:

fn main() {
let arr = [1, 2, 3];
let elem = arr[3];  // Panic!
let _elem = elem;
}

That’s a common source of bugs. Unlike C, Rust does check array bounds and prevents a security vulnerability, but it still panics at runtime.

这是 bug 的常见来源. 与 C 语言不同, Rust 确实会检查数组边界, 但不影响它还是会导致运行时 panic.

Instead, use the get method:

请改用 get 方法:

#![allow(unused)]
fn main() {
let elem = arr.get(3);
}

It returns an Option which you can now handle gracefully.

它返回一个 Option, 您现在可以优雅地处理它.

See this blog post for more info on the topic.

请参阅此博客文章获得更多信息.

Use split_at_checked Instead Of split_at | 使用 split_at_checked 而不是 split_at

This issue is related to the previous one. Say you have a slice and you want to split it at a certain index.

此问题与上一个有关. 假设你有一个切片, 你想在某个索引处拆分它:

fn main() {
let mid = 4;
let arr = [1, 2, 3];
let (left, right) = arr.split_at(mid);
let _ = (left, right);
}

You might expect that this returns a tuple of slices where the first slice contains all elements and the second slice is empty.

您可能只是觉得这会返回一个切片元组, 其中第一个切片包含所有元素, 而第二个切片为空.

Instead, the above code will panic because the mid index is out of bounds! 相反, 上面的代码会 panic, 因为 mid 索引超出范围!

To handle that more gracefully, use split_at_checked instead:

要更优雅地处理这个问题, 请改用 split_at_checked:

fn main() {
let mid = 4;
let arr = [1, 2, 3];
// This returns an Option
match arr.split_at_checked(mid) {
    Some((left, right)) => {
        // Do something with left and right
       let _ = (left, right);
    }
    None => {
        // Handle the error
    }
}
}

This returns an Option which allows you to handle the error case.

这将返回一个 Option.

More info about split_at_checked here.

有关 split_at_checked 的更多信息, 请点击此处.

Avoid Primitive Types For Business Logic | 避免在业务逻辑中直接使用基本类型

It’s very tempting to use primitive types for everything. Especially Rust beginners fall into this trap.

对所有事情都使用原始类型是非常直接的, 尤其是 Rust 初学者会落入这个陷阱.

#![allow(unused)]
fn main() {
// DON'T: Use primitive types for usernames
// 不要为用户名使用基本类型!
fn authenticate_user(username: String) {
    // Raw String could be anything - empty, too long, or contain invalid characters
    // 可以是任何字符串: 空的, 过长的, 或者含非法字符的
}
}

However, do you really accept any string as a valid username? What if it’s empty? What if it contains emojis or special characters?

但是, 您真的接受任何字符串作为有效的用户名吗? 如果它是空的怎么办? 如果它包含表情符号或特殊字符怎么办?

You can create a custom type for your domain instead:

您可以改为创建自定义类型, 下面是一个完整示例:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Username(String);

enum UsernameError {
    Empty,
    TooLong,
    InvalidCharacters,
}

impl Username {
    pub fn new(name: &str) -> Result<Self, UsernameError> {
        // Check for empty username
        // 非空
        if name.is_empty() {
            return Err(UsernameError::Empty);
        }

        // Check length (for example, max 30 characters)
        // 限制长度
        if name.len() > 30 {
            return Err(UsernameError::TooLong);
        }

        // Only allow alphanumeric characters and underscores
        // 限制可用的字符
        // 这里还有个坑是, 不要试图手动 `as_bytes` 再逐个字节 as char, 注意底层是 UTF-8 编码的!
        // 请使用 `chars()` 创建对字符的迭代器 (iterator), 它会帮我们处理编码问题.
        if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
            return Err(UsernameError::InvalidCharacters);
        }

        Ok(Username(name.to_string()))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

fn authenticate_user(username: Username) {
    // We know this is always a valid username!
    // No empty strings, no emojis, no spaces, etc.
}
}

Make Invalid States Unrepresentable | 使无效状态不可表示

The next point is closely related to the previous one.

下一点与上一点密切相关.

Can you spot the bug in the following code?

您能发现以下代码中的错误吗?

#![allow(unused)]
fn main() {
// DON'T: Allow invalid combinations
struct Configuration {
    port: u16,
    host: String,
    ssl: bool,
    ssl_cert: Option<String>, 
}
}

The problem is that you can have ssl set to true but ssl_cert set to None. That’s an invalid state! If you try to use the SSL connection, you can’t because there’s no certificate. This issue can be detected at compile-time:

问题是你可以将 ssl 设置为 true, 但将 ssl_cert 设置为 None. 这是一个无效的状态! 如果您尝试使用 SSL 连接, 则无法使用, 因为没有证书. 实际上, 可以在编译时检测到此问题:

Use types to enforce valid states:

使用类型系统强制保证有效状态:

#![allow(unused)]
fn main() {
// First, let's define the possible states for the connection
// 首先让我们确认连接的所有可能状态
enum ConnectionSecurity {
    Insecure,
    // We can't have an SSL connection
    // without a certificate!
    // 没有证书没办法建立 SSL 链接!
    Ssl { cert_path: String },
}

struct Configuration {
    port: u16,
    host: String,
    // Now we can't have an invalid state!
    // Either we have an SSL connection with a certificate
    // or we don't have SSL at all.
    // 现在, 我们就没办法遇到非法状态了!
    // 要么有证书能建立 SSL 连接, 要不然就是不能建立 SSL 连接!
    security: ConnectionSecurity,
}
}

In comparison to the previous section, the bug was caused by an invalid combination of closely related fields. To prevent that, clearly map out all possible states and transitions between them. A simple way is to define an enum with optional metadata for each state.

与上一节相比, 该 bug 是由密切相关的字段的无效组合引起的. 为了防止这种情况, 请清楚地规划出所有可能的状态和它们之间的转换. 一种简单的方法是定义一个枚举, 为每个状态指定一个变体, 并附加可能需要的字段.

If you’re curious to learn more, here is a more in-depth blog post on the topic.

如果您想了解更多信息, 这里有一篇关于该主题的更深入的博客文章.

(译者注: 等待翻译!)

Handle Default Values Carefully | 谨慎处理默认值

It’s quite common to add a blanket Default implementation to your types. But that can lead to unforeseen issues.

向你的类型添加一个空白的 Default 实现是很常见的. 但这可能会导致不可预见的问题.

For example, here’s a case where the port is set to 0 by default, which is not a valid port number.2

例如, 在这种情况下, 端口默认设置为 0, 这不是一个有效的端口号.2.

#![allow(unused)]
fn main() {
// DON'T: Implement `Default` without consideration
#[derive(Default)]  // Might create invalid states!
struct ServerConfig {
    port: u16,      // Will be 0, which isn't a valid port!
    max_connections: usize,
    timeout_seconds: u64,
}
}

Instead, consider if a default value makes sense for your type.

请考虑默认值是否对您的类型有意义.

#![allow(unused)]
fn main() {
use std::{num::{NonZeroUsize, NonZeroU16}, time::Duration};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Port(NonZeroU16);

// DO: Make Default meaningful or don't implement it
struct ServerConfig {
    port: Port,
    max_connections: NonZeroUsize,
    timeout_seconds: Duration,
}

impl ServerConfig {
    pub const fn new(port: Port) -> Self {
        Self {
            port,
            max_connections: NonZeroUsize::new(100).unwrap(),
            timeout_seconds: Duration::from_secs(30),
        }
    }
}
}

Implement Debug Safely | 安全地实现 Debug

If you blindly derive Debug for your types, you might expose sensitive data. Instead, implement Debug manually for types that contain sensitive information.

如果盲目地为您的类型派生 Debug, 则可能会暴露敏感数据. 请为包含敏感信息的类型手动实现 Debug.

#![allow(unused)]
fn main() {
// DON'T: Expose sensitive data in debug output
#[derive(Debug)]
struct User {
    username: String,
    password: String,  // Will be printed in debug output!
}
}

Instead, you could write: 相反, 您可以编写:

// DO: Implement Debug manually
#[derive(Debug)]
struct User {
    username: String,
    password: Password,
}

struct Password(String);

impl std::fmt::Debug for Password {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("[REDACTED]")
    }
}

fn main() {
    let user = User {
        username: String::from(""),
        password: Password(String::from("")),
    };
    println!("{user:#?}");
}

This prints 此打印

User {
    username: "",
    password: [REDACTED],
}

For production code, use a crate like secrecy.

对于生产代码, 请使用类似 secrecy 这种 crate.

However, it’s not black and white either: If you implement Debug manually, you might forget to update the implementation when your struct changes. A common pattern is to destructure the struct in the Debug implementation to catch such errors.

然而, 它也不是非黑即白的: 如果你手动实现 Debug, 你可能会忘记在结构体更改时更新实现. 一种常见的模式是在 Debug 实现中解构结构以捕获此类错误.

Instead of this:

不要这么干:

#![allow(unused)]
fn main() {
// don't
impl std::fmt::Debug for DatabaseURI {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}://{}:[REDACTED]@{}/{}", self.scheme, self.user, self.host, self.database)
    }
}
}

How about destructuring the struct to catch changes?

如何解构结构体以捕获更改?

// 这里 user, password 这些都不应该直接用 String 的
// 仅作示例, 原文的链接放错了
// 尝试加个字段然后运行看看会发生什么? 把 database_version 一行解除注释吧
struct DatabaseURI {
    scheme: String,
    user: String,
    password: String,
    host: String,
    database: String,
    // database_version: u16
}
// do
impl std::fmt::Debug for DatabaseURI {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
       // Destructure the struct to catch changes
       // This way, the compiler will warn you if you add a new field
       // and forget to update the Debug implementation
       // 解构结构体以捕获变更
       // 这样当你新增字段却忘记更新 Debug 实现时
       // 编译器会发出警告
        let DatabaseURI { scheme, user, password: _, host, database, } = self;
        write!(f, "{scheme}://{user}:[REDACTED]@{host}/{database}")?;
        // -- or --
        // f.debug_struct("DatabaseURI")
        //     .field("scheme", scheme)
        //     .field("user", user)
        //     .field("password", &"***")
        //     .field("host", host)
        //     .field("database", database)
        //     .finish()

        Ok(())
    }
}

Thanks to Wesley Moore (wezm) for the hint and to Simon Brüggen (m3t0r) for the example.

感谢 Wesley Moore (wezm) 的提示和 Simon Brüggen (m3t0r) 提供示例.

(译者注: 可是放错链接了)

Careful With Serialization | 谨慎使用序列化

Don’t blindly derive Serialize and Deserialize – especially for sensitive data. The values you read/write might not be what you expect!

不要盲目地派生 SerializeDeserialize: 尤其是对于敏感数据. 您读/写的值可能不是您期望的值!

#![allow(unused)]
fn main() {
// DON'T: Blindly derive Serialize and Deserialize 
#[derive(Serialize, Deserialize)]
struct UserCredentials {
    #[serde(default)]  // ⚠️ Accepts empty strings when deserializing!
    username: String,
    #[serde(default)]
    password: String, // ⚠️ Leaks the password when serialized!
}
}

When deserializing, the fields might be empty. Empty credentials could potentially pass validation checks if not properly handled

反序列化时, 字段可能为空. 如果处理不当, 空凭证可能会通过验证检查.

On top of that, the serialization behavior could also leak sensitive data. By default, Serialize will include the password field in the serialized output, which could expose sensitive credentials in logs, API responses, or debug output.

最重要的是, 序列化行为还可能泄露敏感数据. 默认情况下, Serialize 将在序列化输出中包含 password 字段, 这可能会在日志、API 响应或调试输出中暴露敏感凭据.

A common fix is to implement your own custom serialization and deserialization methods by using impl<'de> Deserialize<'de> for UserCredentials.

一种常见的解决方法是使用 impl<'de> Deserialize<'de> for UserCredentials 实现你自己的自定义序列化和反序列化方法.

The advantage is that you have full control over input validation. However, the disadvantage is that you need to implement all the logic yourself.

优点是您可以完全控制输入验证. 但是, 缺点是您需要自己实现所有代码逻辑.

An alternative strategy is to use the #[serde(try_from = "FromType")] attribute.

另一种策略是使用 (serde 等库提供的自定义解析方法的属性, 如) #[serde(try_from = "FromType")].

Let’s take the Password field as an example. Start by using the newtype pattern to wrap the standard types and add custom validation:

我们以 Password 字段为例. 首先使用新类型模式包装标准类型并添加自定义验证:

#![allow(unused)]
fn main() {
#[derive(Deserialize)]
// Tell serde to call `Password::try_from` with a `String`
#[serde(try_from = "String")]
pub struct Password(String);
}

Now implement TryFrom for Password: 现在为 Password 实现 TryFrom:

#![allow(unused)]
fn main() {
impl TryFrom<String> for Password {
    type Error = PasswordError;

    /// Create a new password
    ///
    /// Throws an error if the password is too short.
    /// You can add more checks here.
    fn try_from(value: String) -> Result<Self, Self::Error> {
        // Validate the password
        if value.len() < 8 {
            return Err(PasswordError::TooShort);
        }
        Ok(Password(value))
    }
}
}

With this trick, you can no longer deserialize invalid passwords:

使用此技巧, 您将无法再反序列化无效密码:

#![allow(unused)]
fn main() {
// Panic: password too short!
let password: Password = serde_json::from_str(r#""pass""#).unwrap();
}

完整示例:

extern crate serde;
extern crate serde_json;

use serde::Deserialize;
use std::fmt;

#[derive(Debug)]
pub enum PasswordError {
    TooShort,
}

impl fmt::Display for PasswordError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::TooShort => write!(f, "password too short"),
        }
    }
}

impl std::error::Error for PasswordError {}

#[derive(Deserialize)]
// Tell serde to call `Password::try_from` with a `String`
#[serde(try_from = "String")]
pub struct Password(String);

impl TryFrom<String> for Password {
    type Error = PasswordError;

    /// Create a new password
    ///
    /// Throws an error if the password is too short.
    /// You can add more checks here.
    fn try_from(value: String) -> Result<Self, Self::Error> {
        // Validate the password
        if value.len() < 8 {
            return Err(PasswordError::TooShort);
        }
        Ok(Password(value))
    }
}

fn main() {
    let password: Password = serde_json::from_str(r#""pass""#).unwrap(); // Panic: password too short!
   let _password = password.0;
}

Credits go to EqualMa’s article on dev.to and to Alex Burka (durka) for the hint.

感谢 EqualMa 在 dev.to 上的文章Alex Burka (durka) 的提示.

Protect Against Time-of-Check to Time-of-Use (TOCTOU) | 防止 “检查时间与使用时间” 攻击

This is a more advanced topic, but it’s important to be aware of it. TOCTOU (time-of-check to time-of-use) is a class of software bugs caused by changes that happen between when you check a condition and when you use a resource.

这是一个更高级的主题, 但了解它很重要. TOCTOU 是一类软件错误, 这些错误是由检查条件和使用资源之间发生的变化引起的.

#![allow(unused)]
fn main() {
// DON'T: Vulnerable approach with separate check and use
fn remove_dir(path: &Path) -> io::Result<()> {
    // First check if it's a directory
    if !path.is_dir() {
        return Err(io::Error::new(
            io::ErrorKind::NotADirectory,
            "not a directory"
        ));
    }
    
    // TOCTOU vulnerability: Between the check above and the use below,
    // the path could be replaced with a symlink to a directory we shouldn't access!
    remove_dir_impl(path)
}
}

(Rust playground)

The safer approach opens the directory first, ensuring we operate on what we checked:

更安全的方法是首先打开目录, 保证我们所操作的内容是我们先前检查过的:

#![allow(unused)]
fn main() {
// DO: Safer approach that opens first, then checks
fn remove_dir(path: &Path) -> io::Result<()> {
    // Open the directory WITHOUT following symlinks
    let handle = OpenOptions::new()
        .read(true)
        .custom_flags(O_NOFOLLOW | O_DIRECTORY) // Fails if not a directory or is a symlink
        .open(path)?;
    
    // Now we can safely remove the directory contents using the open handle
    remove_dir_impl(&handle)
}
}

(Rust playground)

Here’s why it’s safer: while we hold the handle, the directory can’t be replaced with a symlink. This way, the directory we’re working with is the same as the one we checked. Any attempt to replace it won’t affect us because the handle is already open.

这就是为什么它更安全: 当我们持有 handle 时, 目录不能被符号链接替换. 这样, 我们正在使用的目录与我们检查的目录相同. 任何更换它的尝试都不会影响我们, 因为文件句柄已经打开了.

You’d be forgiven if you overlooked this issue before. In fact, even the Rust core team missed it in the standard library. What you saw is a simplified version of an actual bug in the std::fs::remove_dir_all function. Read more about it in this blog post about CVE-2022-21658.

如果您以前忽略了这个问题, 那也是情有可原的. 事实上, 即使是 Rust 核心团队也曾在标准库中忽视了这个问题. 您看到的是出现在 std::fs::remove_dir_all 函数中一个实际的 bug 的简化版本. 在这篇关于 CVE-2022-21658 的博文中阅读更多相关信息.

Use Constant-Time Comparison for Sensitive Data | 对敏感数据使用常数时间比较

Timing attacks are a nifty way to extract information from your application. The idea is that the time it takes to compare two values can leak information about them. For example, the time it takes to compare two strings can reveal how many characters are correct. Therefore, for production code, be careful with regular equality checks when handling sensitive data like passwords.

计时攻击是从应用程序中提取信息的一种巧妙方法. 其思路是, 比较两个值所花费的时间可能会泄露有关它们的信息. 例如, 比较两个字符串所花费的时间可以揭示有多少个字符是正确的. 因此, 对于生产环境代码, 在处理密码等敏感数据时要小心, 相等性检查应当是等时的.

#![allow(unused)]
fn main() {
// DON'T: Use regular equality for sensitive comparisons
fn verify_password(stored: &[u8], provided: &[u8]) -> bool {
    stored == provided  // Vulnerable to timing attacks!
}

// DO: Use constant-time comparison
use subtle::{ConstantTimeEq, Choice};

fn verify_password(stored: &[u8], provided: &[u8]) -> bool {
    stored.ct_eq(provided).unwrap_u8() == 1
}
}

Don’t Accept Unbounded Input | 不接受无限度输入

Protect Against Denial-of-Service Attacks with Resource Limits. These happen when you accept unbounded input, e.g. a huge request body which might not fit into memory.

通过资源限制防止拒绝服务攻击. 当你接受无限度输入时, 就会发生这种情况, 例如, 一个巨大的请求可能无法放入内存.

#![allow(unused)]
fn main() {
// DON'T: Accept unbounded input
fn process_request(data: &[u8]) -> Result<(), Error> {
    let decoded = decode_data(data)?;  // Could be enormous!
    // Process decoded data
    Ok(())
}
}

Instead, set explicit limits for your accepted payloads:

请为请求内容设置显式限制:

#![allow(unused)]
fn main() {
const MAX_REQUEST_SIZE: usize = 1024 * 1024;  // 1MiB

fn process_request(data: &[u8]) -> Result<(), Error> {
    if data.len() > MAX_REQUEST_SIZE {
        return Err(Error::RequestTooLarge);
    }
    
    let decoded = decode_data(data)?;
    // Process decoded data
    Ok(())
}
}

Surprising Behavior of Path::join With Absolute Paths | Path::join 处理绝对路径时的惊人行为

If you use Path::join to join a relative path with an absolute path, it will silently replace the relative path with the absolute path.

如果使用 Path::join 将相对路径与绝对路径连接起来, 则会静默地将相对路径替换为绝对路径.

use std::path::Path;

fn main() {
    let path = Path::new("/usr").join("/local/bin");
    println!("{path:?}"); // Prints "/local/bin" 
}

This is because Path::join will return the second path if it is absolute.

这是因为如果 Path::join 一个绝对路径, 则返回这个绝对路径.

(译者注: 这个函数不是返回一个 Result 毕竟, 两个绝对路径连接没有意义的嘛…)

I was not the only one who was confused by this behavior. Here’s a thread on the topic, which also includes an answer by Johannes Dahlström:

我不是唯一一个对这种行为感到困惑的人. 这是关于该主题的帖子, 其中还包括 Johannes Dahlström 的回答:

The behavior is useful because a caller […] can choose whether it wants to use a relative or absolute path, and the callee can then simply absolutize it by adding its own prefix and the absolute path is unaffected which is probably what the caller wanted. The callee doesn’t have to separately check whether the path is absolute or not.

该行为很有用, 因为调用方 […] 可以选择是否要使用相对路径或绝对路径, 然后被调用方可以通过添加自己的前缀来简单地将其绝对路径化, 并且绝对路径不受影响, 这可能是调用方想要的. 被调用方不必单独检查路径是否为绝对路径.

And yet, I still think it’s a footgun. It’s easy to overlook this behavior when you use user-provided paths. Perhaps join should return a Result instead? In any case, be aware of this behavior.

然而, 我仍然认为这是一柄双刃剑. 当您使用用户提供的路径时, 很容易忽略此行为. 也许 join 应该返回 Result? 无论如何, 请注意此行为.

Check For Unsafe Code In Your Dependencies With cargo-geiger | 使用 cargo-geiger 检查依赖项中的不安全代码

So far, we’ve only covered issues with your own code. For production code, you also need to check your dependencies. Especially unsafe code would be a concern. This can be quite challenging, especially if you have a lot of dependencies.

到目前为止, 我们只介绍了你自己的代码的问题. 对于生产环境代码, 您还需要检查依赖项. 尤其是不安全的代码将是一个问题. 这可能非常具有挑战性, 尤其是在您有很多依赖项的情况下.

cargo-geiger is a neat tool that checks your dependencies for unsafe code. It can help you identify potential security risks in your project.

cargo-geiger 是一个简洁的工具, 可以检查你的依赖项是否存在不安全的代码. 它可以帮助您识别项目中的潜在安全风险.

cargo install cargo-geiger
cargo geiger

This will give you a report of how many unsafe functions are in your dependencies. Based on this, you can decide if you want to keep a dependency or not.

这将为你提供一份报告, 说明你的依赖项中有多少不安全的函数. 基于此, 您可以决定是否要保留依赖项.

Clippy Can Prevent Many Of These Issues | Clippy 可以防止许多这些问题

Here is a set of clippy lints that can help you catch these issues at compile time. See for yourself in the Rust playground.

下面是一组 clippy lint, 可以帮助您在编译时捕获这些问题. 尝试在 Rust playground 中使用 clippy 吧!

Here’s the gist:

要点如下:

  • cargo check will not report any issues.

    cargo check 不会报告任何问题.

  • cargo run will panic or silently fail at runtime.

    cargo run 将在运行时 panic 或静默失败.

  • cargo clippy will catch all issues at compile time (!) 😎

    cargo clippy 将在编译时捕获所有问题 (!) 😎

// 算术运算
#![deny(arithmetic_overflow)] // 防止导致整数溢出的操作
#![deny(clippy::checked_conversions)] // 建议在数值类型间使用受检转换
#![deny(clippy::cast_possible_truncation)] // 检测可能导致值截断的类型转换
#![deny(clippy::cast_sign_loss)] // 检测可能丢失正负值信息的类型转换
#![deny(clippy::cast_possible_wrap)] // 检测可能导致值回绕的类型转换
#![deny(clippy::cast_precision_loss)] // 检测可能丢失精度的类型转换
#![deny(clippy::integer_division)] // 高亮整数除法截断导致的潜在错误
#![deny(clippy::arithmetic_side_effects)] // 检测具有潜在副作用的算术运算
#![deny(clippy::unchecked_duration_subtraction)] // 确保持续时间减法不会导致下溢

// 解包操作
#![warn(clippy::unwrap_used)] // 不鼓励使用可能导致 panic 的 `.unwrap()`
#![warn(clippy::expect_used)] // 不鼓励使用可能导致 panic 的 `.expect()`
#![deny(clippy::panicking_unwrap)] // 禁止对已知会引发 panic 的值进行解包
#![deny(clippy::option_env_unwrap)] // 禁止解包可能不存在的环境变量

// 数组索引
#![deny(clippy::indexing_slicing)] // 避免直接数组索引, 使用更安全的方法如 `.get()`

// 路径处理
#![deny(clippy::join_absolute_paths)] // 防止与绝对路径拼接时出现问题

// 序列化问题
#![deny(clippy::serde_api_misuse)] // 防止错误使用 serde 的序列化/反序列化API

// 无界输入
#![deny(clippy::uninit_vec)] // 防止创建未初始化的向量 (不安全操作)

// 不安全代码检测
#![deny(clippy::transmute_int_to_char)] // 防止从整数到字符的不安全类型转换
#![deny(clippy::transmute_int_to_float)] // 防止从整数到浮点数的不安全类型转换
#![deny(clippy::transmute_ptr_to_ref)] // 防止从指针到引用的不安全类型转换
#![deny(clippy::transmute_undefined_repr)] // 检测具有潜在未定义表示的类型转换

use std::path::Path;
use std::time::Duration;

fn main() {
    // ARITHMETIC ISSUES

    // Integer overflow: This would panic in debug mode and silently wrap in release
    let a: u8 = 255;
    let _b = a + 1;

    // Unsafe casting: Could truncate the value
    let large_number: i64 = 1_000_000_000_000;
    let _small_number: i32 = large_number as i32;

    // Sign loss when casting
    let negative: i32 = -5;
    let _unsigned: u32 = negative as u32;

    // Integer division can truncate results
    let _result = 5 / 2; // Results in 2, not 2.5

    // Duration subtraction can underflow
    let short = Duration::from_secs(1);
    let long = Duration::from_secs(2);
    let _negative = short - long; // This would underflow

    // UNWRAP ISSUES

    // Using unwrap on Option that could be None
    let data: Option<i32> = None;
    let _value = data.unwrap();

    // Using expect on Result that could be Err
    let result: Result<i32, &str> = Err("error occurred");
    let _value = result.expect("This will panic");

    // Trying to get environment variable that might not exist
    let _api_key = std::env::var("API_KEY").unwrap();

    // ARRAY INDEXING ISSUES

    // Direct indexing without bounds checking
    let numbers = vec![1, 2, 3];
    let _fourth = numbers[3]; // This would panic

    // Safe alternative with .get()
    if let Some(fourth) = numbers.get(3) {
        println!("{fourth}");
    }

    // PATH HANDLING ISSUES

    // Joining with absolute path discards the base path
    let base = Path::new("/home/user");
    let _full_path = base.join("/etc/config"); // Results in "/etc/config", base is ignored

    // Safe alternative
    let base = Path::new("/home/user");
    let relative = Path::new("config");
    let full_path = base.join(relative);
    println!("Safe path joining: {:?}", full_path);

    // UNSAFE CODE ISSUES

    // Creating uninitialized vectors (could cause undefined behavior)
    let mut vec: Vec<String> = Vec::with_capacity(10);
    unsafe {
        vec.set_len(10); // This is UB as Strings aren't initialized
    }
}

Conclusion | 结论

Phew, that was a lot of pitfalls! How many of them did you know about?

呼, 陷阱真多啊! 其中多少个你之前有意识到了呢?

Even if Rust is a great language for writing safe, reliable code, developers still need to be disciplined to avoid bugs.

即使 Rust 是督促开发者编写安全可靠代码的好语言, 开发人员仍然需要遵守一些约定以避免错误.

A lot of the common mistakes we saw have to do with Rust being a systems programming language: In computing systems, a lot of operations are performance critical and inherently unsafe. We are dealing with external systems outside of our control, such as the operating system, hardware, or the network. The goal is to build safe abstractions on top of an unsafe world.

我们看到的许多常见错误都与 Rust 是一种系统编程语言有关: 在计算系统中, 很多操作都对性能至关重要, 本质上是不安全的. 我们正在处理我们无法控制的外部系统, 例如作系统、硬件或网络. 我们的目标是在这不安全的世界之上构建安全的抽象.

Rust shares an FFI interface with C, which means that it can do anything C can do. So, while some operations that Rust allows are theoretically possible, they might lead to unexpected results.

Rust 与 C 共享 FFI 接口规范, 这实际上意味着它可以做 C 可以做的任何事情. 因此, 即便 Rust 允许的一些操作也可能会导致意外结果.

But not all is lost! If you are aware of these pitfalls, you can avoid them, and with the above clippy lints, you can catch most of them at compile time.

亡羊补牢! 如果你意识到这些陷阱, 你可以轻松避免它们, 使用上述 clippy lints, 你可以在编译时捕获其中的大部分.

That’s why testing, linting, and fuzzing are still important in Rust.

这就是为什么测试、linting 和模糊测试在 Rust 中仍然很重要.

For maximum robustness, combine Rust’s safety guarantees with strict checks and strong verification methods.

为了获得最大的稳健性, 请将 Rust 的安全保证与严格的检查和强大的验证方法相结合.


  1. There’s also methods for wrapping and saturating arithmetic, which might be useful in some cases. It’s worth it to check out the std::intrinsics documentation to learn more.
    此外, 还有用于有目的地回绕 (wrap, 即发生溢出时结果从另一端重新开始计算) 或截断至边界 (saturate, 即溢出时结果取边界值) 的算术方法, 在某些情况下可能很有用. 查阅 std::intrinsics 文档以了解更多信息. ↩2

  2. Port 0 usually means that the OS will assign a random port for you. So, TcpListener::bind("127.0.0.1:0").unwrap() is valid, but it might not be supported on all operating systems or it might not be what you expect. See the TcpListener::bind docs for more info.
    端口 0 通常意味着作系统将为您分配一个随机端口。因此, TcpListener::bind("127.0.0.1:0").unwrap() 是有效的, 但可能并非在所有系统上都支持它, 或者它可能不是您所期望的. 有关更多信息, 请参阅 TcpListener::bind 的文档. ↩2

This Week in Rust (TWiR) Rust 语言周刊中文翻译计划, 第 595 期

本文翻译自 Oleksandr Prokhorenko 的博客文章 https://minikin.me/blog/rust-type-system-deep-dive, 英文原文版权由原作者所有, 中文翻译版权遵照 CC BY-NC-SA 协议开放. 如原作者有异议请邮箱联系.

相关术语翻译依照 Rust 语言术语中英文对照表.

囿于译者自身水平, 译文虽已力求准确, 但仍可能词不达意, 欢迎批评指正.

2025 年 4 月 19 日晚, 于北京.

GitHub last commit

Rust Type System Deep Dive From GATs to Type Erasure

Rust 类型系统深入探讨: 从 GAT 到类型擦除

TOC

(译者注: mdBook 的目录支持不行, 手动一下吧)

Introduction | 前言

Have you ever stared at a complex Rust error involving lifetimes and wondered if there’s a way to bend the type system to your will? Or perhaps you’ve built a library that relied on runtime checks, wishing you could somehow encode those constraints at compile time?

在遇到一个涉及生命周期的复杂 Rust 错误时, 你曾否红温, 想知道是否有办法让类型系统按照你的意愿让步? 或者, 也许您已经构建了一个依赖于运行时检查的库, 希望可以在编译时以某种方式对这些约束进行编码?

You’re not alone.

您并不孤单.

Rust’s type system is like an iceberg — most developers only interact with the visible 10% floating above the water. But beneath the surface lies a world of powerful abstractions waiting to be harnessed.

Rust 的类型系统就像一座冰山, 大多数开发人员只与漂浮在水面上的可见部分交互, 这部分可能仅占一成. 在水面下, 隐藏着一个等待被利用的强大抽象世界.

In this post, we’ll dive deep beneath the surface to explore five advanced type system features that can transform how you design Rust code:

在这篇文章中, 我们将深入探讨水面下的五个高级类型系统功能, 它们可以改变你设计 Rust 代码的方式:

  • Generic Associated Types (GATs) — The feature that took 6+ years to stabilize, enabling entirely new categories of APIs

    泛型关联类型(GATs): 这一历时 6 年多才得以稳定的特性, 为 API 带来全新的可能性

  • Advanced Lifetime Management — Techniques to express complex relationships between references

    高级生命周期管理: 引用之间复杂的生命周期关联

  • Phantom Types — Using “ghost” type parameters to encode states with zero runtime cost

    虚类型: 使用鬼魂般的类型参数定义状态, 而运行时的成本为零

  • Typeclass Patterns — Bringing functional programming’s power to Rust’s trait system

    类型类模式: 函数式编程赋能 Rust 的 trait 系统

  • Zero-Sized Types (ZSTs) — Types that exist only at compile time but provide powerful guarantees

    零大小类型: 仅在编译时存在但提供强大保证的类型

  • Type Erasure Techniques — Methods to hide implementation details while preserving behavior

    类型擦除: 在保留类型行为时隐藏实现细节.

Why should you care about these advanced patterns? Because they represent the difference between:

为什么您应该关注这些高级模式? 因为它们代表了以下两者之间的区别:

  • Runtime checks vs. compile-time guarantees

    运行时检查 vs 编译时保证

  • Documentation comments vs. compile errors for incorrect usage

    文档注释 vs 错误使用时的强制编译错误

  • Hoping users read your docs vs. ensuring they can’t misuse your API

    希望用户阅读您的文档与确保他们不会滥用您的 API

Let’s begin our journey into the depths of Rust’s type system. By the end, you’ll have new tools to craft APIs that are both more expressive and more robust.

让我们开始深入了解 Rust 类型系统的旅程吧. 到最后, 您将拥有新的工具来给出更具表现力和更健壮的 API.

Generic Associated Types (GATs) | 泛型关联类型

The Long Road to Stabilization | 通往稳定的漫漫长路

“Is it possible to define a trait where the associated type depends on the self lifetime?”

“是否可以定义一个 trait, 其关联类型取决于 self 的生命周期?”

This seemingly innocent question, asked over and over in the Rust community for years, pointed to a critical gap in Rust’s type system. Generic Associated Types (GATs) represent one of Rust’s most anticipated features, finally stabilized in Rust 1.65 after more than six years in development.

这个看似平平无奇的问题, 在 Rust 社区中被一遍又一遍地问了多年, 它指出了 Rust 类型系统中的一个关键差距. 泛型关联类型 (GAT) 是 Rust 最受期待的功能之一, 经过六年多的开发, 终于在 Rust 1.65 中稳定下来.

The journey to stabilization wasn’t just a matter of implementation — it involved fundamental questions about Rust’s type system design. You might wonder: what kind of feature takes more than half a decade to implement? The answer: one that touches the very core of how generics, traits, and lifetimes interact.

这不仅仅是一个实现问题: 它涉及关于 Rust 类型系统设计的基本问题. 您可能想知道: 什么样的功能需要五年多的时间才能实现? 答案是, 一个触及泛型、特质 (trait) 和生命周期如何交互的核心功能.

What Are GATs? | 泛型关联类型是什么?

Before GATs, you found yourself trapped in situations like this:

在没有 GAT 的时候, 您可能遇到这样的情况:

#![allow(unused)]
fn main() {
trait Container {
    type Item;

    fn get(&self) -> Option<&Self::Item>;
}
}

This seems reasonable until you try implementing it for a type like Vec<T>:

看上去没问题, 直到您尝试为像 Vec<T> 这样的类型实现它:

#![allow(unused)]
fn main() {
impl<T> Container for Vec<T> {
    type Item = T;

    fn get(&self) -> Option<&Self::Item> {
        // Wait... this doesn't quite work!
        // The lifetime of the returned reference comes from `&self`
        // but our associated type doesn't know about that lifetime
        self.first()
    }
}
}

With GATs, we can make associated types aware of lifetimes:

使用 GAT, 我们可以让关联类型知道生命周期:

#![allow(unused)]
fn main() {
trait Container {
    type Item<'a>
    where
        Self: 'a;
    fn get<'a>(&'a self) -> Option<Self::Item<'a>>;
}

impl<T> Container for Vec<T> {
    type Item<'a>
        = &'a T
    where
        Self: 'a;

    fn get<'a>(&'a self) -> Option<Self::Item<'a>> {
        self.first()
    }
}
}

This seemingly small addition unlocks entirely new categories of APIs that were previously impossible or required unsafe workarounds.

这个看似很小的新增功能解锁了以前不可能或需要不安全解决方法的全新 API 类别.

tip

Key Takeaway

GATs let you create associated types that can reference the lifetime of &self, allowing for APIs that were previously impossible to express safely.

划重点

GAT 允许您创建可以引用 &self 生命周期的关联类型, 从而让以前无法以 safe 方法表达的 API 成为可能.

(译者注: 这点或许那些 2021 edition 入坑的 Rust 开发者感受不深了, 包括我)

Real-world Example: A Collection Iterator Factory

实际示例: 集合迭代器工厂模式

Let’s see how GATs enable elegant APIs for creating iterators with different lifetimes:

让我们看看 GAT 如何让我们能提供优雅的 API 来创建具有不同生命周期的迭代器:

#![allow(unused)]
fn main() {
trait CollectionFactory {
    type Collection<'a> where Self: 'a;
    type Iterator<'a>: Iterator where Self: 'a;

    fn create_collection<'a>(&'a self) -> Self::Collection<'a>;
    fn iter<'a>(&'a self) -> Self::Iterator<'a>;
}

struct VecFactory<T>(Vec<T>);

impl<T: Clone> CollectionFactory for VecFactory<T> {
    type Collection<'a> = Vec<T> where T: 'a;
    type Iterator<'a> = std::slice::Iter<'a, T> where T: 'a;

    fn create_collection<'a>(&'a self) -> Vec<T> {
        self.0.clone()
    }

    fn iter<'a>(&'a self) -> std::slice::Iter<'a, T> {
        self.0.iter()
    }
}
}

Before GATs, this pattern would have required boxing, unsafe code, or simply wouldn’t have been possible. Now it’s type-safe and zero-cost.

在 GAT 实装前, 这种模式需要装箱 (Box 堆分配)、编写 unsafe 代码, 或者根本不可能. 现在, 它是类型安全且零成本的.

Think of GATs as the tool that lets you build APIs that adapt to their context — rather than forcing users to adapt to your API.

将 GAT 视为一种工具, 可让您构建适应其上下文的 API, 而不是强迫用户适应您的 API.

Advanced Lifetime Management | 高级生命周期管理

Lifetimes in Rust are like the air we breathe — essential, ever-present, but often invisible until something goes wrong. Advanced lifetime management gives you the tools to work with this invisible force.

Rust 中的生命周期就像我们呼吸的空气, 是必不可少的、永恒的, 但直到出现问题之前都难被被意识到的存在. 高级生命周期管理为您提供了与这股无形力量合作的工具.

Higher-Rank Trait Bounds (HRTBs)

(译者注: 专有名词组合, 不译, 后面简称 HRTB)

You’ve likely encountered this cryptic syntax before, maybe in compiler errors:

您以前可能遇到过这种神秘的语法, 可能是在编译器错误提示中:

for<'a> T: Trait<'a>

This strange-looking for<’a> is the gateway to higher-rank trait bounds. But what does it actually mean?

这个看起来很奇怪的 for<'a> 是通往更高等级 trait 约束的门户. 但它实际上意味着什么?

Imagine you’re writing an API to parse strings:

假设您正在编写一个 API 来解析字符串, (伪代码如下):

#![allow(unused)]
fn main() {
trait Parser {
    fn parse(&self, input: &str) -> Output;
}
}

But wait — the input’s lifetime is tied to the function call, not the trait definition. Traditional generics can’t express this relationship properly. Instead, we need HRTBs:

但是等等: 我们期望的是输入的生命周期与函数调用相关联, 而不是 trait 的定义. 传统泛型无法正确表达这种关系. 在这种情况下, 我们需要 HRTB:

#![allow(unused)]
fn main() {
trait Parser {
    fn parse<F, O>(&self, f: F) -> O
    where
        F: for<'a> FnOnce(&'a str) -> O;
}
}

Now we can implement the Parser trait for our SimpleParser:

现在我们可以为 SimpleParser 实现 Parser trait:

#![allow(unused)]
fn main() {
struct SimpleParser;

impl Parser for SimpleParser {
    fn parse<F, O>(&self, f: F) -> O
    where
        F: for<'a> FnOnce(&'a str) -> O,
    {
        let data = "sample data";
        f(data)
    }
}
}

The for<'a> syntax is a universal quantification over lifetimes, meaning “for all possible lifetimes ‘a”. It’s saying that F must be able to handle a string slice with any lifetime, not just a specific one determined in advance.

for<'a> 语法是生命周期的通用量化, 含义是 “对于所有可能的生命周期 'a”…

tip

译者碎碎念

这里的例子写得莫名其妙的.

请看最简单的代码示例, 可以尝试运行看看报什么错:

#![allow(unused)]
fn main() {
fn call_on_ref_zero<'a, F>(f: F)
where
   F: Fn(&'a i32)
{
    let zero = 0;
    f(&zero);
}
}

能看出来问题吗? F 接受的引用生命周期和 call_on_ref_zero 关联, 引用至少得活得比 call_on_ref_zero 执行时全程一样久.

问题来了: call_on_ref_zero 函数块内的局部变量呢? 这就和我们的设计要求不符合了.

修改方法很简单:

#![allow(unused)]
fn main() {
fn call_on_ref_zero<F>(f: F)
where
     F: for <'a> Fn(&'a i32)
{
    let zero = 0;
    f(&zero);
}
}

含义是告诉编译器, Fn 接受的引用 比 Fn 活得久就行(接受任意可能的生命周期 'a).

这就是所谓 HRTB.

tip

🔑 Key Takeaway: Higher-rank trait bounds let you express relationships between lifetimes that can’t be captured with simple lifetime parameters, enabling more flexible APIs.

🔑 关键要点: HRTB 允许您表达生命周期之间的关系, 这些关系本无法使用简单的生命周期参数捕获. 从而实现更灵活的 API.

Lifetime Variance and 'static | 生命周期型变和 'static

tip

译者注

variance 中文译法不一, 此处译为 型变.

本节搭配 Rust 死灵书对应 子类型和型变 章节阅读更佳.

Imagine you’re designing an authentication system:

假设您正在设计一个身份验证系统:

#![allow(unused)]
fn main() {
struct AdminToken<'a>(&'a str);
struct UserToken<'a>(&'a str);

fn check_admin_access(token: AdminToken) -> bool {
    // Verification logic
    true
}
}

A critical question arises: Could someone pass a UserToken where an AdminToken is required? The answer depends on variance.

一个关键问题出现了: 有人可以在需要 AdminToken 的地方传递 UserToken 吗? 答案取决于型变 (variance).

tip

译者注

可能这里看着有点莫名其妙, 这不显而易见不是一个类型吗? 不过请耐心看下去.

Variance determines when one type can be substituted for another based on their lifetime relationships:

型变根据类型的生命周期关系确定何时可以将一种类型替换为另一种类型:

  • Covariant: If 'a outlives 'b, then T<'a> can be used where T<'b> is expected (most common)

    协变 (covariant): 最常见的情况是, 若 'a'b 长寿, 则可以在预期 T<'b> 的地方使用 T<'a>

  • Contravariant: The opposite relationship

    逆变 (contravariant): 相反的关系.

  • Invariant: No substitution is allowed (critical for security)

    不变 (invariant): 不允许替换 (对安全性至关重要).

For example, &'a T is covariant over 'a, meaning you can use a longer-lived reference where a shorter-lived one is expected:

例如, &'a T'a 是协变的, 这意味着你可以使用寿命较长的引用, 而预期引用的引用寿命较短:

#![allow(unused)]
fn main() {
fn needs_short_lived<'a, 'b: 'a>(data: &'a u32) {
    // Some code
}


fn provide_longer_lived<'long>(long_lived: &'long u32) {
    needs_short_lived(long_lived); // This works because of covariance
}
}

Understanding these relationships becomes essential when designing APIs that deal with sensitive resources or complex lifetime interactions.

在设计处理敏感资源或复杂生命周期交互的 API 时, 了解这些关系变得至关重要.

译者补充

这里非常抽象, 再次建议搭配 Rust 死灵书 - 子类型与型变 一节阅读.

子类型化是隐式的, 可以出现在类型检查或类型推断的任何阶段.

Rust 中的子类型化的概念仅出现在和生命周期的型变以及 HRTB 这两个地方. 如果我们擦除了类型的生命周期, 那么唯一的子类型化就只是类型相等 (type equality) 了.

对于两个生命周期 ’a, ’b: 更长寿那个被称为子类型, 更短寿那个被称为父类型. 子类型化规则是可以生命周期相对短的地方使用生命周期长的类型(用父类型代替子类型):

#![allow(unused)]
fn main() {
fn bar<'a>() {
    let s: &'static str = "hi";
    let t: &'a str = s;
}
}

上面的例子, s 具备最长的生命周期 ('static), 但我们能在要求的生命周期更短的地方使用它, 这就是所谓的子类型化.

类似地: trait 类似:

#![allow(unused)]
fn main() {
// 这里 'a 被替换成了 'static
let subtype: &(for<'a> fn(&'a i32) -> &'a i32) = &((|x| x) as fn(&_) -> &_);
let supertype: &(fn(&'static i32) -> &'static i32) = subtype;

// 显然地, 我们也可以用一个 HRTB 来代替另一个, 这里可以理解为 'c 同时是 'a  和 'b 的子类型
let subtype: &(for<'a, 'b> fn(&'a i32, &'b i32))= &((|x, y| {}) as fn(&_, &_));
let supertype: &for<'c> fn(&'c i32, &'c i32) = subtype;

// 这对于 trait 对象也是类似的. 注意 Fn 大写 F 是个 trait 哦.
let subtype: &(for<'a> Fn(&'a i32) -> &'a i32) = &|x| x;
let supertype: &(Fn(&'static i32) -> &'static i32) = subtype;
}

泛型类型在它的某个参数上的型变描述了该参数的子类型化去如何影响此泛型类型的子类型化.

前面提到:

对于两个生命周期 ’a, ’b: 更长寿那个被称为子类型, 更短寿那个被称为父类型. 子类型化规则是可以生命周期相对短的地方使用生命周期长的类型(用父类型代替子类型)

这种子类型化规则被称为协变, 反之则为逆变, 不能代替则为不变.

在单个生命周期子类型化规则的基础上, 一些常见的泛型类型的型变规则如表格所示:

’aTU
&'a T 协变协变
&'a mut T协变不变
Box<T>协变
Vec<T>协变
UnsafeCell<T>不变
Cell<T>不变
fn(T) -> U协变
*const T协变
*mut T不变
  • 不可变引用 &'a T 中的 T 遵循 协变 的规则.

    作为泛型参数, T 自然也可以是一个引用之类的玩意, T 是实际类型不是引用咱不说(擦除生命周期了), 以 &'m K&'n K 为例, 已知 'm'n 长寿, 根据协变的规则, &'a &'m T 可以代替 &'a &'n T 用在要求参数 &'a &'n T 的地方.

    不可变原始指针 *const T 和不具有内部可变性的 Box<T> 等智能指针具有类似行为.

  • 可变引用 &'a mut T 中的 T 遵循 不变 的规则,

    可变原始指针 *mut T 和具有内部可变性 UnsafeCell, Cell 的智能指针具有类似行为.

  • 非常特殊地, 语言中仅有的 逆变 来自函数参数, 背后机制过于复杂, 译者也不会 (^_^).

  • 结构体、枚举、联合体 (union) 和元组 (tuple) 内的泛型参数的型变规则由其所有使用到该泛型参数的字段的型变关系联合决定.

    如果参数用在了多处且具有不同型变关系的位置上, 则该类型在该参数上是不变的.

    例如, 下面示例的结构体在 'aT 上是协变的, 在 'bU 上是不变的.

    #![allow(unused)]
    fn main() {
    use std::cell::UnsafeCell;
    struct Variance<'a, 'b, T, U: 'a> {
        x: &'a U,               // 整个结构体在 'a 上是协变的
        y: *const T,            // 在 T 上是协变的
        z: UnsafeCell<&'b f64>, // 在 'b 上是不变的
        w: *mut U,              // 虽然 &'a U 在 U 上是协变的, 但这里在 U 上是不变的, 导致整个结构体在 U 上是不变的
    }
    }

Phantom Types | 虚类型

Have you ever wished you could distinguish between two values of the same type but with different meanings? Consider these examples:

您是否曾经希望能够区分相同类型但含义不同的两个值? 请考虑以下示例:

// These are all just strings, but they have very different meanings!
let user_id = "usr_123456";
let order_id = "ord_789012";
let coupon_code = "KFCV50";

Nothing prevents you from accidentally mixing them up. This is where phantom types come in — they let you create type-level distinctions without runtime cost.

没有什么能阻止您不小心将它们混淆. 这就是虚类型的用武之地: 它们允许您创建类型差异, 而无需运行时成本.

Phantom types are type parameters that don’t appear in the data they parameterize:

虚类型是不会出现在它们参数化的数据中的类型参数:

#![allow(unused)]
fn main() {
use std::marker::PhantomData;

struct Token<State> {
    value: String,
    _state: PhantomData<Role>,
}
}

The PhantomData<State> field takes no space at runtime, but it creates a distinct type at compile time.

PhantomData<State> 字段在运行时不占用空间, 但它在编译时创建一个特定的类型.

tip

🔑 Key Takeaway: Phantom types allow you to encode additional information in the type system without adding any runtime overhead, creating distinctions that exist purely at compile time.

🔑 关键要点: 虚类型允许您充分利用类型系统制造纯粹在编译时存在的区别以实现特定功能, 而不会增加任何运行时开销.

State Machines at Compile Time | 编译时状态机

One of the most powerful applications of phantom types is encoding state machines directly in the type system:

虚类型最强大的应用之一是直接在类型系统中对状态机进行编码:

use std::marker::PhantomData;

struct Token<State> {
    value: String,
    _state: PhantomData<State>,
}

// States (empty structs)
struct Unvalidated;
struct Validated;

#[derive(Debug)]
// Validation error type
enum ValidationError {
    TooShort,
    InvalidFormat,
}

impl Token<Unvalidated> {
    fn new(value: String) -> Self {
        Token {
            value,
            _state: PhantomData,
        }
    }

    fn validate(self) -> Result<Token<Validated>, ValidationError> {
        // Perform validation
        if self.value.len() > 3 {
            Ok(Token {
                value: self.value,
                _state: PhantomData,
            })
        } else {
            Err(ValidationError::TooShort)
        }
    }
}

impl Token<Validated> {
    fn get(&self) -> &str {
        // Only callable on validated tokens
        &self.value
    }
}

fn main() {
    let token = Token::new("Hello".into());

    // 尝试注释这行再运行, 看看会报什么错!
    let token = token.validate().unwrap();

    println!("{}", token.get());
}

This pattern ensures that get() can only be called on tokens that have passed validation, with the guarantee enforced at compile time.

此模式确保 get() 只能在已通过验证的令牌上调用, 并在编译时强制执行保证.

Type-Level Validation | 类型级别的验证

Phantom types can encode domain-specific rules at the type level, essentially moving validation from runtime to compile time:

虚类型可以在类型级别指定特定规则, 实质上是将验证从运行时转移到编译时:

#![allow(unused)]
fn main() {
use std::marker::PhantomData;

struct UserId<Validated>(String, PhantomData<Validated>);
struct EmailAddress<Validated>(String, PhantomData<Validated>);

struct Unverified;
struct Verified;

trait Validator<T> {
    type Error;
    fn validate(value: String) -> Result<T, Self::Error>;
}

struct UserIdValidator;
impl Validator<UserId<Verified>> for UserIdValidator {
    type Error = String;

    fn validate(value: String) -> Result<UserId<Verified>, Self::Error> {
        if value.len() >= 3 && value.chars().all(|c| c.is_alphanumeric()) {
            Ok(UserId(value, PhantomData))
        } else {
            Err("Invalid user ID".to_string())
        }
    }
}

// Now functions can require verified types
fn register_user(id: UserId<Verified>, email: EmailAddress<Verified>) {
    // We know both ID and email have been validated
}
}

This approach creates a “validation firewall” — once data passes through validation, its type guarantees it’s valid throughout the rest of your program.

这种方法创建了一个 “验证防火墙”: 一旦数据通过验证, 其类型就会保证它在程序的其余部分都有效.

The Typeclass Pattern | 类型类模式

What if you could define behavior for types you don’t control?

如果您可以为不受您控制的类型定义行为, 那会怎样?

Haskell programmers have long enjoyed typeclasses, a powerful mechanism for defining interfaces that types can implement. Rust’s trait system offers similar capabilities, but we can go further to implement true typeclass-style programming.

Haskell 程序员长期以来一直喜欢类型类, 这是一种用于定义类型可以实现的接口的强大机制. Rust 的 trait 系统提供了类似的功能, 但我们可以更进一步地实现真正的类型类风格的编程.

What Are Typeclasses?

什么是类型类?

Imagine you’re building a serialization library and want to support many different formats. Without typeclasses, you’d need to:

假设您正在构建一个序列化库, 并希望支持许多不同的格式. 如果没有类型类, 你需要:

  • Create a trait

    创建一个 trait

  • Implement it for every type you own

    为您拥有的每种类型实施它

  • Hope other library authors implement it for their types

    希望其他库作者为他们的类型实现它

  • Resort to newtype wrappers for types you don’t control

    对你无法控制的类型求助于 newtype 包装器

In functional languages like Haskell, typeclasses solve this elegantly by allowing you to define behavior for any type, even ones you didn’t create. Rust’s trait system gives us similar power through “orphan implementations” (with some restrictions).

在像 Haskell 这样的函数式语言中, 类型类允许你为任何类型的类型定义行为, 即使是那些不是你创建的, 从而优雅地解决了这个问题. Rust 的 trait 系统通过 “孤儿实现” (有一些限制) 为我们提供了类似的能力.

The key components of typeclass patterns in Rust are:

Rust 中类型类模式的关键组件是:

  • Traits as interfaces

    作为接口的 trait

  • Trait implementations for existing types (including foreign types)

    现有类型 (包括外部类型) 的 trait 实现

  • Associated types or type parameters for related types

    相关类型的关联类型或类型参数

  • Trait bounds to express constraints

    用于表达约束的 trait 限定

tip

🔑 Key Takeaway: Typeclasses let you add behavior to types after they’re defined, enabling powerful generic programming.

🔑 关键要点: 类型类允许您在定义类型后向类型添加行为, 从而实现强大的泛型编程.

From Monoids to Semigroups | 从幺半群到半群

Let’s dive into some algebraic abstractions to see typeclasses in action:

让我们深入研究一些代数抽象, 看看类型类的实际应用:

#![allow(unused)]
fn main() {
trait Semigroup {
    fn combine(&self, other: &Self) -> Self;
}

trait Monoid: Semigroup + Clone {
    fn empty() -> Self;
}

// Implementing for built-in types
impl Semigroup for String {
    fn combine(&self, other: &Self) -> Self {
        let mut result = self.clone();
        result.push_str(other);
        result
    }
}

impl Monoid for String {
    fn empty() -> Self {
        String::new()
    }
}

// Product and Sum types for numbers
#[derive(Clone)]
struct Product<T>(T);

#[derive(Clone)]
struct Sum<T>(T);

impl<T: Clone + std::ops::Mul<Output = T>> Semigroup for Product<T> {
    fn combine(&self, other: &Self) -> Self {
        Product(self.0.clone() * other.0.clone())
    }
}

impl<T: Clone + std::ops::Mul<Output = T> + From<u8>> Monoid for Product<T> {
    fn empty() -> Self {
        Product(T::from(1))
    }
}

impl<T: Clone + std::ops::Add<Output = T>> Semigroup for Sum<T> {
    fn combine(&self, other: &Self) -> Self {
        Sum(self.0.clone() + other.0.clone())
    }
}

impl<T: Clone + std::ops::Add<Output = T> + From<u8>> Monoid for Sum<T> {
    fn empty() -> Self {
        Sum(T::from(0))
    }
}
}

You might be thinking: “That looks like a lot of boilerplate just to add strings or multiply numbers.” But the magic happens when we build generic algorithms that work with any type that implements our traits.

您可能会想: “这看起来就像很多样板, 只是为了添加字符串或乘以数字.” 但是, 当我们构建适用于实现我们 trait 的任何类型的通用算法时, 奇迹就会发生.

Leveraging Typeclasses for Generic Algorithms | 将类型类用于泛型算法

Once we have these abstractions, we can write algorithms that work with any Monoid, regardless of the actual data type:

一旦我们有了这些抽象, 我们就可以编写适用于任何幺半群的算法, 而不管实际数据类型如何:

#![allow(unused)]
fn main() {
fn combine_all<M: Monoid + Clone>(values: &[M]) -> M {
    values.iter().fold(M::empty(), |acc, x| acc.combine(x))
}

// Usage
let strings = vec![String::from("Hello, "), String::from("typeclasses "), String::from("in Rust!")];
let result = combine_all(&strings);
// "Hello, typeclasses in Rust!"

let numbers = vec![Sum(1), Sum(2), Sum(3), Sum(4)];
let sum_result = combine_all(&numbers);
// Sum(10)

let products = vec![Product(2), Product(3), Product(4)];
let product_result = combine_all(&products);
// Product(24)
}

With just a few lines of code, we’ve created a function that can concatenate strings, sum numbers, multiply numbers, or work with any other type that follows the Monoid abstraction. This is the power of typeclass-based programming!

只需几行代码, 我们就创建了一个函数, 它可以连接字符串、求和、乘以数字, 或者使用遵循幺半群抽象的任何其他类型. 这就是基于类型类的编程的强大之处!

Zero-Sized Types (ZSTs) | 零大小类型

Zero-sized types (ZSTs) are types that occupy no memory at runtime but carry semantic meaning at compile time. They’re a powerful tool for type-level programming without runtime overhead.

零大小类型 (ZST) 是在运行时不占用内存但在编译时具有语义含义的类型. 它们是类型级编程的强大工具, 无运行时开销.

What Are Zero-Sized Types? | 什么是零大小类型?

A ZST is any type that requires 0 bytes of storage. Common examples include:

ZST 是不需要任何存储空间的任何类型. 常见示例包括:

  • Empty structs: struct Marker;

    空结构体: struct Marker;

  • Empty enums: enum Void {}

    空枚举: enum Void {}

  • PhantomData: PhantomData<T>

  • Unit type: ()

    单元类型: ()

Despite taking no space, ZSTs provide valuable type information to the compiler.

尽管不占用空间, 但 ZST 为编译器提供了有价值的类型信息.

Marker Types | 标记类型

One common use of ZSTs is as marker types to implement compile-time flags:

ZST 的一个常见用途是作为标记类型来实现编译时标志:

#![allow(unused)]
fn main() {
// Markers for access levels
struct ReadOnly;
struct ReadWrite;

struct Database<Access> {
    connection_string: String,
    _marker: PhantomData<Access>,
}

impl<Access> Database<Access> {
    fn query(&self, query: &str) -> Vec<String> {
        // Common query logic
        vec![format!("Result of {}", query)]
    }
}

impl Database<ReadWrite> {
    fn execute(&self, command: &str) -> Result<(), String> {
        // Only available in read-write mode
        Ok(())
    }
}

// Usage
let read_only_db = Database::<ReadOnly> {
    connection_string: "sql://readonly".to_string(),
    _marker: PhantomData,
};

let read_write_db = Database::<ReadWrite> {
    connection_string: "sql://admin".to_string(),
    _marker: PhantomData,
};

read_only_db.query("SELECT * FROM users");
// read_only_db.execute("DROP TABLE users"); // Won't compile!
read_write_db.execute("INSERT INTO users VALUES (...)"); // Works
}

Type-Level State Machines with ZSTs | 利用 ZST 的类型状态机

ZSTs excel at encoding state machines where state transitions happen at compile time:

ZST 擅长描述状态, 状态转换发生在编译时:

#![allow(unused)]
fn main() {
// States
struct Draft;
struct Published;
struct Archived;

// Post with type-level state
struct Post<State> {
    content: String,
    _state: PhantomData<State>,
}

// Operations available on Draft posts
impl Post<Draft> {
    fn new(content: String) -> Self {
        Post {
            content,
            _state: PhantomData,
        }
    }

    fn edit(&mut self, content: String) {
        self.content = content;
    }

    fn publish(self) -> Post<Published> {
        Post {
            content: self.content,
            _state: PhantomData,
        }
    }
}

// Operations available on Published posts
impl Post<Published> {
    fn get_views(&self) -> u64 {
        42 // Placeholder
    }

    fn archive(self) -> Post<Archived> {
        Post {
            content: self.content,
            _state: PhantomData,
        }
    }
}

// Operations available on Archived posts
impl Post<Archived> {
    fn restore(self) -> Post<Draft> {
        Post {
            content: self.content,
            _state: PhantomData,
        }
    }
}
}

Type-Level Integers and Dimensional Analysis | 类型级整数和维度分析

With const generics, we can use ZSTs to encode types with numeric properties:

借助 const 泛型, 我们可以使用 ZST 对具有数字属性的类型进行编码:

#![allow(unused)]
fn main() {
// Type-level integers with const generics
struct Length<const METERS: i32, const CENTIMETERS: i32>;

// Type-level representation of physical quantities
impl<const M: i32, const CM: i32> Length<M, CM> {
    // A const function to calculate total centimeters (for demonstration)
    const fn total_cm() -> i32 {
        M * 100 + CM
    }
}

// Type-safe addition using type conversion rather than type-level arithmetic
fn add<const M1: i32, const CM1: i32, const M2: i32, const CM2: i32>(
    _: Length<M1, CM1>,
    _: Length<M2, CM2>,
) -> Length<3, 120> {
    // Using fixed return type for the example
    // In a real implementation, we would define constant expressions and
    // use const generics with a more flexible type, but that gets complex
    Length
}

// Usage
let a = Length::<1, 50> {};
let b = Length::<2, 70> {};
let c = add(a, b); // Type is Length<3, 120>
}

Optimizations with ZSTs | 使用 ZST 进行优化

Because ZSTs take no space, the compiler can optimize away operations with them while preserving their type-level semantics:

由于 ZST 不占用空间, 因此编译器可以在保留其类型级语义的同时优化掉它们:

  • Collections of ZSTs take no space

    ZST 集合不占用空间

  • Functions returning ZSTs are optimized to simple jumps

    返回 ZST 的函数优化为简单跳转

  • Fields of type ZST don’t increase struct size

    ZST 类型的字段不会增加结构体大小

This makes ZSTs perfect for:

这使得 ZST 非常适合:

  • Type-level programming

    类型级编程

  • Differentiating between identical data layouts with different semantics

    区分具有不同语义的相同数据布局

  • Building extensible APIs with marker traits

    构建具有标记特征的可扩展 API

Type Erasure Patterns | 类型擦除模式

Type erasure is a powerful technique for hiding concrete types behind abstract interfaces while maintaining type safety. In Rust, there are several ways to implement type erasure, each with different trade-offs.

类型擦除是一种强大的技术, 用于在保持类型安全的同时将具体类型隐藏在抽象接口后面. 在 Rust 中, 有几种方法可以实现类型擦除, 每种方法都有不同的权衡.

Understanding Type Erasure | 了解类型擦除

Type erasure refers to the process of “erasing” or hiding concrete type information while preserving the necessary behavior. This allows for:

类型擦除是指在保留必要行为的同时 “擦除” 或隐藏具体类型信息的过程.这允许:

  • Handling multiple types uniformly

    统一处理多种类型

  • Creating heterogeneous collections

    创建异构集合

  • Simplifying complex generic interfaces

    简化复杂的通用接口

  • Providing abstraction boundaries in APIs

    在 API 中提供抽象边界

Dynamic Trait Objects | 动态特征对象

The most common form of type erasure in Rust uses trait objects with dynamic dispatch:

Rust 中最常见的类型擦除形式是使用带有动态分派的 trait 对象:

#![allow(unused)]
fn main() {
trait Drawable {
    fn draw(&self);
    fn bounding_box(&self) -> BoundingBox;
}

struct BoundingBox {
    x: f32,
    y: f32,
    width: f32,
    height: f32,
}

struct Rectangle {
    x: f32,
    y: f32,
    width: f32,
    height: f32,
}

impl Drawable for Rectangle {
    fn draw(&self) {
        // Draw the rectangle
    }

    fn bounding_box(&self) -> BoundingBox {
        BoundingBox {
            x: self.x,
            y: self.y,
            width: self.width,
            height: self.height,
        }
    }
}

struct Circle {
    x: f32,
    y: f32,
    radius: f32,
}

impl Drawable for Circle {
    fn draw(&self) {
        // Draw the circle
    }

    fn bounding_box(&self) -> BoundingBox {
        BoundingBox {
            x: self.x - self.radius,
            y: self.y - self.radius,
            width: self.radius * 2.0,
            height: self.radius * 2.0,
        }
    }
}
}

Now we can create a Canvas that can hold different types of Drawable objects:

现在我们可以创建一个可以容纳不同类型 Drawable 对象的 Canvas:

#![allow(unused)]
fn main() {
struct Canvas {
    // A collection of drawable objects with different concrete types
    elements: Vec<Box<dyn Drawable>>,
}

impl Canvas {
    fn new() -> Self {
        Canvas {
            elements: Vec::new(),
        }
    }

    fn add_element<T: Drawable + 'static>(&mut self, element: T) {
        self.elements.push(Box::new(element));
    }

    fn draw_all(&self) {
        for element in &self.elements {
            element.draw();
        }
    }
}
}

This approach uses runtime polymorphism (vtables) to call the correct implementation. The concrete type is erased, but at the cost of dynamic dispatch and heap allocation.

此方法使用运行时多态性 (虚表, vtables) 来调用正确的实现. 具体类型被擦除, 但代价是动态调度和堆分配.

The Object-Safe Trait Pattern | 对象安全 trait 模式

Creating object-safe traits requires careful design:

创建对象安全的 trait 需要仔细设计:

important

“对象安全” 的说法不够严谨, 现已改称 dyn 兼容性” (dyn compatibility)

规则简述如下:

  • 父 trait 是 dyn 兼容的.
  • 不能有 Sized 的约束, 包括其关联函数.
  • 不能有关联常数, 关联类型不能带泛型参数.
  • 关联函数相关要求
    • 不能带泛型参数.
    • 接受器类型只能是 &Self (即 &self), &mut Self (即 &mut self), Box<Self>, Rc<Self>, Arc<Self> 以及 Pin<P>, P 是前述类型之一.
    • 除接收器外, 不使用 Self 作为参数.
    • 返回类型不能为不透明类型 (opaque type), 如: 不支持 RPIT (自然不支持 async, AFIT).
    • 对于不支持动态分发的方法, 可以显式添加 Self: Sized 约束以排除之.
#![allow(unused)]
fn main() {
// Non-object-safe trait with generic methods
trait NonObjectSafe {
    fn process<T>(&self, value: T);
}

// Object-safe wrapper
trait ObjectSafe {
    fn process_i32(&self, value: i32);
    fn process_string(&self, value: String);
    // Add concrete methods for each type you need
}

fn _assert_dyn_capable(_x: Box<dyn ObjectSafe>) {}

// Bridge implementation
impl<T: NonObjectSafe> ObjectSafe for T {
    fn process_i32(&self, value: i32) {
        self.process(value);
    }

    fn process_string(&self, value: String) {
        self.process(value);
    }
}
}

This pattern allows you to create trait objects from traits that would otherwise not be object-safe, at the cost of some flexibility.

这种模式允许您从原本不是对象安全的 trait 创建 trait 对象, 但代价是丧失一定的灵活性.

Building Heterogeneous Collections | 构建异构集合

Type erasure is particularly useful for creating collections of different types:

类型擦除对于创建不同类型的集合特别有用:

trait Message {
    fn process(&self);
}

// Type-erased message holder
struct AnyMessage {
    inner: Box<dyn Message>,
}

// Specific message types
struct TextMessage(String);
struct BinaryMessage(Vec<u8>);

impl Message for TextMessage {
    fn process(&self) {
        println!("Processing text: {}", self.0);
    }
}

impl Message for BinaryMessage {
    fn process(&self) {
        println!("Processing binary data of size: {}", self.0.len());
    }
}

// Usage
fn main() {
    let messages: Vec<AnyMessage> = vec![
        AnyMessage { inner: Box::new(TextMessage("Hello".to_string())) },
        AnyMessage { inner: Box::new(BinaryMessage(vec![1, 2, 3, 4])) },
    ];

    for msg in messages {
        msg.inner.process();
    }
}

For performance-critical code, you might use an enum-based approach instead:

对于性能要求高的代码, 您可以改用基于枚举的方法:

#![allow(unused)]
fn main() {
enum MessageKind {
    Text(String),
    Binary(Vec<u8>),
}

impl MessageKind {
    fn process(&self) {
        match self {
            MessageKind::Text(text) => {
                println!("Processing text: {}", text);
            }
            MessageKind::Binary(data) => {
                println!("Processing binary data of size: {}", data.len());
            }
        }
    }
}
}

This approach avoids the dynamic dispatch overhead but requires enumerating all possible types upfront.

这种方法避免了动态调度开销, 但需要预先枚举所有可能的类型.

Conclusion | 结论

We’ve journeyed deep into Rust’s type system, exploring powerful Rust features. Let’s recap what we’ve discovered:

我们深入研究了 Rust 的类型系统, 探索了强大的 Rust 功能.让我们回顾一下我们的发现:

  • Generic Associated Types (GATs) — The feature years in the making that lets you create associated types that depend on lifetimes, enabling entirely new categories of safe APIs.

    泛型关联类型: 历经多年开发的功能可让您创建依赖于生命周期的关联类型, 从而启用全新的安全 API 类别.

  • Advanced Lifetime Management — Techniques like higher-rank trait bounds and lifetime variance that give you fine-grained control over how references relate to each other.

    高级生命周期管理: HRTB 和生命周期型变等技术, 可让您精细控制引用之间的相互关系.

  • Phantom Types — “Ghost” type parameters that take no space at runtime but create powerful type distinctions, perfect for encoding state machines and validation requirements.

    虚类型: 像鬼魂一样的参数, 在运行时不占用空间, 但可以创建强大的类型区分, 非常适合对状态机和验证要求进行编码.

  • Typeclass Patterns — Functional programming techniques brought to Rust, enabling highly generic code that works across different types through trait abstraction.

    类型类模式: 引入 Rust 的函数式编程技术, 通过特征抽象实现跨不同类型工作的高度通用代码.

  • Zero-Sized Types (ZSTs) — Types that exist only at compile time but provide powerful guarantees with zero runtime cost, from marker traits to dimensional analysis.

    零大小类型: 仅在编译时存在, 以零运行时成本提供强大保证的类型, 从标记 trait 到维度分析.

  • Type Erasure Techniques — Methods to hide implementation details while preserving behavior, essential for creating clean API boundaries and heterogeneous collections.

    类型擦除技术: 在保留行为的同时隐藏实现细节的方法, 这对于创建干净的 API 边界和异构集合至关重要.

So what should you do with this knowledge?

那么你应该如何利用这些知识呢?

The next time you find yourself writing:

下次你发现自己在写这些代码时:

  • Runtime checks that could be compile-time guarantees

    可以转换为编译时保证的运行时检查

  • Documentation about how API functions must be called in a certain order

    有关如何必须按特定顺序调用 API 函数的文档

  • Warning comments about not mixing up similar-looking values

    关于不要混淆外观相似的值的警告注释

  • Complex validation logic scattered throughout your codebase

    分散在整个代码库中的复杂验证逻辑

…consider whether one of these type system features could solve your problem more elegantly.

…考虑一下这些类型系统功能之一是否可以更优雅地解决您的问题.

The beauty of Rust’s type system is that it turns the compiler into your ally. Instead of fighting with it, you can teach it to catch your domain-specific errors before your code even runs.

Rust 类型系统的美妙之处在于它将编译器变成了你的盟友. 您可以教它在代码运行之前捕获特定于域的错误, 而不是与它斗争.

This Week in Rust (TWiR) Rust 语言周刊中文翻译计划, 第 595 期

本文翻译自 Fernando Borretti 的博客文章 https://borretti.me/article/two-years-of-rust, 英文原文版权由原作者所有, 中文翻译版权遵照 CC BY-NC-SA 协议开放. 如原作者有异议请邮箱联系.

相关术语翻译依照 Rust 语言术语中英文对照表.

囿于译者自身水平, 译文虽已力求准确, 但仍可能词不达意, 欢迎批评指正.

2025 年 5 月 31 日下午, 于北京.

GitHub last commit

我使用 Rust 的这两年

I recently wrapped up a job where I spent the last two years writing the backend of a B2B SaaS product in Rust, so now is the ideal time to reflect on the experience and write about it.

我最近结束了一份工作, 过去两年我一直在用 Rust 编写一个 B2B SaaS 产品的后端. 现在是回顾这段经历并落笔记录下它的理想时机.

Contents | 目录

Learning | Rust 学习之路

I didn’t learn Rust the usual way: by reading tutorials, or books; or writing tiny projects. Rather, I would say that I studied Rust, as part of the research that went into building Austral. I would read papers about Rust, and the specification, and sometimes I’d go on the Rust playground and write a tiny program to understand how the borrow checker works on a specific edge case.

我入门 Rust 的方式并不寻常: 既没有阅读教程或书籍, 也没有编写过小型项目. 更准确地说, 我是在研究构建 Austral 语言的过程中, 将 Rust 作为研究对象来学习的. 我会阅读关于 Rust 的论文和规范文档, 有时还会在 Rust Playground 上编写小程序, 以理解借用检查器在特定边缘情况下的工作原理.

So, when I started working in Rust, my knowledge was very lopsided: I had an encyclopedic knowledge of the minutiae of the borrow checker, and couldn’t have told you how to write “Hello, world!”. The largest Rust program I had written was maybe 60 lines of code and it was to empirically test how trait resolution works.

所以, 当我开始使用 Rust 工作时, 难免有点 “眼高手低”: 我对借用检查器的细节了如指掌, 却不知道如何写 “Hello, world!”. 我写过的最长的 Rust 程序大概只有 60 行代码, 那是为了测试验证 trait 解析是如何工作的.

This turned out fine. Within a day or two I was committing changes. The problem is when people ask me for resources to learn Rust, I draw a blank.

结果还不错, 一两天内我就开始提交我的补丁了. 问题是, 当有人问我学习 Rust 的资源时, 我却一时想不起来.

The Good | 优点

The way I would summarize Rust is: it’s a better Go, or a faster Python. It’s fast and statically-typed, it has SOTA tooling, and a great ecosystem. It’s not hard to learn. It’s an industrial language, not an academic language, and you can be immensely productive with it. It’s a general-purpose language, so you can build backends, CLIs, TUIs, GUIs, and embedded firmware. The two areas where it’s not yet a good fit are web frontends (though you can try) and native macOS apps.

我认为 Rust 是一个更好的 Go, 或者更快的 Python. 它速度快且静态类型化, 拥有最先进的工具链和强大的生态系统. 学习起来并不困难. 它是一门工业级语言, 而非学术语言, 使用它可以极大提升生产力. 作为通用编程语言, 你可以用它构建后端CLITUIGUI 以及嵌入式固件. 目前它还不适合的两个领域是网页前端 (尽管可以尝试) 和原生 macOS 应用开发.

Performance | 性能

Rust is fast.

Rust 很快.

You can write slow code in any language: quadratic loops and n+1 queries and bad cache usage. But these are discrete bottlenecks. In Rust, when you fix the bottlenecks, the program is fast.

你可以用任何语言写出垃圾代码, 譬如嵌套循环、n+1 查询和糟糕的缓存操作. 但这些是散在的瓶颈. 在 Rust 中, 当你修复这些瓶颈时, 程序就会变得很快.

In other languages performance problems are often pervasive, so e.g. in Python it’s very common to have a situation where you’ve fixed all the bottlenecks—and everything is still unacceptably slow. Why? Because in Python the primitives are 10x to 100x slower than in Rust, and the composition of slow primitives is a slow program. No matter how much you optimize within the program, the performance ceiling is set by the language itself.

在其他语言中, 性能问题往往普遍存在. 例如, 在 Python 中, 经常会出现这样的情况: 你已经解决了所有的瓶颈, 但程序仍然慢得无法接受. 为什么? 因为在 Python 中, 基本操作的执行速度比 Rust 慢 10 到 100 倍, 而由这些慢速基本操作组成的程序自然也是慢的. 无论你在程序内部如何优化, 性能的上限都是由语言本身决定的.

And when you find yourself in that situation, what is there to do? You can scale the hardware vertically, and end up like those people who spend five figures a month on AWS to get four requests per second. You can keep your dependencies up to date, and hope that the community is doing the work of improving performance. And you can use async as much as possible on the belief that your code is I/O-bound, and be disappointed when it turns out that actually you’re CPU-bound.

而当你发现自己处于那种境地时, 该怎么办呢? 你可以纵向扩展硬件, 最终像那些每月在 AWS 上花费五位数却只能处理每秒四个请求的人一样. 你可以保持依赖项的最新状态, 并希望社区正在努力提升性能. 你也可以尽可能多地使用异步, 坚信你的代码是 I/O 密集型的, 结果却发现实际上是 CPU 密集型的, 失望.

By having a high performance ceiling, Rust lets you write programs that are default fast without thinking too much about optimization, and when you need to improve performance, you have a lot of room to optimize before you hit the performance ceiling.

通过提供一个高的性能下限, Rust 让你无需过多考虑优化就能编写出高性能的程序; 而当需要提升性能时, 在触及性能上限前你仍有大量优化空间.

Tooling | 工具

Cargo has the best DX of any build system+package manager I have used. Typically you praise the features of a program, with cargo you praise the absences: there’s no gotchas, no footguns, no lore you have to learn in anger, no weirdness, no environment variables to configure, no virtualenvs to forget to activate. When you copy a command from the documentation and run it, it works, it doesn’t spit out a useless error message that serves only as a unique identifier to find the relevant StackOverflow / Discourse thread.

对比我用过的其他所有构建系统外加包管理器, Cargo 给了我最好的开发者体验. 通常你会去称赞一个程序的功能, 而对于 Cargo, 你称赞的是它的默默无闻: 没有陷阱, 没有隐患, 没有需要痛苦学习的知识, 没有古怪之处, 没有需要配置的环境变量, 没有忘记激活的虚拟环境. 当你从官方文档中复制命令并运行时, 它(大概率)能正常工作, 而不是直接吐出一个作用仅限于作为搜索相关的 StackOverflow / Discourse 帖子的关键词的错误消息.

Much of the DX virtues are downstream of the fact that cargo is entirely declarative rather than stateful. An example: something that always trips me up with npm is when I update the dependencies in the package.json, running the type-checker/build tool/whatever doesn’t pick up the change. I get an unexpected error and then I go, oh, right, I have to run npm install first. With cargo, if you update the dependencies in the Cargo.toml file, any subsequent command (cargo check or build or run) will first resolve the dependencies, update Cargo.lock, download any missing dependencies, and then run the command. The state of (Cargo.toml, Cargo.lock, local dependency store) is always synchronized.

Cargo 的诸多开发者体验优势源于其完全声明式而非状态化的特性. 举个例子: 使用 npm 时, 一个总让我困扰的问题是, 当我更新了 package.json 中的依赖项后, 运行类型检查器/构建工具等却无法感知变更. 我会遇到意外错误, 然后才反应过来: 哦对, 得先运行 npm install. 而 Cargo 则不同, 如果你更新了 Cargo.toml 文件中的依赖, 后续任何命令都会先解析依赖关系、更新 Cargo.lock、下载缺失的依赖项, 再执行原命令. 整个系统 (Cargo.toml、Cargo.lock、本地依赖缓存) 的状态始终保持同步.

Type Safety | 类型安全

Rust has a good type system: sum types with exhaustiveness checking, option types instead of null, no surprising type conversions. Again, as with tooling, what makes a type system good is a small number of features, and a thousand absences, mistakes that were not made.

Rust 拥有良好的类型系统: 带有穷尽性检查的求和类型、使用 Option 类型而非 null、没有意外的类型转换. 与优秀的工具链一样, 一个好的类型系统的关键在于维持有限必要特性, 避免无数错误.

The practical consequence is you have a high degree of confidence in the robustness of your code. In e.g. Python the state of nature is you have zero confidence that the code won’t blow up in your face, so you spend your time writing tests (to compensate for the lack of a type system) and waiting for the tests to clear CI (because Python is slow as shit). In Rust you write the code and if it compiles, it almost always works. Writing tests can feel like a chore because of how rarely they surface defects.

由此, 你能完全信任你写出来的代码的健壮性. 而在 Python 中, 一般情况下下你完全无法确信代码不会在你面前崩溃, 因此你还得花时间编写测试 (以弥补类型系统的缺失), 并等待测试通过 CI (因为 Python 慢得要命). 而在 Rust 中, 代码能编译通过就能跑. 编写测试可能感觉像件苦差事, 因为它们极少暴露出缺陷.

(译者注: 夸张了点, 逻辑错误还是得好好写测试用例去验证的, 不能想当然.)

To give an example: I don’t really know how to debug Rust programs because I never had to. The only parts of the code I had to debug were the SQL queries, because SQL has many deficiencies. But the Rust code itself was overwhelmingly solid. When there were bugs, they were usually conceptual bugs, i.e., misunderstanding the specification. The type of bugs that you can make in any language and that testing would miss.

举个例子: 我其实不太知道如何调试 Rust 程序, 因为我从不需要这么做. 我唯一需要调试的代码部分是 SQL 查询, 因为 SQL 有很多缺陷. 而 Rust 代码本身极其稳健. 当出现 bug 时, 通常是概念性错误, 也就是对规范的理解有误. 这类错误在任何语言中都可能发生, 而且测试也无法发现.

Error Handling | 错误处理

There’s two ways to do errors: traditional exception handling (as in Java or Python) keeps the happy path free of error-handling code, but makes it hard to know the set of errors that can be raised at a given program point. Errors-as-values, as in Go, makes error handling more explicit at the cost of being very verbose.

有两种处理错误的方式: 传统的异常处理(如Java或Python)让主逻辑路径保持干净, 不掺杂错误处理代码, 但难以明确知道在特定程序点可能抛出哪些错误. 而像 Go 语言那样将错误作为返回值, 虽然让错误处理更加显式, 但代价是代码变得非常冗长.

Rust has a really nice solution where errors are represented as ordinary values, but there’s syntactic sugar that means you don’t have to slow down to write if err != nil a thousand times over.

Rust 有一个非常优雅的解决方案, 它将错误表示为普通值, 但通过语法糖的修饰, 你无需反复书写 if err != nil 上千次而降低效率.

In Rust, an error is any type that implements the Error trait. Then you have the Result type:

在 Rust 中, 任何实现了 Error trait 的类型都是一个错误类型. 然后还有 Result 类型.

#![allow(unused)]
fn main() {
enum Result<T, E: Error> {
    Ok(T),
    Err(E)
}
}

Functions which are fallible simply return a Result, e.g.:

可能出错的函数只需返回一个 Result, 例如:

#![allow(unused)]
fn main() {
enum DbError {
    InvalidPath,
    Timeout,
    // ...
}

fn open_database(path: String) -> Result<Database, DbError>
}

The question mark operator, ?, makes it possible to write terse code that deals with errors. Code like this:

问号操作符 ? 使得编写简洁的错误处理代码成为可能. 类似这样的代码:

#![allow(unused)]
fn main() {
fn foo() -> Result<(), DbError> {
    let db = open_database(path)?;
    let tx = begin(db)?;
    let data = query(tx, "...")?;
    rollback(tx)?;
    Ok(())
}
}

Is transformed to the much more verbose:

解语法糖后类似这样:

#![allow(unused)]
fn main() {
fn foo() -> Result<(), DbError> {
    let db = match open_database(path) {
        Ok(db) => db,
        Err(e) => {
            // Rethrow.
            return Err(e);
        }
    };
    let tx = match begin(db) {
        Ok(tx) => tx,
        Err(e) => {
            return Err(e);
        }
    };
    let data = match query(tx, "...") {
        Ok(data) => data,
        Err(e) => {
            return Err(e);
        }
    };
    match rollback(tx) {
        Ok(_) => (),
        Err(e) => {
            return Err(e);
        }
    };
    Ok(())
}
}

When you need to explicitly handle an error, you omit the question mark operator and use the Result value directly.

当你需要显式处理错误时, 可以(像这样)省略问号运算符并直接处理 Result 值.

The Borrow Checker | 借用检查器

The borrow checker is Rust’s headline feature: it’s how you can have memory safety without garbage collection, it’s the thing that enables “fearless concurrency”. It’s also, for most people, the most frustrating part of learning and using Rust.

借用检查器是 Rust 的标志性特性: 它让你无需垃圾回收即可实现内存安全, 也是实现 “无畏并发” 的关键所在. 然而对大多数人而言, 这恰恰也是学习和使用 Rust 过程中最令人沮丧的部分.

Personally I didn’t have borrow checker problems, but that’s because before I started using Rust at work I’d designed and built my own borrow checker. I don’t know if that’s a scalable pedagogy. Many people report they have to go through a lengthy period of fighting the borrow checker, and slowly their brain discovers the implicit ruleset, and eventually they reach a point where they can write code without triggering inscrutable borrow checker errors. But that means a lot of people drop out of learning Rust because they don’t like fighting the borrow checker.

就我个人而言, 我没有遇到过借用检查器的问题, 但那是因为在我开始在工作中使用 Rust 之前, 我已经设计并构建了自己的借用检查器. 我不确定这是否是一种可扩展的教学方法. 许多人反映他们必须经历一段漫长的与借用检查器斗争的过程, 慢慢地他们的大脑会习惯那些隐含的约束, 最终他们会达到一个直接码代码而不会触发难以理解的借用检查器错误的阶段. 但这也意味着很多人因为不喜欢与借用检查器斗争而放弃了学习 Rust.

So, how do you learn Rust more effectively, without building your own compiler, or banging your head against the borrow checker?

那么, 如何更有效地学习 Rust, 而不必自己构建编译器, 或者与借用检查器较劲呢?

Firstly, it’s useful to understand the concepts behind the borrow checker, the “aliased XOR mutable” rule, the motivation behind linear types, etc. Unfortunately I don’t have a canonical resource that explains it ab initio.

首先, 了解借用检查器背后的概念、“别名 XOR 可变” 规则、线性类型动机等是有用的. 不幸的是, 我没有一个从头开始解释这些内容的权威资源.

Secondly, a change in mindset is useful: a lot of people’s mental model of the borrow checker is as something bolted “on top” of Rust, like a static analyzer you can run on a C/C++ codebase, which just happens to be built into the compiler. This mindset leads to fighting the system, because you think: my code is legitimate, it type-checks, all the types are there, it’s only this final layer, the borrow checker, that objects. It’s better to think of the borrow checker as an intrinsic part of the language semantics. Borrow checking happens, necessarily, after type-checking (because it needs to know the types of terms), but a program that fails the borrow checker is as invalid as a program that doesn’t type-check. Rather than mentally implementing something in C/C++, and then thinking, “how do I translate this to Rust in a way that satisfies the borrow-checker?”, it’s better to think, “how can I accomplish the goal within the semantics of Rust, thinking in terms of linearity and lifetimes?”. But that’s hard, because it requires a high level of fluency.

其次, 转变思维方式很有帮助: 许多人将借用检查器视为 “附加” 在 Rust 之上的东西, 就像可运行于 C/C++ 代码库的静态分析工具, 只不过被内置在编译器中. 这种思维会导致与系统对抗, 因为你会想: 我的代码是合法的, 通过了类型检查, 所有类型都正确, 只是这最后一关——借用检查器在反对. 更好的方式是将借用检查器视为语言语义的内在组成部分. 借用检查必然发生在类型检查之后 (因为它需要知道各术语的类型), 但一个无法通过借用检查的程序, 其无效性等同于类型检查失败的程序. 与其先在脑海中用 C/C++ 实现功能, 再思考 “如何将其翻译成满足借用检查器要求的 Rust 代码”, 不如直接思考 “如何在 Rust 的语义框架下, (基于线性类型及生命周期思维) 直接地实现目标”. 但这很难, 因为这需要高度的语言熟练度.

When you are comfortable with the borrow checker, life is pretty good. “Fighting the borrow checker” isn’t something that happens. When the borrow checker complains it’s either because you’re doing something where multiple orthogonal features impinge on each other (e.g. async + closures + borrowing) or because you’re doing something that’s too complex, and the errors are a signal you have to simplify. Often, the borrow checker steers you towards designs that have mechanical sympathy, that are aligned with how the hardware works. When you converge on a design that leverages lifetimes to have a completely clone()-free flow of data, it is really satisfying. When you design a linearly-typed API where the linearity makes it really hard to misuse, you’re grateful for the borrow checker.

当你适应借用检查器后, 生活会变得轻松愉快. “与借用检查器搏斗” 的情形将不复存在. 当借用检查器报错时, 要么是因为你在处理多个特性相互交织的场景(例如异步+闭包+借用), 要么是因为设计过于复杂, 错误提示正是需要简化的信号. 通常, 借用检查器会引导你走向具有机器亲和力的设计, 与硬件工作原理相契合. 当你设计出利用生命周期实现完全无需 clone() 的数据流时, 会感到无比满足. 当你设计出线性类型 API, 其线性特性让误用变得极其困难时, 你会由衷感激借用检查器的存在.

Async | 异步

Everyone complains about async. They complain that it’s too complex or they invoke that thought-terminating cliche about “coloured functions”. It’s easy to complain about something when comparing it to some vague, abstract, ideal state of affairs; but what, exactly, is the concrete and existing alternative to async?

每个人都抱怨异步编程. 他们抱怨它太复杂, 或者搬出那个终结思考的陈词滥调——“函数染色”. 当将某物与某种模糊、抽象的理想状态相比较时, 抱怨是很容易的; 但是, 具体且现实存在的异步替代方案到底是什么呢?

The binding constraint is that OS threads are slow. Not accidentally but intrinsically, because of the kernel, and having to swap the CPU state and stack on each context switch. OS threads are never going to be fast. If you want to build high-performance network services, it matters a lot how many concurrent connections and how much throughput you can get per CPU. So you need an alternative way to do concurrency that lets you maximize your hardware resources.

核心限制在于操作系统线程速度慢. 这不是偶然的, 而是本质上的问题, 原因在于内核以及每次上下文切换时都需要交换 CPU 状态和堆栈. 操作系统线程永远不会快. 如果你想构建高性能的网络服务, 每个 CPU 能处理多少并发连接和吞吐量就非常重要. 因此, 你需要一种替代的并发方式, 以最大化利用硬件资源.

And there are basically two alternatives.

基本上有两种选择.

  • Green threads, which give programmers the same semantics as OS threads (good!) but often leave a lot of performance on the table (bad!) because you need to allocate memory for each thread’s stack and you need a runtime scheduler to do preemptive multitasking.

    绿色线程 (Green thread) 为程序员提供了与操作系统线程相同的语义 (优势), 但由于需要为每个线程的栈分配内存, 并且需要一个运行时调度器来实现抢占式多任务处理, 它们往往牺牲了大量性能 (劣势).

  • Stackless coroutines, as in Rust, which add complexity to the language semantics and implementation (bad!) but have a high performance ceiling (good!).

    无栈协程, 如 Rust 中的实现, 虽然增加了语言的语义和实现的复杂性 (劣势), 但具有很高的性能上限 (优势).

From the perspective of a language implementor, or someone who cares about specifying the semantics of programming languages, async is not a trivial feature. The intersection of async and lifetimes is hard to understand. From the perspective of a library implementor, someone who writes the building blocks of services and is down in the trenches with Pin / Poll / Future , it’s rough.

从语言实现者或关心编程语言语义规范的人的角度来看, async 并非一个简单的特性. async 与生命周期的交集难以理解. 而从库实现者的角度来看, 那些编写服务构建块并与 Pin / Poll / Future 打交道的人, 这确实很棘手.

But from the perspective of a user, async Rust is pretty good. It mostly “just works”. The user perspective is you put async in front of function definitions that perform IO and you put await at the call sites and that’s it. The only major area where things are unergonomic is calling async functions inside iterators.

但从用户的角度来看, 异步 Rust 相当不错. 它基本上 “开箱即用”. 用户视角是: 你在执行 IO 的函数定义前加上 async, 在调用处加上 await, 就完事了. 唯一不够顺手的地方是在迭代器内部调用异步函数.

Refactoring | 重构

It’s paint by numbers. The type errors make refactoring extremely straightforward and safe.

何尝不是 “枯燥无味” 的工作: (Rust 对重构中极可能引入的)类型错误 (的提示) 使得重构变得极其直接和安全.

Hiring | 招聘

Is it hard to hire Rust programmers? No.

Rust 程序员难招吗? 不难.

First, mainstream languages like Python and TypeScript are so easy to hire for that they wrap back around and become hard. To find a truly talented Python programmer you have to sift through a thousand resumes.

首先, 像 Python 和 TypeScript 这样的主流语言因为招聘太容易, 反而变得困难. 要找到一个真正有才华的Python程序员, 你得筛选上千份简历.

Secondly, there’s a selection effect for quality. “Has used Rust”, “has written open-source code in Rust”, or “wants to use Rust professionally” are huge positive signals about a candidate because it says they are curious and they care about improving their skills.

其次, 质量上存在筛选效应. “使用过 Rust”、“用 Rust 编写过开源代码” 或 “希望专业使用 Rust” 这些信号对候选人而言是巨大的加分项, 因为这表明他们 (对编程) 充满好奇心且注重提升自身技能.

Personally I’ve never identified as a “Python programmer” or a “Rust programmer”. I’m just a programmer! When you learn enough languages you can form an orthogonal basis set of programming concept and translate them across languages. And I think the same is true for the really talented programmers: they are able to learn the language quickly.

我个人从未自称为 “Python程序员” 或 “Rust程序员”. 我只是个程序员! 当你掌握足够多的语言时, 就能构建编程概念的交界理解, 并在不同语言间转换它们. 我认为真正有才华的程序员也是如此: 他们能快速掌握新语言.

Affect | 直接影响

Enough about tech. Let’s talk about feelings.

科技的话题就到此为止吧. 我们来谈谈感受.

When I worked with Python+Django the characteristic feeling was anxiety. Writing Python feels like building a castle out of twigs, and the higher you go, the stronger the wind gets. I expected things to go wrong, I expected the code to be slow, I expected to watch things blow up for the most absurd reasons. I had to write the code defensively, putting type assertions everywhere.

当我使用 Python+Django 工作时, 那种特有的感觉是焦虑. 写 Python 就像用细树枝搭建城堡, 爬得越高, 风就越大. 我预料事情会出错, 预料代码会运行缓慢, 预料会因为最荒谬的原因看着一切崩溃. 我不得不防御性地编写代码, 到处加上类型断言.

Rust feels good. You can build with confidence. You can build things that not only work as desired but which are also beautiful. You can be proud of the work that you do, because it’s not slop.

Rust 让人感觉很好. 你可以充满信心地构建. 你不仅可以构建出按预期工作的东西, 还能创造出优美的作品. 你可以为自己的工作感到自豪, 因为它不是粗制滥造的.

The Bad | 缺点

This section describes the things I don’t like.

这一部分描述的是我不喜欢的事情.

The Module System | module 系统

In Rust, there’s two levels of code organization:

在 Rust 中, 代码组织有两个层次:

  • Modules are namespaces with visibility rules.

    module 是具有可见性规则的命名空间.

  • Crates are a collection of modules, and they can depend on other crates. Crates can be either executables or libraries.

    crate 是模块的集合, 它们可以依赖于其他 crates. crate 可以是可执行文件或库.

A project, or workspace, can be made up of multiple crates. For example a web application could have library crates for each orthogonal feature and an executable crate that ties them together and starts the server.

一个项目或工作空间 (workspace) 可以由多个 crate 组成.例如, 一个网络应用程序可以为每个共用功能提取为 crate, 并准备一个 crate 将它们整合在一起编译为可执行文件.

What surprised me was learning that modules are not compilation units, and I learnt this by accident when I noticed you can have a circular dependency between modules within the same crate1. Instead, crates are the compilation unit. When you change any module in a crate, the entire crate has to be recompiled. This means that compiling large crates is slow, and large projects should be broken down into many small crates, with their dependency DAG arranged to maximize parallel compilation.

让我惊讶的是, 模块并不是编译单元, 这一点是我偶然发现的, 当时我注意到在同一 crate1 中的模块之间可以存在循环依赖. 实际上, crate 才是编译单元. 当你修改 crate 中的任何模块时, 整个 crate 都必须重新编译. 这意味着编译大型 crate 会很慢, 因此大型项目应该被分解成许多小型 crate, 并通过安排它们的依赖关系图来最大化并行编译的效率.

This is a problem because creating a module is cheap, but creating a crate is slow. Creating a new module is just creating a new file and adding an entry for it in the sibling mod.rs file. Creating a new crate requires running cargo new, and don’t forget to set publish = false in the Cargo.toml, and adding the name of that crate in the workspace-wide Cargo.toml so you can import it from other crates. Importing a symbol within a crate is easy: you start typing the name, and the LSP can auto-insert the use declaration, but this doesn’t work across crates, you have to manually open the Cargo.toml file for the crate you’re working on and manually add a dependency to the crate you want to import code from. This is very time-consuming.

问题在于创建 module 成本低廉, 但创建 crate 却耗时费力. 新建模块只需创建文件并在同级 mod.rs 中添加条目 (译者注: 现在推荐创建 xxx.rs, 在 xxx.rs 所在目录下创建同名文件夹 xxx, 然后在 xxx.rs 里写子 module, 如 mod yyy, 然后创建 xxx/yyy.rs 即可, 不需要 mod.rs 了), 而新建 crate 则需运行 cargo new 命令, 别忘了在 Cargo.toml 中设置 publish = false, 还要在全局工作空间的 Cargo.toml中添加该crate名称以便其他crate` 引用. 在同一个 crate 内引入依赖很简单: 输入名称时 LSP 能自动插入 use 声明, 但跨 crate 时这招就失效了. 你必须手动打开当前 crate 的 Cargo.toml 文件, 手工添加对目标 crate 的依赖才能使用. 整个过程极其耗时.

Another problem with crate-splitting is that rustc has a really nice feature that warns you when code is unused. It’s very thorough and I like it because it helps to keep the codebase tidy. But it only works within a crate. In a multi-crate workspace, declarations that are exported publicly in a crate, but not imported by any other sibling crates, are not reported as unused.2

crate 拆分带来的另一个问题是, rustc 有个非常实用的功能: 当代码未被使用时它会发出警告. 这个功能非常全面, 我很喜欢它, 因为它有助于保持代码库的整洁. 但它仅在单个 crate 内部有效. 在多 crate 工作区中, 某个 crate 公开导出但未被任何同级 crate 导入, 不会被标记为未使用代码.2

So if you want builds to be fast, you have to completely re-arrange your architecture and manually massage the dependency DAG and also do all this make-work around creating and updating crate metadata. And for that you gain… intra-crate circular imports, which are a horrible antipattern and make it much harder to understand the codebase. I would much prefer if modules were disjoint compilation units.

所以, 如果你想构建速度快, 就必须彻底重新安排你的架构, 手动调整依赖关系的有向无环图, 还要做所有这些创建和更新 crate 元数据的无用功. 而你所得到的… 是 crate 内部的循环导入, 这是一种非常糟糕的反模式, 会让代码库更难理解. 我宁愿模块是独立的编译单元.

I also think the module system is just a hair too complex, with re-exports and way too many ways to import symbols. It could be stripped down a lot.

我也认为模块系统有点过于复杂了, 包括重新导出和导入符号的方式太多.可以大幅简化.

Build Performance | 构建性能

The worst thing about the Rust experience is the build times. This is usually blamed on LLVM, which, fair enough, but I think part of it is just intrinsic features of the language, like the fact that modules are not independent compilation units, and of course monomorphization.

Rust体验中最糟糕的事情就是编译时间. 这通常归咎于 LLVM, 虽然确实如此, 但我认为部分原因在于语言本身的固有特性, 比如模块不是独立的编译单元, 当然还有单态化.

There are various tricks to speed up the builds: caching, cargo chef, tweaking the configuration. But these are tricks, and tricks are fragile. When you notice a build performance regression, it could be for any number of reasons:

有多种技巧可以加快构建速度: [缓存]((https://github.com/Swatinem/rust-cache)、使用 cargo chef调整配置. 但这些都只是技巧, 而技巧往往是脆弱的. 当你注意到构建性能出现倒退时, 可能的原因有无数种:

  • The code is genuinely larger, and takes longer to build.

    代码确实更大了, 构建时间也更长.

  • You’re using language features that slow down the frontend (e.g. complex type-level code).

您正在使用会拖慢前端速度的语言特性 (例如复杂的类型级代码).

  • You’re using language features that slow down the backend (e.g. excessive monomorphization).

    您使用的语言特性会拖慢后端性能 (例如过度单态化).

  • A proc macro is taking a very long time (tracing::instrument in particular is fantastically slow).

    一个过程宏花费了很长时间 (尤其是 tracing::instrument 慢得出奇).

  • The crate DAG has changed shape, and crates that used to be built in parallel are now being built serially.

    crate 依赖有向无环图的形状发生了变化, 以前可以并行构建的箱子现在需要串行构建. .

  • Any of the above, but in the transitive closure of your dependencies.

    上述任意一项, 但位于你的依赖的依赖中.

  • You’ve added/updated an immediate dependency, which pulls in lots of transitive dependencies.

    您添加/更新了一个直接依赖项, 这会引入大量传递依赖项.

  • You’re caching too little, causing dependencies to be downloaded.

    你缓存得太少, 导致依赖项需要重新下载.

  • You’re caching too much, bloating the cache, which takes longer to download.

    你缓存太多了, 导致缓存膨胀, 下载时间变长.

  • The cache was recently invalidated (e.g. by updating Cargo.lock) and has not settled yet.

    缓存最近失效了 (例如通过更新 Cargo.lock) 且尚未稳定下来.

  • The CI runners are slow today, for reasons unknowable.

    今天 CI runner 很慢, 原因不明.

  • The powerset of all of the above.

    上述所有内容的幂集.

  • (Insert Russell’s paradox joke)

    (插入罗素悖论笑话)

It’s not worth figuring out. Just pay for the bigger CI runners. Four or eight cores should be enough. Too much parallelism is waste: run cargo build with the --timings flag, open the report in your browser, and look at the value of “Max concurrency”. This tells you how many crates can be built in parallel, and, therefore, how many cores you can buy before you hit diminishing returns.

不值得费心去琢磨. 直接花钱买更大的 CI runner 吧. 四核或八核应该足够了. 并行度过高是浪费: 用 --timings 标志运行 cargo build, 在浏览器中打开报告, 查看 “最大并发数 (max concurrency)” 的值. 这会告诉你可以并行构建多少个 crate, 从而知道在收益递减前可以购买多少核.

The main thing you can do to improve build performance is to split your workspace into multiple crates, and arranging the crate dependencies such that as much of your workspace can be built in parallel. This is easy to do at the start of a project, and very time-consuming after.

提高构建性能的主要方法是将工作区拆分为多个 crate, 并合理安排 crate 之间的依赖关系, 以便尽可能多地并行构建工作区. 这在项目开始时很容易做到, 但之后再做会非常耗时.

Mocking | 嘲笑

Maybe this is a skill issue, but I have not found a good way to write code where components have swappable dependencies and can be tested independently of their dependencies. The central issue is that lifetimes impinge on late binding.

也许这是一个技术问题, 我想要编写一类代码, 其组件具有可交换的依赖项, 并且可以独立于其依赖项进行测试. 核心问题是生命周期影响了后期绑定.

Consider a workflow for creating a new user in a web application. The three external effects are: creating a record for the user in the database, sending them a verification email, and logging the event in an audit log:

考虑一个在网页应用中创建新用户的工作流程. 三个外部效应分别是: 在数据库中为用户创建记录、向他们发送验证邮件以及在审计日志中记录该事件.

#![allow(unused)]
fn main() {
fn create_user(
    tx: &Transaction,
    email: Email,
    password: Password
) -> Result<(), CustomError>  {
    insert_user_record(tx, &email, &password)?;
    send_verification_email(&email)?;
    log_user_created_event(tx, &email)?;
    Ok(())
}
}

Testing this function requires spinning up a database and an email server. No good! We want to detach the workflow from its dependencies, so we can test it without transitively testing its dependencies. There’s three ways to do this:

测试此功能需要启动数据库和电子邮件服务器. 这不好! 我们希望将工作流与其依赖项分离, 这样我们就可以在不间接测试其依赖项的情况下进行测试. 有三种方法可以实现这一点.

  • Use traits to define the interface, and pass things at compile-time.

    使用 trait 来定义接口, 并在编译时传递参数.

  • Use traits to define the interface, and use dynamic dispatch to pass things at run-time.

    使用 trait 来定义接口, 并通过动态分派在运行时传递对象.

  • Use function types to define the interface, and pass dependencies as closures.

    使用函数类型来定义接口, 并将依赖项作为闭包传递.

And all of these approaches work. But they require a lot of make-work. In TypeScript or Java or Python it would be painless, because those languages don’t have lifetimes, and so dynamic dispatch or closures “just work”.

所有这些方法都有效. 但它们需要大量额外工作. 在 TypeScript、Java 或 Python 中这会很轻松, 因为这些语言没有生命周期概念, 动态分发或闭包 “直接就能用”.

For example, say we’re using traits and doing everything at compile-time. To minimize the work let’s just focus on the dependency that writes the user’s email and password to the database. We can define a trait for it:

例如, 假设我们正在使用 trait 并在编译时完成所有工作. 为了最小化工作量, 我们只关注将用户的电子邮件和密码写入数据库的依赖项. 我们可以为它定义一个特质.

#![allow(unused)]
fn main() {
trait InsertUser<T> {
    fn execute(
        &mut self,
        tx: &T,
        email: &Email,
        password: &Password
    ) -> Result<(), CustomError>;
}
}

(We’ve parameterized the type of database transactions because the mock won’t use a real database, therefore, we won’t have a way to construct a Transaction type in the tests.)

(我们参数化了数据库事务的类型, 因为模拟测试不会使用真实的数据库, 因此在测试中我们将无法构造一个 Transaction 类型的实例. )

The real implementation requires defining a placeholder type, and implementing the InsertUser trait for it:

真正的实现需要定义一个占位类型 (Marker 类型, 作 ZST), 并为它实现 InsertUser trait.

#![allow(unused)]
fn main() {
struct InsertUserAdapter {}

impl InsertUser<Transaction> for InsertUserAdapter {
    fn execute(
        &mut self,
        tx: &Transaction,
        email: &Email,
        password: &Password
    ) -> Result<(), CustomError> {
        insert_user_record(tx, email, password)?;
        Ok(())
    }
}
}

The mock implementation uses the unit type () as the type of transactions:

模拟实现使用 unit 类型 () 作为 transaction 的类型.

#![allow(unused)]
fn main() {
struct InsertUserMock {
    email: Email,
    password: Password,
}

impl InsertUser<()> for InsertUserMock {
    fn execute(
        &mut self,
        tx: &(),
        email: &Email,
        password: &Password
    ) -> Result<(), CustomError> {
        // Store the email and password in the mock object, so
        // we can afterwards assert the right values were passed
        // in.
        self.email = email.clone();
        self.password = password.clone();
        Ok(())
    }
}
}

Finally we can define the create_user workflow like this:

最后我们可以这样定义 create_user 工作流程.

#![allow(unused)]
fn main() {
fn create_user<T, I: InsertUser<T>>(
    tx: &T,
    insert_user: &mut I,
    email: Email,
    password: Password,
) -> Result<(), CustomError> {
    insert_user.execute(tx, &email, &password)?;
    // Todo: the rest of the dependencies.
    Ok(())
}
}

The live, production implementation would look like this:

生产实现将如下所示.

#![allow(unused)]
fn main() {
fn create_user_for_real(
    tx: &Transaction,
    email: Email,
    password: Password,
) -> Result<(), CustomError> {
    let mut insert_user = InsertUserAdapter {};
    create_user(tx, &mut insert_user, email, password)?;
    Ok(())
}
}

While in the unit tests we would instead create a InsertUserMock and pass it in:

在单元测试中, 我们会创建一个 InsertUserMock 并传入.

#![allow(unused)]
fn main() {
#[test]
fn test_create_user() -> Result<(), CustomError> {
    let mut insert_user = InsertUserMock {
        email: "".to_string(),
        password: "".to_string()
    };
    let email = "foo@example.com".to_string();;
    let password = "hunter2".to_string();

    create_user(&(), &mut insert_user, email, password)?;

    // Assert `insert_user` was called with the right values.
    assert_eq!(insert_user.email, "foo@example.com");
    assert_eq!(insert_user.password, "hunter2");

    Ok(())
}
}

Obviously this is a lot of typing. Using traits and dynamic dispatch would probably make the code marginally shorter. Using closures is probably the simplest approach (a function type with type parameters is, in a sense, a trait with a single method), but then you run into the ergonomics issues of closures and lifetimes.

显然, 这需要大量输入. 使用 trait 和动态分发可能会使代码稍微简短一些. 使用闭包可能是最简单的方法 (从某种意义上说, 带有类型参数的函数类型就是一种具有单一方法的 Fn 系列 trait), 但随后你会遇到闭包和生命周期的使用体验问题.

Again, this might be a skill issue, and maybe there’s an elegant and idiomatic way to do this.

这可能是技术问题, 或许有一种优雅且地道的方法可以做到这一点.

Alternatively, you might deny the entire necessity of mocking, and write code without swappable implementations, but that has its own problems: tests become slower, because you have to spin up servers to mock things like API calls; tests require a lot of code to set up and tear down these dependencies; tests are necessarily end-to-end, and the more end-to-end your tests, the more test cases you need to check every path because of the combinatorial explosion of inputs.

或者, 你可能会完全否定模拟的必要性, 编写没有可交换实现的代码, 但这也有其自身的问题: 测试会变得更慢, 因为你必须启动服务器来模拟诸如 API 调用之类的东西; 测试需要大量代码来设置和拆除这些依赖项; 测试必然是端到端的, 而你的测试越端到端, 由于输入的组合爆炸, 你需要更多的测试用例来检查每条路径.

Expressive Power | 表现力

It’s easy to go insane with proc macros and trait magic and build an incomprehensible codebase where it’s impossible to follow the flow of control or debug anything. You have to rein it in.

使用过程宏和 trait 魔法很容易让人发疯, 构建出一个难以理解的代码库, 无法跟踪控制流或调试任何东西. 你必须加以控制.


  1. If modules were separate compilation units this wouldn’t work. If module A depends on B, to compile A you need to first compile B to know what declarations it exports and what their types are. But if B also depends on A, you have an infinite regression. 如果模块是独立的编译单元, 这就行不通了. 如果模块 A 依赖于 B, 要编译 A, 你需要先编译 B, 以了解它导出了哪些声明以及它们的类型是什么. 但如果 B 也依赖于 A, 你就会陷入无限递归. ↩2

  2. One way to fix this is to make extremely fine-grained crates, and rely on cargo-machete to identify unused code at the dependency level. But this would take up way too much time. 一种解决方法是创建极其细粒度的crate, 并依赖 cargo-machete 在依赖项级别识别未使用的代码. 但这会占用太多时间. ↩2

This Week in Rust (TWiR) Rust 语言周刊中文翻译计划, 第 595 期

本文翻译自 Melody Horn 的博客文章 https://www.boringcactus.com/2025/04/13/2025-survey-of-rust-gui-libraries.html, 英文原文版权由原作者所有, 中文翻译版权遵照 CC BY-NC-SA 协议开放. 如原作者有异议请邮箱联系.

相关术语翻译依照 Rust 语言术语中英文对照表.

囿于译者自身水平, 译文虽已力求准确, 但仍可能词不达意, 欢迎批评指正.

2025 年 4 月 19 日晚, 于北京.

(翻译中)

This Week in Rust (TWiR) Rust 语言周刊中文翻译计划, 第 595 期

本文翻译自 Natalie Klestrup Röijezon 的博客文章 https://natkr.com/2025-04-10-async-from-scratch-1/, 英文原文版权由原作者所有, 中文翻译版权遵照 CC BY-NC-SA 协议开放. 如原作者有异议请邮箱联系.

相关术语翻译依照 Rust 语言术语中英文对照表.

囿于译者自身水平, 译文虽已力求准确, 但仍可能词不达意, 欢迎批评指正.

2025 年 6 月 1 日下午, 于北京.

祝端午安康, 也祝孩子们六一国际儿童节快乐!

GitHub last commit

Async from scratch 1: What’s in a Future, anyway?

There are a lot of guides about how to use async Rust from a “user’s perspective”, but I think it’s also worth understanding how it works, what those async blocks actually mean.

关于如何从 “用户角度” 使用异步 Rust 的指南有很多, 但我认为同样值得去了解它的工作原理, 以及那些 async 块究竟意味着什么.

Why you get all those weird pinning errors.

(再者, 有助于解答诸如) 为什么你会遇到那些奇怪的 “固定” (pinning) 错误 (的问题).

(译者注: Pin 作为异步 Rust 的一个术语, 直译为 “大头针”, 引申为 “固定”, 后续保留不译.)

This is the first post in a series where we’re going to slowly build our way up to reinventing the modern async Rust environment, in an attempt to explain the whys and the hows. It’s not going to end up being a competitor to Tokio or anything, but hopefully it should make understanding it a bit less daunting afterwards.

这是系列文章的第一篇, 我们将逐步构建现代异步 Rust 环境, 试图解释其背后的原因和实现方式. 它最终不会成为 Tokio 之类的异步运行时库, 只是希望能在之后让你更好地理解 (这些库提供的大同小异的 API).

I’m writing the series targeted at people who’ve written a trait and an async fn (or two), but don’t worry if “polling”, “pinning”, or “wakers” mean nothing to you. That’s what we’re going to try to untangle, one step at a time!

本系列面向那些写过 trait 和/ 或 async fn 的人, 但如果你对 “轮询 (polling)” 、 “固定 (pinning)” 或 “唤醒器 (wakers)” 一无所知, 也不用担心. 我们将一步步尝试解开这些谜团!

Now… If you’ve written any async Rust code, it probably looked something like this:

现在… 如果你写过任何异步 Rust 代码, 它可能看起来像这样:

#![allow(unused)]
fn main() {
async fn trick_or_treat() {
    for house in &STREET {
        match demand_treat(house).await {
            Ok(candy) => eat(candy).await,
            Err(_) => play_trick(house).await,
        }
    }
}
}

But, uh, what does that do? Why do I need to await things, how is an async fn different from any other fn, and what does any of that actually… do, anyway?

但是, 呃, 这到底做了什么? 为什么我需要 await 它, async fn 和其他 fn 有什么不同, 这些到底… 是干什么的?

In the Future… | Future 是什么

Well, to understand that, we’re going to need to rewind the tape a bit. We’re going to have to meet a trait that you probably haven’t really seen before. We’re going to have to deal with… Future. Just like Add defines whether a + b is valid, Future defines “something that can be .await-ed”.1 It looks like this:

要理解这些, 我们需要稍微倒带一下. 我们将遇到一个你可能从未真正见过的 trait: Future. 就像 Add 定义了 a + b 是否有效一样, Future 定义了 “可以被 .await 的东西1” . 它的定义如下:

#![allow(unused)]
fn main() {
use std::{task::{Context, Poll}, pin::Pin};

trait Future {
    type Output;
    fn poll(
        self: Pin<&mut Self>,
        context: &mut Context<'_>,
    ) -> Poll<Self::Output>;
}
}

..Y’know, for a trait with only one function, that’s a pretty spicy one signature. It could even be called a bit overwhelming. Especially if you’re new to Rust in general.

… 要知道, 对于一个只有一个函数的 trait 来说, 这个方法签名相当 “辣” (一眼让人迷糊). 甚至可以说有点让人不知所措, 尤其是如果你刚接触 Rust 的话.

But most of that doesn’t really matter, so we can make a few simplifications for now. Don’t worry, we’ll get back to all of them later. But for now, we can strip most of that away, and just pretend that it looks like this instead:

但大部分内容其实并不重要, 所以我们现在可以做一些简化. 别担心, 稍后我们会回到所有这些内容. 但现在, 我们可以去掉大部分内容, 假装它看起来像这样:

#![allow(unused)]
fn main() {
use std::task::Poll;

trait SimpleFuture<Output> {
    fn poll(&mut self) -> Poll<Output>;
}
}

So what does this (Simple)Future::poll thing do?

那么这个 SimpleFuture::poll 是做什么的呢?

Let’s take a stroll down to the poll box | 让我们看看 poll

At its core, a Future is a function call that can pause itself when it needs to wait for something.2

本质上, Future 是一种能在需要等待时自行暂停的函数调用.2

poll asks the Future to try to continue, returning Poll::Ready if it was able to finish, or Poll::Pending if it had to pause itself again.3

poll 方法会要求 Future 尝试继续执行, 若完成则返回 Poll::Ready, 若未执行完毕则返回 Poll::Pending.3

This can start out pretty simple. We could have a Future that is always ready to produce some extremely random numbers:

初始实现可以非常简单. 比如我们可以创建一个总能生成特定随机数的 Future:

#![allow(unused)]
fn main() {
struct FairDice;

impl SimpleFuture<u8> for FairDice {
    fn poll(&mut self) -> Poll<u8> {
        Poll::Ready(4) // chosen by fair dice roll
    }
}
}

We could also just wait forever, grabbing some breathing room:

我们也可以选择永远等待, 给自己留些喘息空间:

#![allow(unused)]
fn main() {
struct LookBusy;

impl SimpleFuture<()> for LookBusy {
    fn poll(&mut self) -> Poll<()> {
        Poll::Pending
    }
}
}

These have all been pretty trivial problems, but to be able to pause things midway we’ll need to save all the context that should be kept.

这些问题虽然都很简单, 但要想中途暂停操作, 我们需要保存所有应保留的上下文.

This is where our Future becomes relevant as a type, and not just a marker for which poll function to call. We could have a Future that needs to be polled 10 times before it completes:

这时我们的 Future 就不仅仅是标记该调用哪个 poll 函数的标识了, 而是作为一个类型真正发挥作用. 比如可能存在需要轮询 10 次才能完成的 Future:

#![allow(unused)]
fn main() {
struct Stubborn {
    counter: u8,
}

impl SimpleFuture<()> for Stubborn {
    fn poll(&mut self) -> Poll<()> {
        self.counter += 1;
        if self.counter == 10 {
            Poll::Ready(())
        } else {
            Poll::Pending
        }
    }
}
}

Or a wrapper that delegates to another Future:

或者一个对其他 Future 的包装:

#![allow(unused)]
fn main() {
struct LoadedDice {
    inner: FairDice,
}

impl SimpleFuture<u8> for LoadedDice {
    fn poll(&mut self) -> Poll<u8> {
        match self.inner.poll() {
            Poll::Ready(x) => Poll::Ready(x + 1),
            Poll::Pending => Poll::Pending,
        }
    }
}
}

Now.. writing all those “match poll, if pending then return, if ready then continue” blocks can also get pretty tedious. Thankfully, Rust provides the ready! macro that does it for us.4

现在… 编写那些 “匹配 poll 结果, 若 pending 则返回, 若 ready 则继续” 的代码块也相当繁琐. 幸运的是, Rust 提供了 ready! 宏来帮我们处理这些.4

The example above could also be written like this:

上面的例子也可以这样写:

#![allow(unused)]
fn main() {
use std::task::ready;

struct LoadedDice {
    inner: FairDice,
}

impl SimpleFuture<u8> for LoadedDice {
    fn poll(&mut self) -> Poll<u8> {
        let x = ready!(self.inner.poll());
        Poll::Ready(x + 1)
    }
}
}

But eventually we’ll want to be able to await multiple times, and to save stuff between them. For example, we might want to sum up pairs of our dice:

但最终我们会需要多次 await, 并在其间保存状态. 例如, 我们可能想累加骰子的点数对:

#![allow(unused)]
fn main() {
async fn fair_dice() -> u8 {
    4 // still guaranteed to be completely fair
}

async fn fair_dice_pair() -> u8 {
    let first_dice = fair_dice().await;
    let second_dice = fair_dice().await;
    first_dice + second_dice
}
}

We can do this by saving the shared state in an enum instead, with a variant for each await point. This kind of rearrangement is called a “state machine”, and this is also effectively what async fn does for us behind the scenes. That ends up looking like this:

可以通过将共享状态保存在枚举中实现, 每个 await 点对应一个枚举变体. 这种重构方式被称为“状态机“, 实际上 async fn 在底层也是这么做的. 最终代码会变成这样:

#![allow(unused)]
fn main() {
enum FairDicePair {
    Init,
    RollingFirstDice {
        first_dice: FairDice,
    },
    RollingSecondDice {
        first_dice: u8,
        second_dice: FairDice,
    }
}

impl SimpleFuture<u8> for FairDicePair {
    fn poll(&mut self) -> Poll<u8> {
        // The loop lets us continue running the state machine
        // until one of the ready! clauses pauses us.
        loop {
            match self {
                Self::Init => {
                    *self = Self::RollingFirstDice {
                        first_dice: FairDice,
                    };
                },
                Self::RollingFirstDice { first_dice } => {
                    // Every time we're poll()ed, we'll do _everything_ up to the
                    // next ready! again (poll() is just another method, after all),
                    // so it should be the first (non-trivial) thing we do every time
                    // it's called.
                    let first_dice = ready!(first_dice.poll());
                    *self = Self::RollingSecondDice {
                        first_dice,
                        second_dice: FairDice,
                    }
                }
                Self::RollingSecondDice { first_dice, second_dice } => {
                    let second_dice = ready!(second_dice.poll());
                    return Poll::Ready(*first_dice + second_dice)
                }
            }
        }
    }
}
}

This is.. just a bit.. more verbose.

这… 只是稍微… 啰嗦了一点.

But on the flip side, a raw poll lets us do things that async fn can’t really express. For example, we can build a timeout that only lets us poll some arbitrary wrapped Future so many times:5

但另一方面, 原始 poll 操作能实现 async fn 难以表达的功能. 比如我们可以构建一个超时机制, 限制对任意包裹在其中的 Future 的轮询次数:5

#![allow(unused)]
fn main() {
struct Timeout {
    inner: Stubborn,
    polls_left: u8,
}

#[derive(Debug)]
struct TimedOut;

impl SimpleFuture<Result<(), TimedOut>> for Timeout {
    fn poll(&mut self) -> Poll<Result<(), TimedOut>> {
        match self.polls_left.checked_sub(1) {
            Some(x) => self.polls_left = x,
            None => return Poll::Ready(Err(TimedOut)),
        }
        let inner = ready!(self.inner.poll());
        Poll::Ready(Ok(inner))
    }
}
}

Let’s dance run | 让我们开始运行

So.. we’ve defined our (Simple)Future. A few, in fact. But they’re not really worth much unless we can actually run them. How do we do that?

所以…我们已经定义了我们的 (Simple)Future. 实际上定义了好几个. 但除非能真正运行它们, 否则这些定义意义不大. 我们该怎么做呢?

Simple. We just keep calling poll until it returns Ready6.

很简单. 只需不断调用 poll 直到返回 Ready6.

#![allow(unused)]
fn main() {
fn run_future<Output, F: SimpleFuture<Output>>(mut fut: F) -> Output {
    loop {
        if let Poll::Ready(out) = fut.poll() {
            return out;
        }
    }
}
}

For example:

如:

#![allow(unused)]
fn main() {
println!("=> {}", run_future(FairDice));
}
=> 4

Now, this does have a catch. Just a tiny one. A teeny-tiny one. A teeny tiny toy catch.

不过这里有个小问题. 非常小的问题. 微小到像玩具般的问题.

While waiting for our Future to complete we’re wasting a lot of CPU cycles, just calling poll over and over.7 That’s not ideal, but for now, let’s just put a pin in that. We’ll come back to it soon enough.

在等待 Future 完成时, 我们只是不断调用 poll, 浪费了大量 CPU 周期.7 这并不理想, 但暂时先记下这点, 稍后再来处理.

Enter the combinatrix | 组合器的登场

As we can see, trying to write all of our logic as a poll quickly grows out of control, but sometimes we do need to express things that regular sequences of function calls.. can’t.8

可以看到, 将所有逻辑写成 poll 形式会迅速失控, 但有时确实需要表达普通函数调用序列无法实现的功能.8

Is there a way to let us combine them, so we can use whatever fits the job best?

有没有办法让我们组合它们, 以便选择最适合任务的方案?

Well, yes. We can write combinators, generalizing our special logic into new building blocks that our async fn can then reuse.

当然有. 我们可以编写组合器, 将特殊逻辑泛化为新的构建块, 供 async fn复用.

For example, our Timeout example can be changed to accept any arbitrary Future, instead of only Stubborn:

例如, Timeout 示例可以修改为接受任意 Future, 而不仅是 Stubborn:

#![allow(unused)]
fn main() {
struct Timeout<F> {
    inner: F,
    polls_left: u8,
}

struct TimedOut;

impl<F, Output> SimpleFuture<Result<Output, TimedOut>> for Timeout<F>
where
    F: SimpleFuture<Output>,
{
    fn poll(&mut self) -> Poll<Result<Output, TimedOut>> {
        match self.polls_left.checked_sub(1) {
            Some(x) => self.polls_left = x,
            None => return Poll::Ready(Err(TimedOut)),
        }
        let inner = ready!(self.inner.poll());
        Poll::Ready(Ok(inner))
    }
}

fn with_timeout<F, Output>(
    inner: F,
    max_polls: u8,
) -> impl SimpleFuture<Result<Output, TimedOut>>
where
    F: SimpleFuture<Output>,
{
    Timeout {
        inner,
        polls_left: max_polls,
    }
}
}

Which we could then use in our async fn, by wrapping the sub-Future before await-ing it:9

然后可以在 async fn 中使用, 通过在 await 前包装子 Future:9

#![allow(unused)]
fn main() {
async fn send_email(target: &str, msg: &str) {}

struct TimedOut;

async fn with_timeout<F: Future>(inner: F, max_polls: u8) -> Result<F::Output, TimedOut> { Ok(inner.await) }

async fn send_email_with_retry() {
    for _ in 0..5 {
        if with_timeout(send_email("nat@nullable.se", "message"), 10).await.is_ok() {
            return;
        }
    }
    panic!("repeatedly timed out trying to send email, giving up...");
}
}

Input, output | 输入与输出

We’ve spent some time working out how to combine our Futures… but they don’t really.. do anything yet. If a Future runs in the forest computer, but nobody was around to run it.. we haven’t really done much more than burn some electricity.

我们花了些时间研究如何组合 Future… 但它们实际上还没做任何事. 如果 Future 在森林计算机中运行, 却无人执行它… 我们不过是浪费了些电力.

To be useful we’ll need to be able to interact with external systems. Network calls, and so on.

要让其有用, 需要能与外部系统交互. 比如网络调用等.

Let’s try reading something from a TCP socket, for example. We’ll provide a server that provides our luggage code whenever we connect. For safekeeping, of course.

以读取 TCP 套接字为例. 我们将搭建一个服务器, 连接时提供行李代码 (当然是为了安全保管).

#![allow(unused)]
fn main() {
let listener = std::net::TcpListener::bind("127.0.0.1:9191").unwrap();

std::thread::spawn(move || {
    use std::{io::Write, time::Duration};
    let (mut conn, _) = listener.accept().unwrap();
    // Ensure that the client needs to wait for
    std::thread::sleep(Duration::from_millis(200));
    conn.write_all(&[1, 2, 3, 4, 5]).unwrap();
});
}

To do this, we’ll need to do a few things:

为此需要:

  1. Create the socket (this happens implicitly in Rust’s API)

    创建套接字 (Rust API 隐式完成)

  2. Connect to the remote destination

    连接远程目标

  3. Configure the socket to be non-blocking (since otherwise the receive itself would just wait for the message, preventing any other Futures from running on the same thread)10

    配置非阻塞套接字 (否则接收操作会阻塞, 阻止同线程运行其他 Future)

  4. Try to read the message

    尝试读取消息

  5. If the read returned WouldBlock, return Pending and retry from step 4 on the next poll

    如果读取返回 WouldBlock, 返回 Pending 并在下次 poll 时重试步骤 4

Putting it together looks something like this:

组合起来如下:

#![allow(unused)]
fn main() {
use std::{io::Read, net::TcpStream};

struct TcpRead<'a> {
    socket: &'a mut TcpStream,
    buffer: &'a mut [u8],
}

impl<'a> SimpleFuture<usize> for TcpRead<'a> {
    fn poll(&mut self) -> Poll<usize> {
        match self.socket.read(self.buffer) {
            Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => Poll::Pending,
            size => Poll::Ready(size.unwrap()),
        }
    }
}
}
#![allow(unused)]
fn main() {
let luggage_code_server_address = "127.0.0.1:9191";

let mut socket = TcpStream::connect(luggage_code_server_address).unwrap();

socket.set_nonblocking(true).unwrap();

let mut buffer = [0; 16];
let received = run_future(TcpRead {
    socket: &mut socket,
    buffer: &mut buffer,
});

println!("=> The luggage code is {:?}", &buffer[..received]);
}
=> The luggage code is [1, 2, 3, 4, 5]

Until next time… | 下回分解…

Hopefully, you now have a bit of a handle on the general idea of how Futures interact. We’ve seen how to define, run, combine them, and used them to communicate with a network service.

希望你现在对 Future 的原理有了基本认识. 我们已了解如何定义、运行和组合它们, 并用其与网络服务通信.

But as I mentioned, we’ve only really talked about our simplified SimpleFuture variant. Through the rest of the series, I’ll focus on pulling back those curtains, one by one, until we arrive back at the real Future trait.

但如前所述, 我们讨论的只是简化版 SimpleFuture. 本系列后续将逐步揭开面纱, 直至触及真正的 Future 特性.

First up, our SimpleFuture is pretty wasteful since we need to keep polling constantly, not just when there is anything useful for us to do. The solution to that is called a waker. But that’s a topic for next time…

首先, SimpleFuture 持续轮询的方式效率低下. 解决方案称为 waker, 这将是下期主题…

UPDATE: It’s now out, go take a look!

更新: 新篇已发布, 快去看看吧!


  1. Well actually, .await is defined by IntoFuture.. but that’s just a thin conversion wrapper. 实际上 .await 定义于 IntoFuture, 虽然只是个薄薄一层转换用的包装 (wrapper) 罢了. ↩2

  2. Like waiting for a timer, receiving a message over the network, that sort of thing. 像计时器, 从网络接收信息, 等等. ↩2

  3. If that sounds like an Option.. It basically is! Except the code often becomes clearer when our types embed the meaning that they represent. An Option could be None for many reasons, but a Pending is always a work in progress. 如果这听起来像是一个 Option… 它本质上就是! 只不过当我们的类型能嵌入其所代表的含义时, 代码通常会变得更清晰. Option 可能因多种原因而为 None, 但 Pending 始终代表进行中的工作. ↩2

  4. If this reminds you of the ? operator.. Yeah, this is another parallel. 如果这让你想起了 ? 操作符… 没错, 这是另一个相似之处. ↩2

  5. In reality, you’d want to use time instead of trying to count poll calls.. but dealing with time brings in more moving parts that I don’t want to deal with right now. 实际上, 你应该用时间而非试图统计轮询调用的次数… 但处理时间会引入更多我现在不想应对的复杂因素. (说白了就是用 std::time::Instant 记录起始时间, 调用 .elapsed() 获得已经过的时间.) ↩2

  6. Calling poll again after that point is undefined, but usually it’ll either panic or keep returning Pending forever. 在此之后再次调用 poll 的行为是未定义的, 但通常它要么会 panic, 要么永远返回 Pending 状态。 ↩2

  7. Someone once said something about the sanity that that would imply… 曾有人说过, 那暗示着某种理智…… ↩2

  8. And even those regular sequences need to call into primitives that actually do things eventually. Futures don’t just come fully formed out of the ether, after all. 即便是那些常规的序列, 最终也需要调用真正执行操作的底层原语. 毕竟, Future 不会凭空完整地出现. ↩2

  9. In our imaginary world where Rust supports await-ing SimpleFuture rather than Future, anyway. 在我们假想的世界里, Rust 支持 await 一个 SimpleFuture 而非 Future. ↩2

This Week in Rust (TWiR) Rust 语言周刊中文翻译计划, 第 595 期

本文翻译自 Natalie Klestrup Röijezon 的博客文章 https://natkr.com/2025-04-15-async-from-scratch-2/, 英文原文版权由原作者所有, 中文翻译版权遵照 CC BY-NC-SA 协议开放. 如原作者有异议请邮箱联系.

相关术语翻译依照 Rust 语言术语中英文对照表.

囿于译者自身水平, 译文虽已力求准确, 但仍可能词不达意, 欢迎批评指正.

2025 年 6 月 1 日下午, 于北京.

祝端午安康, 也祝孩子们六一国际儿童节快乐!

GitHub last commit

Async from scratch 2: Wake me maybe

So. You’ve read my last post. You got inspired. Excited, even. Deployed SimpleFuture to production. Spun up a few worker threads to share the load. Called it a friday. This is Rust after all, what could go wrong?

那么, 你应该读过我上一篇文章了. 你深受启发. 甚至很兴奋. 把 SimpleFuture 部署到了生产环境. 启动了几个工作线程分担负载. 周五就这么愉快地收工了. 毕竟这是 Rust, 能出什么问题呢?

…aaand then someone took a look at the CPU usage. … 然后有人看了眼CPU使用率.

A screenshot of htop showing the process “target/release/awesome-luggage-code-server” keeping 24 cores busy.

Oops. Good thing we’re not paying for those CPU hours anyway, right?

艹. 好在我们不用为这些 CPU 时间买单, 对吧?

Maybe we should look into some of those asterisks we left unresolved, after all. We won’t get through all of them today1, but we’ve got to start somewhere.

也许我们该看看之前留的那些带星号未解决的问题了. 今天肯定解决不完1, 但总得有个开始.

When is poll o’clock, anyway? | poll 到底是什么时候?

So this is the part where I start to pull back the curtain, and unravel the first lie: that poll is only responsible for one job (attempting to make progress).

现在我要揭开帷幕, 拆穿第一个谎言: poll 只负责一项工作 (尝试推进 Future 进度).

It actually has a secret second job: to ensure that whatever is running the Future is notified the next time that it would make sense to poll it again. This is where wakers (and, by extension, the Context that I handwaved away before) come in.2 It looks, roughly3, like this:

它其实还有个秘密任务: 确保运行 Future 的调度器能在下次应该轮询时得到通知. 这就是 waker (以及我之前一笔带过的 Context) 的用武之地2. 它大致长这样3:

#![allow(unused)]
fn main() {
use std::sync::Arc;

trait Wake: Send + Sync {
    // If you haven't seen `self: Foo<Self>` before, it lets you define methods that apply to certain wrapper types instead.
    // If it helps, `&self` is the same as `self: &Self`.
    //
    // 如果没见过`self: Foo<Self>`这种写法, 它允许你为特定包装类型定义方法
    // 类比来说, `&self`就等同于`self: &Self`
    fn wake(self: Arc<Self>);
}
struct Context {
    waker: Arc<dyn Wake>,
    // and some other stuff we don't really care about right now
    //
    // 其他暂时不关心的字段
}
}

The Future is responsible for ensuring that wake is called once there is something new to do, and the runtime is free to not bother polling the Future until that happens.

Future 需要确保在有新进展时调用 wake, 而运行时可以放心地不轮询 Future 直到被唤醒.

To manage this, we’ll need to change our (Simple)Future trait to propagate the context:

为此我们需要修改 (Simple)Future 以传递上下文:

#![allow(unused)]
fn main() {
use std::task::Poll;

trait SleepyFuture<Output> {
    fn poll(
        &mut self,
        // Our new and shiny
        context: &mut Context,
    ) -> Poll<Output>;
}
}

We’ve got to walk sleep before we can run | 先学会 睡觉才能跑

Now, our old runner is still basically legal.4 We could just keep polling constantly and provide a no-op Wake and to shut the compiler up. It’s always fine to poll our Future without being awoken.. the Future just can’t rely on it.

原来的执行器基本还能用4. 我们可以持续轮询并提供一个空实现的 Wake 来糊弄编译器. 未经唤醒就轮询 Future 总是安全的… 只是 Future 不能依赖这种行为.

#![allow(unused)]
fn main() {
struct InsomniacWaker;

impl Wake for InsomniacWaker {
    fn wake(self: Arc<Self>) {
        // Who needs to wake up if you never managed to fall asleep?
        // 从未入睡, 又何须唤醒?
    }
}

fn insomniac_runner<Output, F: SleepyFuture<Output>>(mut fut: F) -> Output {
    let mut context = Context {
        waker: Arc::new(InsomniacWaker),
    };
    loop {
        if let Poll::Ready(out) = fut.poll(&mut context) {
            return out;
        }
    }
}
}

But… that’s not particularly useful. We’re passing around the context now, but.. we’re still burning all that CPU time.

但这没啥用. 我们虽然传入了 context… 但 CPU 仍在空转.

Instead, we should provide a Waker that pauses the thread when there is nothing to do:

应该实现一个能让线程休眠的 Waker:

#![allow(unused)]
fn main() {
use std::sync::{Condvar, Mutex};

#[derive(Default)]
struct SleepWaker {
    awoken: Mutex<bool>,
    wakeup_cond: Condvar,
}

impl SleepWaker {
    fn sleep_until_awoken(&self) {
        let mut awoken = self.wakeup_cond
            .wait_while(
                self.awoken.lock().unwrap(),
                |awoken| !*awoken,
            )
            .unwrap();
        *awoken = false;
    }
}

impl Wake for SleepWaker {
    fn wake(self: Arc<Self>) {
        *self.awoken.lock().unwrap() = true;
        self.wakeup_cond.notify_one();
    }
}
}

Condvars are a whole rabbit hole of their own, but the idea here is basically that Condvar::wait_while runs some test on a Mutex-locked value every time notify_one is called (as well as on the initial wait_while call), but unlocks the Mutex in between5. sleep_until_awoken waits for wake to be called, and then resets itself so that it’s ready for the next call.6

Condvar 本身是个深坑, 但核心思想是: Condvar::wait_while 会在每次 notify_one 调用时 (包括初始调用) 检查 Mutex 锁住的值, 期间会释放锁5. sleep_until_awoken 等待唤醒后重置状态以备下次调用6.

Now we just need to change our runner to call sleep_until_awoken between each poll:

现在修改执行器在轮询间 sleep_until_awoken:

fn run_sleepy_future<Output, F: SleepyFuture>(mut fut: F) -> Output { let waker = Arc::::default(); let mut context = Context { waker: waker.clone() }; loop { match fut.poll(&mut context) { Poll::Ready(out) => return out, Poll::Pending => waker.sleep_until_awoken(), } } }

Just to be sure.. let’s try it out before continuing. To make sure that our wakeup works, and that we’re actually sleeping when we can:

测试一下确保唤醒机制有效:

#![allow(unused)]
fn main() {
struct ImmediatelyAwoken(bool);
impl SleepyFuture<()> for ImmediatelyAwoken {
    fn poll(&mut self, context: &mut Context) -> Poll<()> {
        if self.0 {
            Poll::Ready(())
        } else {
            self.0 = true;
            context.waker.clone().wake();
            Poll::Pending
        }
    }
}

struct BackgroundAwoken(bool);
impl SleepyFuture<()> for BackgroundAwoken {
    fn poll(&mut self, context: &mut Context) -> Poll<()> {
        if self.0 {
            Poll::Ready(())
        } else {
            self.0 = true;
            let waker = context.waker.clone();
            std::thread::spawn(|| {
                std::thread::sleep(std::time::Duration::from_millis(200));
                waker.wake();
            });
            Poll::Pending
        }
    }
}

let before_immediate = std::time::Instant::now();
run_sleepy_future(ImmediatelyAwoken(false));
println!("=> immediate: {:?}", before_immediate.elapsed());

let before_background = std::time::Instant::now();
run_sleepy_future(BackgroundAwoken(false));
println!("=> background: {:?}", before_background.elapsed());
}
=> immediate: 7µs
=> background: 200.148889ms

Whew! That looks reasonable to me, at least. Let’s move on, before the eye of Sauron insomnia sees us…

看起来没问题. 趁失眠的索伦之眼发现前继续…

A screenshot of a Detector Tower from the video game Helldivers 2, affectionately known as an “Eye of Sauron” for resembling a mechanical version of the Lord of the Rings “character”.

Return of the combinators | 组合起来

This also “just works” for most combinators, as long as they make sure to pass the Context down the tree. Here’s the the with_timeout example from last time; all we need to change is adding the context arguments and search/replacing7 SimpleFuture -> SleepyFuture:

只要确保 Context 能向下传递, 大多数组合子都能 “直接工作”. 这是上次的 with_timeout 改造版:

#![allow(unused)]
fn main() {
use std::task::ready;

struct Timeout<F> {
    inner: F,
    polls_left: u8,
}

#[derive(Debug)]
struct TimedOut;

impl<F, Output> SleepyFuture<Result<Output, TimedOut>> for Timeout<F>
where
    F: SleepyFuture<Output>,
{
    fn poll(
        &mut self,
        context: &mut Context,
    ) -> Poll<Result<Output, TimedOut>> {
        match self.polls_left.checked_sub(1) {
            Some(x) => self.polls_left = x,
            None => return Poll::Ready(Err(TimedOut)),
        }
        let inner = ready!(self.inner.poll(context));
        Poll::Ready(Ok(inner))
    }
}

fn with_timeout<F, Output>(
    inner: F,
    max_polls: u8,
) -> impl SleepyFuture<Result<Output, TimedOut>>
where
    F: SleepyFuture<Output>,
{
    Timeout {
        inner,
        polls_left: max_polls,
    }
}
}

Sleepy I/O (or: Showing our Interest) | 休眠式 I/O (或: 展示 Interest)

But this has all been (relatively) easy mode. It’s all useless, if we aren’t actually woken up for our I/O routines. Sadly… operating systems don’t officially support our (or Rust’s) Wake trait out of the box.

但这些都是简单模式. 如果不能为 I/O 操作唤醒, 一切都白搭. 可惜… 操作系统 (Rust 也是) 并不原生支持我们的 Wake trait.

Building on our old TcpRead example from last time, the Future itself is still pretty simple:

基于之前TcpRead 例子:

#![allow(unused)]
fn main() {
use std::{io::Read, net::TcpStream};

fn wake_when_readable(
    socket: &mut std::net::TcpStream,
    context: &mut Context,
) { todo!() }

struct TcpRead<'a> {
    socket: &'a mut TcpStream,
    buffer: &'a mut [u8],
}
impl SleepyFuture<usize> for TcpRead<'_> {
    fn poll(&mut self, context: &mut Context) -> Poll<usize> {
        match self.socket.read(self.buffer) {
            Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
                wake_when_readable(self.socket, context);
                Poll::Pending
            }
            size => Poll::Ready(size.unwrap()),
        }
    }
}
}

But.. uh.. how on earth do we define wake_when_readable? That.. is going to have to depend on your operating system, and is going outside of what the Rust standard library really provides for us.

但如何实现 wake_when_readable?这取决于操作系统, 超出了 Rust 标准库范畴.

Here in Linux-land8, the9 API for this is epoll. It still blocks, but it lets us ask the operating system to unpark us when any of a set of “files”10 are ready. In the Rust world, we can access this using the nix crate, which provides a safe but otherwise 1:1 mapping to the system API.11

在 Linux 世界8, epoll9 API 可以做到这点. 虽然仍会阻塞, 但能让我们在 “文件”10 就绪时被唤醒. Rust中可以通过 nix11 crate 使用这个 API.

The epoll API is fairly simple to use: we need to create an Epoll, register the events12 that we care about, and then wait for some events to occur. wait returns when any of the registered event(s) have occurred. When we’re done, we unregister the event.

epoll 用法简单: 创建 Epoll 实例, 注册关注的事件12, 然后等待所关注的事件发生. 完成后, 取消注册即可.

Now, in theory, we could wait from our main loop. It’s not like it has anything better to do while it’s waiting anyway. But wakes could come from anywhere, not just direct I/O events.13 And we need to handle all of them. So that’s out.

理论上, 我们可以让主循环原地等待. 反正它在等待期间也没有更重要的事情可做. 但唤醒信号可能来自任何地方, 而不仅仅是 I/O 事件13. 我们需要处理所有情况. 所以这个方案行不通.

So, instead, we’ll shove this off to a secondary I/O driver thread, which translates our epoll events into wakes. Which we already know how to handle!14

我们可以分出单独的 I/O 线程处理 epoll 事件并转换为 wake 调用, 那是我们已经知道怎么处理的14.

#![allow(unused)]
fn main() {
use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent};
use std::{collections::BTreeMap, sync::LazyLock};

static EPOLL: LazyLock<Epoll> =
    LazyLock::new(|| Epoll::new(EpollCreateFlags::EPOLL_CLOEXEC).unwrap());
static REGISTERED_WAKERS: Mutex<BTreeMap<u64, Arc<dyn Wake>>> = Mutex::new(BTreeMap::new());

fn io_driver() {
    let mut events = [EpollEvent::empty(); 16];
    loop {
        let event_count = EPOLL.wait(&mut events, 1000u16).unwrap();
        let wakers = REGISTERED_WAKERS.lock().unwrap();
        for event in &events[..event_count] {
            let waker_id = event.data();
            if let Some(waker) = wakers.get(&waker_id) {
                waker.clone().wake();
            } else {
                // This could also be an "innocent" race condition,
                // if the event is delivered just as we're deregistering a waker.
                println!("=> (Waker {waker_id} not found, bug?)")
            }
        }
    }
}
}

Then, we need some way to register an interest in a “file” (and unregister it when it isn’t needed anymore). This just ensures that it’ll be seen by our io_driver:

接着, 我们需要某种方式来注册对 “文件” 的关注 (并在不再需要时取消注册) . 这确保了它会被我们的 io_driver 感知到:

#![allow(unused)]
fn main() {
use nix::sys::epoll::EpollFlags;
use std::{ops::RangeFrom, os::fd::AsFd};

// We need some unique ID for each reason to be awoken..
// In reality you'd probably want some way to reuse these.
// 我们需要为每个唤醒原因分配一个唯一标识符..
// 实际上,你可能希望有某种方式来复用这些标识符。
static NEXT_WAKER_ID: Mutex<RangeFrom<u64>> = Mutex::new(0..);

struct Interest<T: AsFd> {
    // Interest needs to own the file (or borrow it),
    // to make sure that the file stays alive for as long as our interest does.
    // Interest 需要拥有文件 (或借用它),
    // 以确保文件的生命周期与我们的 interest 一样长。
    fd: T,
    registered_waker_id: Option<u64>,
}
impl<T: AsFd> Interest<T> {
    fn new(fd: T) -> Self {
        Interest {
            fd,
            registered_waker_id: None,
        }
    }

    fn register(&mut self, mut flags: EpollFlags, context: &mut Context) {
        let is_new = self.registered_waker_id.is_none();
        let id = *self
            .registered_waker_id
            .get_or_insert_with(|| NEXT_WAKER_ID.lock().unwrap().next().unwrap());
        REGISTERED_WAKERS
            .lock()
            .unwrap()
            .insert(id, context.waker.clone());
        // It's enough to get awoken once - if the Future is still interested then it should call `register`
        // to renew its interest.
        // 被唤醒一次就足够了. 如果 Future 仍有兴趣,则应调用 `register` 来续期其关注.
        flags |= EpollFlags::EPOLLONESHOT;
        let mut event = EpollEvent::new(flags, id);
        if is_new {
            EPOLL.add(&self.fd, event).unwrap()
        } else {
            EPOLL.modify(&self.fd, &mut event).unwrap()
        }
    }
}
impl<T: AsFd> Drop for Interest<T> {
    fn drop(&mut self) {
        if let Some(id) = self.registered_waker_id {
            // what if we have multiple interests open on the same fd? (read+write? multiple reads?)
            EPOLL.delete(&self.fd).unwrap();
            REGISTERED_WAKERS.lock().unwrap().remove(&id).unwrap();
        }
    }
}
}

Finally, we can slot this all into our TcpRead. we’ll need to change it slightly to keep the Interest’s state, but.. it should still be recognizable enough:

把这些融合进 TcpRead 里面:

#![allow(unused)]
fn main() {
use std::{io::Read, net::TcpStream};

struct TcpRead<'a> {
    socket: Interest<&'a mut TcpStream>,
    buffer: &'a mut [u8],
}
impl SleepyFuture<usize> for TcpRead<'_> {
    fn poll(&mut self, context: &mut Context) -> Poll<usize> {
        match self.socket.fd.read(self.buffer) {
            Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
                // EPOLLIN is the event for when we're allowed to read
                // from the "file".
                self.socket.register(EpollFlags::EPOLLIN, context);
                Poll::Pending
            }
            size => Poll::Ready(size.unwrap()),
        }
    }
}
}

Finally, we can put all the parts back together, and test it all against our old friend, the luggage code server:

最后让我们组合起来:

#![allow(unused)]
fn main() {
std::thread::spawn(io_driver);

let luggage_code_server_address = "127.0.0.1:9191";
let mut socket = TcpStream::connect(luggage_code_server_address).unwrap();
socket.set_nonblocking(true).unwrap();
let mut buffer = [0; 16];
let received = run_sleepy_future(
    // Let's limit the number of poll()s to make sure we're not cheating!
    // This is just for demonstration; remember that the runtime is /allowed/
    // to poll as often as it wants to.
    with_timeout(
        TcpRead {
            socket: Interest::new(&mut socket),
            buffer: &mut buffer,
        },
        // One initial poll to establish interest, then one poll once the data is ready for us.
        2,
    ),
).unwrap();
println!("=> The luggage code is {:?}", &buffer[..received]);
}
=> The luggage code is [1, 2, 3, 4, 5]

Success! We’re back to where we started.. but at least our computer15 can rest a bit easier.

成功! 虽然回到了原点… 但至少电脑15能轻松点了.

Another wrap… for now | 阶段性总结…

Hopefully, you have a bit more of an idea about what that weird Context thing is now.

现在你应该更理解那个奇怪的 Context 了.

But we’re still not quite back at the real Future trait16. So.. the next entry will be about just that: clearing up the remaining concepts we need to understand to be able to read the Future.17

但我们还没完全还原真正的 Future trait16. 下篇文章将补齐剩余概念, 让我们能完全理解 Future17.


  1. It’s a series for a reason, after all. 总归是一个系列. ↩2

  2. Not to be confused with the quakers or shakers. ↩2

  3. Actually, Context contains a Waker instead. It’s close enough to our Arc<dyn Wake>, but doesn’t require using Arc if you’re okay maintaining your own vtable. If the word “vtable” tells you nothing, just implementWake and be done with it. If the word “vtable” does tell you something… you should probably still just implement Wake and be done with it. 实际上, Context 内部包含的是一个 Waker. 它与我们的 Arc<dyn Wake> 非常接近, 但如果你愿意维护自己的虚函数表 (vtable) , 就不需要使用 Arc. 如果 “vtable” 这个词对你毫无意义, 直接实现 Wake trait 即可. 即便 “vtable” 这个词对你有所触动… 你可能还是应该直接实现 Wake 完事. ↩2

  4. It’s not self-plagiarism if we cite it! Oh, and I guess it fulfills the type contracts, too… 只要引用了就不算自我剽窃! 哦, 我想这也符合类型契约的要求吧… ↩2

  5. Otherwise, we’d never be able to modify the Mutex-guarded value! 否则无法修改 Mutex 锁定的值! ↩2

  6. If this looks like we’re just reimplementing Condvar.. we are, kind of. Except Condvar::wait isn’t specified to return immediately if notify_one was was called before wait. That’s a problem for us; it would silently prevent the Future from waking itself during the poll. 如果这看起来像是我们在重新实现 Condvar… 某种程度上确实如此. 只不过 Condvar::wait 并未规定若在 wait 之前调用了 notify_one 就立即返回. 这对我们而言是个问题;它会悄无声息地阻止 Futurepoll 期间自我唤醒. ↩2

  7. I hear verbing is so hot this year. Can we verb a nouned verb?

  8. Cross-platform support is left as an exercise for the reader. Enjoy! 跨平台支持作为练习留给读者完成. 享受吧! ↩2

  9. Not the only API, there are others. But it’s the one that hits the “standard” tradeoff between not being too slow or too experimental. Maybe io_uring will be everywhere in a few years, when you’re the one writing the “Everything natkr got wrong” article. When you do, please send it to me, I look forward to reading it! 并非唯一的 API, 还有其他选择. 但它是那个在 “不过于缓慢” 与 “不过于实验性” 之间找到了 “标准” 平衡点的方案. 或许几年后 io_uring 会无处不在, 届时就该由你来写那篇《natkr犯下的所有错误》了. 写完后请务必发给我, 我期待着拜读! ↩2

  10. In the Unixy sense where “everything” is a “file”, including network sockets. Unix 万物皆文件, 包括网络套接字. ↩2

  11. For anyone following along at home, I’m going to be using nix v0.29.0, since that’s the latest version when I’m writing this. 我将应用 nix v0.29.0 作示范, 这是我写作此文时的最新版本. ↩2

  12. It would’ve been nice if we could link to specifically the list of event flags, but alas… 要是能直接链接到具体的事件标志列表就好了, 可惜… ↩2

  13. Timers, background threads, and so on… 计算器, 后台线程, 等等. ↩2

  14. Sometimes this is called a reactor. 有时也称 reactor. ↩2

  15. And power bill. 以及电费账单. ↩2

  16. It’d sure be helpful if it ever felt like standing up. 能站起来就好. ↩2

  17. Spoiler: This means Pin and associated types. 剧透: 包括 Pin 及其关联类型. ↩2

This Week in Rust (TWiR) Rust 语言周刊中文翻译计划, 第 595 期

本文翻译自 Ana Hobden 的博客文章 https://hoverbear.org/blog/rust-state-machine-pattern/, 英文原文版权由原作者所有, 中文翻译版权遵照 CC BY-NC-SA 协议开放. 如原作者有异议请邮箱联系.

相关术语翻译依照 Rust 语言术语中英文对照表.

囿于译者自身水平, 译文虽已力求准确, 但仍可能词不达意, 欢迎批评指正.

2025 年 5 月 31 日下午, 于北京.

GitHub last commit

Rust 状态机模式

Lately I’ve been thinking a lot about the patterns and structures which we program with. It’s really wonderful to start exploring a project and see familiar patterns and styles which you’ve already used before. It makes it easier to understand the project, and empowers you to start working on the project faster.

最近, 我一直在思考我们编程时使用的模式和结构. 开始探索一个项目并看到你以前已经使用过的熟悉的模式和样式, 这真是太棒了. 它使您更容易理解项目, 并使您能够更快地开始处理项目.

Sometimes you’re working on a new project and realize that you need to do something in the same way as you did in another project. This thing might not be a functionality or a library, it might not be something which you can encode into some clever macro or small crate. Instead, it may be simply a pattern, or a structural concept which addresses a problem nicely.

有时, 您正在处理一个新项目, 并意识到您需要以与在另一个项目中相同的方式执行某些作. 这个东西可能不是一个功能或库, 它可能不是你可以编码到一些聪明的宏或小 crate 中的东西. 相反, 它可能只是一个模式, 或者一个很好地解决了一个问题的结构概念.

One interesting pattern that is commonly applied to problems is that of the ‘State Machine’. Let’s take some time to consider what exactly we mean when we say that, and why they’re interesting.

一个有趣的模式是状态机模式. 让我们花点时间考虑一下我们这么说到底是什么意思, 以及为什么它们很有趣.

Throughout this post you can run all examples in the playground, I typically use ‘Nightly’ out of habit.

在这篇文章中, 你可以在 Rust Playground 中运行所有示例, 我通常出于习惯使用 nightly Rust.

TOC

Founding Our Concepts | 概念

There are a lot of resources and topical articles about state machines out there on the internet. Even more so, there are a lot of implementations of state machines.

互联网上有很多关于状态机的资源和主题文章. 更重要的是, 状态机有很多实现.

Just to get to this web page you used one. You can model TCP as a state machine. You can model HTTP requests with one too. You can model any regular language, such as a regex, as a state machine. They’re everywhere, hiding inside things we use every day.

只是为了访问这个网页, 你用了一个状态机. 您可以将 TCP 建模为状态机. 您也可以对 HTTP 请求建模为状态机. 您可以将任何常规语言 (例如正则表达式) 建模为状态机. 它们无处不在, 隐藏在我们每天使用的物品中.

So, a State Machine is any ‘machine’ which has a set of ‘states’ and ‘transitions’ defined between them.

因此, 状态机是任何在概念之间定义了一组 “状态” 和 “转换” 过程的 “机器”.

When we talk about a machine we’re referring to the abstract concept of something which does something. For example, your ‘Hello World!’ function is a machine. It is started and eventually outputs what we expect it to. Some model which you use to interact with your database is just the same. We’ll regard our most basic machine simply as a struct that can be created and destroyed.

当我们谈论机器时, 我们指的是做某事的抽象概念. 例如, 你的 ‘Hello World!’ 函数是一台机器. 它被启动并最终输出我们期望的结果. 您用来与数据库交互的某些模型是相同的. 我们将最基本的机器简单地视为可以创建和销毁的 struct.

struct Machine;

fn main() {
  let my_machine = Machine; // Create.
  // `my_machine` is destroyed when it falls out of scope below.
  // `my_machine` 离开作用域即被销毁
}

States are a way to reason about where a machine is in its process. For example, we can think about a bottle filling machine as an example. The machine is in a ‘waiting’ state when it is waiting for a new bottle. Once it detects a bottle it moves to the ‘filling’ state. Upon detecting the bottle is filled it enters the ‘done’ state. After the bottle is left the machine we return to the ‘waiting’ state.

状态是推理机器在其过程中所处位置的一种方式. 例如, 我们可以以瓶子灌装机为例. 机器在等待新瓶子时处于 “等待” 状态. 一旦检测到瓶子, 它就会进入 “充装” 状态. 检测到瓶子已装满后, 它会进入 “完成” 状态. 瓶子离开机器后, 我们返回 “等待” 状态.

A key takeaway here is that none of the states have any information relevant for the other states. The ‘filling’ state doesn’t care how long the ‘waiting’ state waited. The ‘done’ state doesn’t care about what rate the bottle was filled at. Each state has discrete responsibilities and concerns. The natural way to consider these variants is as an enum.

这里的一个关键要点是, 状态间是独立的. “充装” 状态下并不关心 “等待” 状态持续了多长时间. “完成” 状态也不关心瓶子的充装速率. 每个状态都有不同的责任和关注点. 考虑这些变体的自然方法是枚举 (enum).

#![allow(unused)]
fn main() {
enum BottleFillerState {
  Waiting { waiting_time: std::time::Duration },
  Filling { rate: usize },
  Done,
}

struct BottleFiller {
  state: BottleFillerState,
}
}

Using an enum in this way means all the states are mutually exclusive, you can only be in one at a time. Rust’s ‘fat enums’ allow us to have each of these states to carry data with them as well. As far as our current definition is concerned, everything is totally okay.

以这种方式使用 enum 意味着所有状态都是互斥的, 您一次只能处于一个状态. Rust 的胖 enums 允许我们让这些状态随身携带数据. 就我们目前的定义而言, 一切都完全没问题.

But there is a bit of a problem here. When we described our bottle filling machine above we described three transitions: Waiting -> Filling, Filling -> Done, and Done -> Waiting. We never described Waiting -> Done or Done -> Filling, those don’t make sense!

但这里有一点问题. 当我们在上面描述我们的瓶子灌装机时, 我们描述了三个转换关系: 等待 -> 充装充装 -> 完成完成 -> 等待. 我们从来没有描述过等待 -> 完成完成 -> 装, 这些都没有意义!

This brings us to the idea of transitions. One of the nicest things about a true state machine is we never have to worry about our bottle machine going from Done -> Filling, for example. The state machine pattern should enforce that this can never happen. Ideally this would be done before we even start running our machine, at compile time.

由此引入转换的概念. 例如, 真实状态机最好的一点是, 我们永远不必担心我们的瓶子机会从 完成 -> 充装 开始. 状态机模式应该强制要求这种情况永远不会发生. 理想情况下, 这应该在我们开始运行我们的机器之前, 或者说在编译时完成检查.

Let’s look again at the transitions we described for our bottle filler in a diagram:

让我们再次看一下我们在图表中为瓶子灌装机描述的过渡:

  +++++++++++   +++++++++++   ++++++++
  |         |   |         |   |      |
  | Waiting +-->+ Filling +-->+ Done |
  |         |   |         |   |      |
  ++++-++++-+   +++++++++++   +--+++++
       ^                         |
       +++++++++++++++++++++++++-+

As we can see here there are a finite number of states, and a finite number of transitions between these states. Now, it is possible to have a valid transition between each state and every other state, but in most cases this is not true.

正如我们在这里看到的, 状态的数量是有限的, 这些状态之间的转换数量是有限的. 现在, 每个状态和每个其他状态之间可以有一个有效的转换, 但在大多数情况下, 情况并非如此.

This means moving between a state such as ‘Waiting’ to a state such as ‘Filling’ should have defined semantics. In our example this can be defined as “There is a bottle in place.” In the case of a TCP stream it might be “We have received a FIN packet” which means we need to finish closing out the stream.

这意味着在状态 (如 “等待”) 和状态 (如 “充装”) 之间转换时, 应定义语义. 在我们的示例中, 这可以定义为 “瓶子就位”. 对于 TCP 流, 它可能是 “收到 FIN”, 这意味着我们需要完成关闭流.

Determining What We Want | 目标

Now that we know what a state machine is, how do we represent them in Rust? First, let’s think about what we want from some pattern.

现在我们知道了什么是状态机, 我们如何在 Rust 中表示它们呢? 首先, 让我们考虑一下我们想要从某个模式中得到什么.

Ideally, we’d like to see the following characteristics:

理想情况下, 我们希望看到以下特征:

  • Can only be in one state at a time.

    一次只能处于一种状态.

  • Each state should have its own associated values if required.

    如果需要, 每个状态都应该有自己的关联值.

  • Transitioning between states should have well defined semantics.

    状态之间的转换应该有明确定义的语义.

  • It should be possible to have some level of shared state.

    应该可以有一定程度的共享状态.

  • Only explicitly defined transitions should be permitted.

    只允许显式定义的过渡.

  • Changing from one state to another should consume the state so it can no longer be used.

    从一种状态更改为另一种状态应该会消耗该状态, 因此它不能再被使用.

  • We shouldn’t need to allocate memory for all states. No more than largest sized state certainly

    我们不需要为所有状态分配内存. 肯定不超过最大的状态(所将占用的).

  • Any error messages should be easy to understand.

    任何错误消息都应该易于理解.

  • We shouldn’t need to resort to heap allocations to do this. Everything should be possible on the stack.

    我们不应求助于堆分配. 尽量在栈上操作.

  • The type system should be harnessed to our greatest ability.

    应当充分利用类型系统.

  • As many errors as possible should be at compile-time.

    尽量在编译时考虑到错误.

So if we could have a design pattern which allowed for all these things it’d be truly fantastic. Having a pattern which allowed for most would be pretty good too.

因此, 如果我们能有一个允许所有这些事情的设计模式, 那将非常棒. 拥有一个允许大多数人的模式也很好.

Exploring Possible Implementation Options | 探索

With a type system as powerful and flexible as Rusts we should be able to represent this. The truth is: there are a number of ways to try, each has valuable characteristics, and each teaches us lessons.

有了像 Rust 这样强大和灵活的类型系统, 我们应该能够处理这些要求. 事实是: 有很多方法可以尝试, 每一种都有有价值的特性, 每一种都能给我们带来教训.

A Second Shot with Enums | 再看看枚举

As we saw above the most natural way to attempt this is an enum, but we noted already that you can’t control which transitions are actually permitted in this case. So can we just wrap it? We sure can! Let’s take a look:

正如我们在上面看到的, 实现状态机模式的最自然方法是枚举, 但我们已经注意到, 在这种情况下, 您无法控制实际允许的过渡. 那么我们能不能做一些包装呢? 我们当然可以! 让我们来看看:

enum State {
    Waiting { waiting_time: std::time::Duration },
    Filling { rate: usize },
    Done
}

struct StateMachine { state: State }

impl StateMachine {
    fn new() -> Self {
        StateMachine {
            state: State::Waiting { waiting_time: std::time::Duration::new(0, 0) }
        }
    }
    fn to_filling(&mut self) {
        self.state = match self.state {
            // Only Waiting -> Filling is valid.
            State::Waiting { .. } => State::Filling { rate: 1 },
            // The rest should fail.
            _ => panic!("Invalid state transition!"),
        }
    }
    // ...
}

fn main() {
    let mut state_machine = StateMachine::new();
    state_machine.to_filling();
}

At first glance it seems okay. But notice some problems?

乍一看似乎还不错. 但是注意到一些问题了吗?

  • Invalid transition errors happen at runtime, which is awful!

    无效的过渡错误发生在运行时, 这太可怕了!

  • This only prevents invalid transitions outside of the module, since the private fields can be manipulated freely inside the module. For example, state_machine.state = State::Done is perfectly valid inside the module.

这只能防止 module 之外的无效转换, module 内部是不限制的. 例如, state_machine.state = State::Done 在模块内是完全有效的.

  • Every function we implement that works with the state has to include a match statement!

    我们实现的每个与 state 一起使用的函数都必须包含一个 match 语句!

However this does have some good characteristics:

但是, 这确实具有一些很好的特性:

  • The memory required to represent the state machine is only the size of the largest state. This is because a fat enum is only as big as its biggest variant.

    表示状态机的结构大小取决于最大的变体的大小. 这是因为胖枚举的大小取决于其最大的变体.

  • Everything happens on the stack.

  • 一切都发生在栈上.

  • Transitioning between states has well defined semantics… It either works or it crashes!

    状态之间的转换具有明确定义的语义… 要么有效, 要么崩溃!

Now you might be thinking “Hoverbear you could totally wrap the to_filling() output with a Result<T, E> or have an InvalidState variant!” But let’s face it: That doesn’t make things that much better, if at all. Even if we get rid of the runtime failures we still have to deal with a lot of clumsiness with the match statements and our errors would still only be found at runtime! Ugh! We can do better, I promise.

现在你可能会想, “Hoverbear, 你可以完全用 Result<T, E> 或有 InvalidState 变体来包装 to_filling() 输出! 但让我们面对现实吧: 这并没有让事情变得更好, 如果有的话. 即使我们摆脱了运行时的失败, 我们仍然需要处理 match 语句的许多笨拙问题, 我们的错误仍然只能在运行时发现! 呸! 我可以做得更好, 我保证.

So let’s keep looking!

所以让我们继续寻找吧!

Structures With Transitions | 带转换的结构体

So what if we just used a set of structs? We could have them all implement traits which all states should share. We could use special functions that transitioned the type into the new type! How would it look?

那么, 如果我们只使用一组结构体呢? 我们可以让它们都实现所有状态都应该共享的特征. 我们可以使用特殊函数将类型转换为新类型! 它会是什么样子?

// This is some functionality shared by all of the states.
trait SharedFunctionality {
    fn get_shared_value(&self) -> usize;
}

struct Waiting {
    waiting_time: std::time::Duration,
    // Value shared by all states.
    shared_value: usize,
}

impl Waiting {
    fn new() -> Self {
        Waiting {
            waiting_time: std::time::Duration::new(0,0),
            shared_value: 0,
        }
    }

    // Consumes the value!
    fn to_filling(self) -> Filling {
        Filling {
            rate: 1,
            shared_value: 0,
        }
    }
}

impl SharedFunctionality for Waiting {
    fn get_shared_value(&self) -> usize {
        self.shared_value
    }
}

struct Filling {
    rate: usize,
    // Value shared by all states.
    shared_value: usize,
}

impl SharedFunctionality for Filling {
    fn get_shared_value(&self) -> usize {
        self.shared_value
    }
}

// ...

fn main() {
    let in_waiting_state = Waiting::new();
    let in_filling_state = in_waiting_state.to_filling();
}

Gosh that’s a buncha code! So the idea here was that all states have some common shared values along with their own specialized values. As you can see from the to_filling() function we can consume a given ‘Waiting’ state and transition it into a ‘Filling’ state. Let’s do a little rundown:

天哪, 这真是一大坨代码! 所以这里的想法是, 所有状态都有一些共同的值以及他们自己的特定值. 正如你从 to_filling() 函数中看到的, 我们可以消耗掉给定的 “等待” 状态并将其转换为 “充装” 状态. 让我们做一个小概要:

  • Transition errors are caught at compile time! For example you can’t even create a Filling state accidentally without first starting with a Waiting state. (You could on purpose, but this is beside the matter.)

    在编译时捕获转换错误!例如, 如果不先从 “等待” 状态开始, 你甚至不能意外地创建一个 “充装” 状态. (你可以故意的, 但这不是问题.)

  • Transition enforcement happens everywhere.

    转换的强制保证无处不在.

  • When a transition between states is made the old value is consumed instead of just modified. We could have done this with the enum example above as well though.

    在状态之间进行转换时, 将消费掉旧值, 而不仅仅是修改. 不过, 我们也可以使用上面的 enum 示例来做到这一点.

  • We don’t have to match all the time.

    我们不必一直 match.

  • Memory consumption is still lean, at any given time the size is that of the state.

    内存消耗仍然很少, 在任何给定时间, 大小都是状态的大小.

There are some downsides though:

但也有一些缺点:

  • There is a bunch of code repetition. You have to implement the same functions and traits for multiple structures.

    有一堆代码重复. 您必须为多个结构实现相同的函数和 trait.

  • It’s not always clear what values are shared between all states and just one. Updating code later could be a pain due to this.

    并不总是很清楚所有状态和只有一个状态之间共享哪些值. 因此, 稍后更新代码可能会很痛苦.

  • Since the size of the state is variable we end up needing to wrap this in an enum as above for it to be usable where the state machine is simply one component of a more complex system. Here’s what this could look like:

    由于 state 的大小是可变的, 我们最终需要像上面一样将其包装在 enum 中, 以便在状态机只是更复杂系统的一个组件时可用. 具体情况如下:

enum State {
    Waiting(Waiting),
    Filling(Filling),
    Done(Done),
}

fn main() {
    let in_waiting_state = State::Waiting(Waiting::new());
    // This doesn't work since the `Waiting` struct is wrapped! We need to `match` to get it out.
    let in_filling_state = State::Filling(in_waiting_state.to_filling());
}

As you can see, this isn’t very ergonomic. We’re getting closer to what we want though. The idea of moving between distinct types seems to be a good way forward! Before we go try something entirely different though, let’s talk about a simple way to change our example that could enlighten further thinking.

如您所见, 这并不是很符合人体工程学. 不过, 我们越来越接近我们想要的. 在不同类型之间移动的想法似乎是一个很好的前进方向! 不过, 在我们尝试完全不同的东西之前, 让我们谈谈一种简单的方法来改变我们的例子, 这可能会启发进一步的思考.

The Rust standard library defines two highly related traits: From and Into that are extremely useful and worth checking out. An important thing to note is that implementing one of these automatically implements the other. In general implementing From is preferable as it’s a bit more flexible. We can implement them very easily for our above example like so:

Rust 标准库定义了两个高度相关的 trait: FromInto , 它们非常有用, 值得一试. 需要注意的重要一点是, 实现其中一个会自动实现另一个. 一般来说, 实现 From 是可取的, 因为它更灵活一些. 对于上面的示例, 我们可以很容易地实现它们, 如下所示:

#![allow(unused)]
fn main() {
// ...
impl From<Waiting> for Filling {
    fn from(val: Waiting) -> Filling {
        Filling {
            rate: 1,
            shared_value: val.shared_value,
        }
    }
}
// ...
}

Not only does this give us a common function for transitioning, but it also is nice to read about in the source code! This reduces mental burden on us and makes it easier for readers to comprehend. Instead of implementing custom functions we’re just using a pattern already existing. Building our pattern on top of already existing patterns is a great way forward.

这不仅为我们提供了一个通用的转换函数, 而且在源代码中阅读也很好!这减轻了我们的精神负担, 使读者更容易理解. 我们没有实现自定义函数, 而是使用已经存在的模式. 在现有模式之上构建我们的模式是一种很好的前进方式.

So this is cool, but how do we deal with all this nasty code repetition and the repeating shared_value stuff? Let’s explore a bit more!

所以这很酷, 但是我们如何处理所有这些令人讨厌的代码重复和重复的 shared_value 内容呢? 让我们进一步探索一下!

Generically Sophistication | 通用而精妙的做法

In this adventure we’ll combine lessons and ideas from the first two, along with a few new ideas, to get something more satisfying. The core of this is to harness the power of generics. Let’s take a look at a fairly bare structure representing this:

在这次冒险中, 我们将结合前两个的经验教训和想法, 以及一些新的想法, 以获得更令人满意的东西. 其核心是利用泛型的强大功能. 让我们看一下表示这一点的相当简单的结构:

#![allow(unused)]
fn main() {
struct BottleFillingMachine<S> {
    shared_value: usize,
    state: S
}

// The following states can be the 'S' in StateMachine<S>

struct Waiting {
    waiting_time: std::time::Duration,
}

struct Filling {
    rate: usize,
}

struct Done;
}

So here we’re actually building the state into the type signature of the BottleFillingMachine itself. A state machine in the ‘Filling’ state is BottleFillingMachine which is just awesome since it means when we see it as part of an error message or something we know immediately what state the machine is in.

所以在这里, 我们实际上是将状态构建到 BottleFillingMachine 本身的类型签名中. 处于充装状态的状态机是 BottleFillingMachine<Filling>, 这真是太棒了, 因为这意味着当我们看到它作为错误消息的一部分或内容时, 我们会立即知道机器处于什么状态.

From there we can go ahead and implement From for some of these specific generic variants like so:

从那里, 我们可以继续为其中一些特定的通用变体实现 From<T>, 如下所示:

#![allow(unused)]
fn main() {
impl From<BottleFillingMachine<Waiting>> for BottleFillingMachine<Filling> {
    fn from(val: BottleFillingMachine<Waiting>) -> BottleFillingMachine<Filling> {
        BottleFillingMachine {
            shared_value: val.shared_value,
            state: Filling {
                rate: 1,
            }
        }
    }
}

impl From<BottleFillingMachine<Filling>> for BottleFillingMachine<Done> {
    fn from(val: BottleFillingMachine<Filling>) -> BottleFillingMachine<Done> {
        BottleFillingMachine {
            shared_value: val.shared_value,
            state: Done,
        }
    }
}
}

Defining a starting state for the machine looks like this:

定义计算机的起始状态如下所示:

#![allow(unused)]
fn main() {
impl BottleFillingMachine<Waiting> {
    fn new(shared_value: usize) -> Self {
        BottleFillingMachine {
            shared_value: shared_value,
            state: Waiting {
                waiting_time: std::time::Duration::new(0, 0),
            }
        }
    }
}
}

So how does it look to change between two states? Like this:

那么, 两个状态之间的变化会是什么样子呢? 喜欢这个:

fn main() {
    let in_waiting = BottleFillingMachine::<Waiting>::new(0);
    let in_filling = BottleFillingMachine::<Filling>::from(in_waiting);
}

Alternatively if you’re doing this inside of a function whose type signature restricts the possible outputs it might look like this:

或者, 如果你在一个类型签名限制了可能输出的函数内部执行此作, 它可能看起来像这样:

#![allow(unused)]
fn main() {
fn transition_the_states(val: BottleFillingMachine<Waiting>) -> BottleFillingMachine<Filling> {
    val.into() // Nice right?
}
}

What do the compile time error messages look like? 编译时错误消息是什么样的?

error[E0277]: the trait bound `BottleFillingMachine<Done>: std::convert::From<BottleFillingMachine<Waiting>>` is not satisfied
  --> <anon>:50:22
   |
50 |     let in_filling = BottleFillingMachine::<Done>::from(in_waiting);
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = help: the following implementations were found:
   = help:   <BottleFillingMachine<Filling> as std::convert::From<BottleFillingMachine<Waiting>>>
   = help:   <BottleFillingMachine<Done> as std::convert::From<BottleFillingMachine<Filling>>>
   = note: required by `std::convert::From::from`

It’s pretty clear what’s wrong from that. The error message even hints to us some valid transitions!

这很清楚哪里出了问题. 错误消息甚至向我们提示了一些有效的转换!

So what does this scheme give us?

那么这个给我们带来了什么呢?

  • Transitions are ensured to be valid at compile time.

    编译时检查确保转换有效.

  • The error messages about invalid transitions are very understandable and even list valid options.

    有关无效转换的错误消息很容易理解, 甚至列出了有效的选项.

  • We have a ‘parent’ structure which can have traits and values associated with it that aren’t repeated.

    我们有一个父结构, 它可以有与之关联的 trait 和值, 这些 trait 和值不会重复.

  • Once a transition is made the old state no longer exists, it is consumed. Indeed, the entire structure is consumed so if there are side effects of the transition on the parent (for example altering the average waiting time) we can’t access stale values.

    一旦进行了转换, 旧状态就不再存在, 它将被消耗掉. 事实上, 整个结构都被消耗掉了, 所以如果转换对父级有副作用 (例如改变平均等待时间), 我们就无法访问过时的值.

  • Memory consumption is lean and everything is on the stack.

    内存消耗很少, 一切都在栈上.

There are some downsides still:

仍然有一些缺点:

  • Our From<T> implementations suffer from a fair bit of “type noise”. This is a highly minor concern though. 我们的 From 实现受到相当多的 “类型干扰” 的影响. 不过, 这是一个非常小的问题.

  • Each BottleFillingMachine<S> has a different size, with our previous example, so we’ll need to use an enum. Because of our structure though we can do this in a way that doesn’t completely suck.

    每个 BottleFillingMachine<S> 都有不同的大小, 就像我们前面的例子一样, 所以我们需要使用一个枚举. 不过, 由于我们的结构, 我们可以以一种不完全糟糕的方式做到这一点.

You can play with this example here

您可以在此处运行示例

Getting Messy With the Parents | 结合父结构

So how can we have some parent structure hold our state machine without it being a gigantic pain to interact with? Well, this circles us back around to the enum idea we had at first.

那么, 我们如何让一些父结构来支撑我们的状态机, 而不会造成巨大的交互痛苦呢? 好吧, 这让我们回到了我们最初的 enum 想法.

If you recall the primary problem with the enum example above was that we had to deal with no ability to enforce transitions, and the only errors we got were at runtime when we did try.

如果你还记得上面 enum 示例的主要问题是, 我们没有办法做到转换的编译时强制保证, 错误会在运行时抛出.

#![allow(unused)]
fn main() {
enum BottleFillingMachineWrapper {
    Waiting(BottleFillingMachine<Waiting>),
    Filling(BottleFillingMachine<Filling>),
    Done(BottleFillingMachine<Done>),
}

struct Factory {
    bottle_filling_machine: BottleFillingMachineWrapper,
}

impl Factory {
    fn new() -> Self {
        Factory {
            bottle_filling_machine: BottleFillingMachineWrapper::Waiting(BottleFillingMachine::new(0)),
        }
    }
}
}

At this point your first reaction is likely “Gosh, Hoverbear, look at that awful and long type signature!” You’re quite right! Frankly it’s rather long, but I picked long, explanatory type names! You’ll be able to use all your favorite arcane abbreviations and type aliases in your own code. Have at!

这时你的第一反应可能是“天哪, Hoverbear, 看看那个又糟糕又长的类型签名! 你说得很对! 坦率地说, 它相当长, 但我选择了长长的、解释性的字体名称! 您能够随意在自己的代码中使用所有您最喜欢的晦涩难懂的缩写和类型别名.

impl BottleFillingMachineWrapper {
    fn step(mut self) -> Self {
        match self {
            BottleFillingMachineWrapper::Waiting(val) => BottleFillingMachineWrapper::Filling(val.into()),
            BottleFillingMachineWrapper::Filling(val) => BottleFillingMachineWrapper::Done(val.into()),
            BottleFillingMachineWrapper::Done(val) => BottleFillingMachineWrapper::Waiting(val.into()),
        }
    }
}

fn main() {
    let mut the_factory = Factory::new();
    the_factory.bottle_filling_machine = the_factory.bottle_filling_machine.step();
}

Again you may notice that this works by consumption not mutation. Using match the way we are above moves val so that it can be used with .into() which we’ve already determined should consume the state. If you’d really like to use mutation you can consider having your states #[derive(Clone)] or even Copy, but that’s your call.

同样, 你可能会注意到, 这是通过消费而不是可变性来实现的. 按照上面的方式使用 match 会移动值, 以便它可以与 .into() 一起使用, 我们已经确定应该消耗状态值. 如果你真的想使用可变性, 你可以考虑让你的状态 #[derive(Clone)] 甚至 Copy, 但那是你的决定.

Despite this being a bit less ergonomic and pleasant to work with than we might want we still get strongly enforced state transitions and all the guarantees that come with them.

这一方面比我们想要的更符合人体工程学和使用起来更舒适, 另一方面我们仍然得到了强烈执行的状态转换和随之而来的所有保证.

One thing you will notice is this scheme does force you to handle all potential states when manipulating the machine, and that makes sense. You are reaching into a structure with a state machine and manipulating it, you need to have defined actions for each state that it is in.

你会注意到的一件事是, 这个方案确实会迫使你在作机器时处理所有潜在的状态, 这是有道理的. 您正在处理代表状态机的结构, 您需要为它所处的每个状态作定义.

Or you can just panic!() if that’s what you really want. But if you just wanted to panic!() then why didn’t you just use the first attempt?

或者, 如果这是你真正想要的, 你可以只 panic!(). 但是, 如果您只是想 panic!(), 那么您为什么不使用前面第一种方法呢?

You can see a fully worked example of this Factory example here.

您可以在此处查看此示例的完整代码.

Worked Examples | 实际示例

This is the kind of thing it’s always nice to have some examples for. So below I’ve put together a couple worked examples with comments for you to explore.

这种事情总是很高兴能有一些例子. 因此, 下面我整理了几个带有注释的工作示例供您探索.

Three State, Two Transitions | 三种状态, 两种转换

This example is very similar to the Bottle Filling Machine above, but instead it actually does work, albeit trivial work. It takes a string and returns the number of words in it.

这个例子与上面的瓶子灌装机非常相似, 但它实际上确实有效, 尽管工作微不足道. 它接受一个字符串并返回其中的单词数.

fn main() {
    // The `<StateA>` is implied here. We don't need to add type annotations!
    let in_state_a = StateMachine::new("Blah blah blah".into());

    // This is okay here. But later once we've changed state it won't work anymore.
    in_state_a.some_unrelated_value;
    println!("Starting Value: {}", in_state_a.state.start_value);


    // Transition to the new state. This consumes the old state.
    // Here we need type annotations (since not all StateMachines are linear in their state).
    let in_state_b = StateMachine::<StateB>::from(in_state_a);

    // This doesn't work! The value is moved when we transition!
    // in_state_a.some_unrelated_value;
    // Instead, we can use the existing value.
    in_state_b.some_unrelated_value;

    println!("Interm Value: {:?}", in_state_b.state.interm_value);

    // And our final state.
    let in_state_c = StateMachine::<StateC>::from(in_state_b);

    // This doesn't work either! The state doesn't even contain this value.
    // in_state_c.state.start_value;

    println!("Final state: {}", in_state_c.state.final_value);
}

// Here is our pretty state machine.
struct StateMachine<S> {
    some_unrelated_value: usize,
    state: S,
}

// It starts, predictably, in `StateA`
impl StateMachine<StateA> {
    fn new(val: String) -> Self {
        StateMachine {
            some_unrelated_value: 0,
            state: StateA::new(val)
        }
    }
}

// State A starts the machine with a string.
struct StateA {
    start_value: String,
}
impl StateA {
    fn new(start_value: String) -> Self {
        StateA {
            start_value: start_value,
        }
    }
}

// State B goes and breaks up that String into words.
struct StateB {
    interm_value: Vec<String>,
}
impl From<StateMachine<StateA>> for StateMachine<StateB> {
    fn from(val: StateMachine<StateA>) -> StateMachine<StateB> {
        StateMachine {
            some_unrelated_value: val.some_unrelated_value,
            state: StateB {
                interm_value: val.state.start_value.split(" ").map(|x| x.into()).collect(),
            }
        }
    }
}

// Finally, StateC gives us the length of the vector, or the word count.
struct StateC {
    final_value: usize,
}
impl From<StateMachine<StateB>> for StateMachine<StateC> {
    fn from(val: StateMachine<StateB>) -> StateMachine<StateC> {
        StateMachine {
            some_unrelated_value: val.some_unrelated_value,
            state: StateC {
                final_value: val.state.interm_value.len(),
            }
        }
    }
}

A Raft Example | Raft 算法示例

If you’ve followed my posts for awhile you may know I rather enjoy thinking about Raft. Raft, and a discussion with @argorak were the primary motivators behind all of this research.

如果你关注我的帖子有一段时间了, 你可能会知道我更喜欢思考 Raft 算法. Raft 算法以及与 @argorak 的讨论是所有这些研究背后的主要动机.

Raft is a bit more complex than the above examples as it does not just have linear states where A->B->C. Here is the transition diagram:

Raft 算法比上面的例子要复杂一些, 因为它不仅具有 A->B->C 的线性状态. 这是转换图:

++++++++++-+    ++++++++++--+    +++++++--+
|          ++++->           |    |        |
| Follower |    | Candidate ++++-> Leader |
|          <+++-+           |    |        |
+++++++--^-+    ++++++++++--+    +-++++++++
         |                         |
         +++++++++++++++++++++++++-+
// You can play around in this function.
fn main() {
    let is_follower = Raft::new(/* ... */);
    // Raft typically comes in groups of 3, 5, or 7. Just 1 for us. :)

    // Simulate this node timing out first.
    let is_candidate = Raft::<Candidate>::from(is_follower);

    // It wins! How unexpected.
    let is_leader = Raft::<Leader>::from(is_candidate);

    // Then it fails and rejoins later, becoming a Follower again.
    let is_follower_again = Raft::<Follower>::from(is_leader);

    // And goes up for election...
    let is_candidate_again = Raft::<Candidate>::from(is_follower_again);

    // But this time it fails!
    let is_follower_another_time = Raft::<Follower>::from(is_candidate_again);
}


// This is our state machine.
struct Raft<S> {
    // ... Shared Values
    state: S
}

// The three cluster states a Raft node can be in

// If the node is the Leader of the cluster services requests and replicates its state.
struct Leader {
    // ... Specific State Values
}

// If it is a Candidate it is attempting to become a leader due to timeout or initialization.
struct Candidate {
    // ... Specific State Values
}

// Otherwise the node is a follower and is replicating state it receives.
struct Follower {
    // ... Specific State Values
}

// Raft starts in the Follower state
impl Raft<Follower> {
    fn new(/* ... */) -> Self {
        // ...
        Raft {
            // ...
            state: Follower { /* ... */ }
        }
    }
}

// The following are the defined transitions between states.

// When a follower timeout triggers it begins to campaign
impl From<Raft<Follower>> for Raft<Candidate> {
    fn from(val: Raft<Follower>) -> Raft<Candidate> {
        // ... Logic prior to transition
        Raft {
            // ... attr: val.attr
            state: Candidate { /* ... */ }
        }
    }
}

// If it doesn't receive a majority of votes it loses and becomes a follower again.
impl From<Raft<Candidate>> for Raft<Follower> {
    fn from(val: Raft<Candidate>) -> Raft<Follower> {
        // ... Logic prior to transition
        Raft {
            // ... attr: val.attr
            state: Follower { /* ... */ }
        }
    }
}

// If it wins it becomes the leader.
impl From<Raft<Candidate>> for Raft<Leader> {
    fn from(val: Raft<Candidate>) -> Raft<Leader> {
        // ... Logic prior to transition
        Raft {
            // ... attr: val.attr
            state: Leader { /* ... */ }
        }
    }
}

// If the leader becomes disconnected it may rejoin to discover it is no longer leader
impl From<Raft<Leader>> for Raft<Follower> {
    fn from(val: Raft<Leader>) -> Raft<Follower> {
        // ... Logic prior to transition
        Raft {
            // ... attr: val.attr
            state: Follower { /* ... */ }
        }
    }
}

Alternatives From Feedback | 社区备选方案

I saw an interesting comment by I-impv on Reddit showing off this approach based on our examples above. Here’s what they had to say about it:

我在 Reddit 上看到了 I-impv 的一条有趣的评论, 它根据我们上面的示例展示了这种方法. 以下是他们对此的看法:

I like the way you did it. I am working on a fairly complex FSM myself currently and did it slightly different.

我喜欢你这样做的方式. 我自己目前正在研究一个相当复杂的 FSM, 并且做得略有不同.

Some things I did different:

我做的一些事情有所不同:

  • I also modeled the input for the state machine. That way you can model your transitions as a match over (State, Event) every invalid combination is handled by the ‘default’ pattern

    我还对状态机的输入进行了建模. 这样, 你可以将转换建模为对 (state, event) 执行匹配, 每个无效的组合都由默认模式处理.

  • Instead of using panic for invalid transitions I used a Failure state, So every invalid combination transitions to that Failure state

    我没有对无效的转换使用 panic, 而是使用了 Failure 状态, 所以每个无效的组合都会转换到那个 Failure 状态.

I really like the idea of modeling the input in the transitions!

我真的很喜欢在过渡中对输入进行建模的想法!

Closing Thoughts | 结束语

Rust lets us represent State Machines in a fairly good way. In an ideal situation we’d be able to make enums with restricted transitions between variants, but that’s not the case. Instead, we can harness the power of generics and the ownership system to create something expressive, safe, and understandable.

Rust 让我们以一种相当好的方式表示状态机. 在理想情况下, 我们能够构建枚举在变体之间执行受限转换, 但事实并非如此. 相反, 我们可以利用泛型和所有权系统的力量来创建富有表现力、安全和可理解的东西.

If you have any feedback or suggestions on this article I’d suggest checking out the footer of this page for contact details. I also hang out on Mozilla’s IRC as Hoverbear.

如果您对本文有任何反馈或建议, 我建议您查看此页面的页脚以获取联系方式. 我还以 Hoverbear 的身份在 Mozilla 的 IRC 上闲逛.

This Week in Rust (TWiR) Rust 语言周刊中文翻译计划, 第 601 期

本文翻译自 Natalie Klestrup Röijezon 的博客文章 https://natkr.com/2025-05-22-async-from-scratch-3/, 英文原文版权由原作者所有, 中文翻译版权遵照 CC BY-NC-SA 协议开放. 如原作者有异议请邮箱联系.

相关术语翻译依照 Rust 语言术语中英文对照表.

囿于译者自身水平, 译文虽已力求准确, 但仍可能词不达意, 欢迎批评指正.

2025 年 6 月 1 日晚, 于北京.

祝端午安康, 也祝孩子们六一国际儿童节快乐!

GitHub last commit

Async from scratch 3: Pinned against the wall

So, we’ve covered polling. We’ve tackled sleeping (and waking). Going back to the definition, that leaves us with one core concept left to conquer: pinning!

我们已探讨了轮询 (polling). 解决了休眠与唤醒 (sleeping and waking). 回到定义, 现在只剩下一个核心概念需要攻克: 固定 (pinning).

But before we get there, there was that one tiny other aside I’d like to go over, just so we can actually use the real trait this time. It’ll be quick, I promise.1 And then we’ll be back to what you came here for.

但在深入之前, 我想先解决一个小插曲, 以便这次能真正使用标准库的 Future 特性. 很快就好, 我保证. 1然后我们就会回到正题.

Intermission: Letting our types associate | 插曲: 关联类型

Let’s ignore poll() completely for a second, and focus on another sneaky2 change I pulled between Future and SimpleFuture:

暂时忽略 poll(), 聚焦 FutureSimpleFuture 之间的微妙差异2:

#![allow(unused)]
fn main() {
trait Future {
    type Output;
}

trait SimpleFuture<Output> {}
}

What’s the difference between these? Future::Output is an “associated type”. Associated types are very similar to trait generics, but they aren’t used to pick the right trait implementation.

区别在于 Future::Output 是一个关联类型. 关联类型与泛型类似, 但不用于选择具体的 trait 实现.

The way I tend to think of this is that if we think of our type as a kind-of-a-function, then generics would be the arguments, while its associated types would be the return value(s).

我的理解方式是: 如果将类型视为一种函数, 泛型相当于参数, 而关联类型则是返回值.

We can define our trait implementations for any combination of generics, but for a given set of base type3, each associated type must resolve to exactly one real type.

我们可以为任意泛型组合实现特性, 但对于给定的基础类型3, 每个关联类型必须唯一确定一个具体类型.

For example, this is perfectly fine:

例如:

#![allow(unused)]
fn main() {
struct MyFuture;

impl SimpleFuture<u64> for MyFuture {}
impl SimpleFuture<u32> for MyFuture {}
}

Or this blanket implementation:

或泛型实现:

#![allow(unused)]
fn main() {
struct MyFuture;

impl<T> SimpleFuture<T> for MyFuture {}
}

But this isn’t, because the implementations conflict with each other:4

但以下实现会冲突4:

#![allow(unused)]
fn main() {
struct MyFuture;

impl Future for MyFuture {
    type Output = u64;
}
impl Future for MyFuture {
    type Output = u32;
}
}
error[E0119]: conflicting implementations of trait `Future` for type `MyFuture`
  --> src/main.rs:13:1
   |
10 | impl Future for MyFuture {
   | ------------------------ first implementation here
...
13 | impl Future for MyFuture {
   | ^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `MyFuture`

For more information about this error, try `rustc --explain E0119`.
error: could not compile `cargo0OpMSm` (bin "cargo0OpMSm") due to 1 previous error

We’re also not allowed to do a blanket implementation that covers multiple types:

同样, 不允许覆盖所有类型的泛型实现:

#![allow(unused)]
fn main() {
struct MyFuture;

impl<T> Future for MyFuture {
    type Output = T;
}
}
error[E0207]: the type parameter `T` is not constrained by the impl trait, self type, or predicates
  --> src/main.rs:10:6
   |
10 | impl<T> Future for MyFuture {
   |      ^ unconstrained type parameter

For more information about this error, try `rustc --explain E0207`.
error: could not compile `cargojraklM` (bin "cargojraklM") due to 1 previous error

So… why is this useful? Well, primarily it helps type inference do a better job: if we know the type of x, then we also know the type of f.await, since it can only have one (Into)Future implementation5, which can only have one Output type.6

那么…这有什么用呢? 首先, 它能让类型推断做得更好: 如果我们知道 x 的类型, 也就知道 f.await 的类型, 因为它只能有一个 (Into)Future 实现5, 进而只能有一个 Output 类型. 6

There’s also a bit of a convenience benefit: our generic code we can refer to the associated type as T::Output, rather than having to bind a new type parameter. These mean roughly the same thing:

还有个便利之处: 在泛型代码中可以直接用 T::Output 指代关联类型, 而不用绑定新类型参数. 以下两种写法本质是等价的:

#![allow(unused)]
fn main() {
fn run_simple_future<Output, F: SimpleFuture<Output>>() -> Output {
    todo!()
}

fn run_future<F: Future>() -> F::Output {
    todo!()
}

// Though this also works
fn run_future_2<Output, F: Future<Output = Output>>() -> Output {
    todo!()
}
}

Well, now that that’s out of the way.. let’s get back on track. You came here to get pinned, and I wouldn’t want to disappoint…

好了, 言归正传. 你来看这篇文章是为了学 Pin 的, 我可不想让你失望…

But why, though? | 但为什么要用 Pin 呢?

Back in the ancient days of a-few-weeks-ago, I showed how we can translate any async fn into a state machine enum and a custom Future implementation.

几周前我曾演示过如何将任意 async fn 转换为状态机枚举和自定义 Future 实现.

Let’s try doing that for (a slightly simplified version of) the trick-or-treat example that the whole series started with:

现在让我们用贯穿本系列的 “不给糖就捣蛋” 示例 (简化版). 试试:

#![allow(unused)]
fn main() {
// No, I haven't read "Falsehoods programmers believe about addresses",
// why would you ask that?
// 不, 我没读过《程序员对地址的常见误解》, 
// 你为什么要问这个?
struct House {
    street: String,
    house_number: u16,
}
struct Candy;

// Does nothing except wait
// (This one actually doesn't even do that.. we'll get there. Use `tokio::task::yield_now`.)
// 除了等待什么都不做
// (这个甚至实际上连等待都不做... 我们会讲到. 使用 `tokio::task::yield_now`.)
async fn yield_now() {}

async fn demand_treat(house: &House) -> Result<Candy, ()> {
    for _ in 0..house.house_number {
        // Walking to the house takes time
        yield_now().await;
    }
    Ok(Candy)
}
async fn play_trick(house: &House) {
    todo!();
}

async fn trick_or_treat() {
    // Address chosen by fair dice roll. Obviously. Don't worry about it.
    let house = House {
        street: "Riksgatan".to_string(),
        house_number: 3,
    };
    if demand_treat(&house).await.is_err() {
        play_trick(&house).await;
    }
}
}

Well that’s simple enough, let’s give it a go..

看起来很简单, 让我们开始改写…

#![allow(unused)]
fn main() {
struct DemandTreat<'a> {
    house: &'a House,
}
impl SimpleFuture<Result<Candy, ()>> for DemandTreat<'_> {
    fn poll(&mut self) -> Poll<Result<Candy, ()>> { todo!() }
}
struct PlayTrick<'a> {
    house: &'a House,
}
impl SimpleFuture<()> for PlayTrick<'_> {
    fn poll(&mut self) -> Poll<()> { todo!() }
}

enum TrickOrTreat<'a> {
    Init,
    DemandTreat {
        house: House,
        demand_treat: DemandTreat<'a>,
    },
    PlayTrick {
        house: House,
        play_trick: PlayTrick<'a>,
    },
}
impl<'a> SimpleFuture<()> for TrickOrTreat<'a> {
    fn poll(&mut self) -> Poll<()> {
        loop {
            match self {
                TrickOrTreat::Init => {
                    let house = House {
                        street: "Riksgatan".to_string(),
                        house_number: 3,
                    };
                    *self = TrickOrTreat::DemandTreat {
                        house,
                        demand_treat: DemandTreat {
                            house: &house,
                        }
                    };
                }
                _ => todo!(),
            }
        }
    }
}
```rust,no_run

```plaintext
error[E0597]: `house` does not live long enough
  --> src/main.rs:76:36
   |
64 | impl<'a> SimpleFuture<()> for TrickOrTreat<'a> {
   |      -- lifetime `'a` defined here
...
69 |                     let house = House {
   |                         ----- binding `house` declared here
...
73 |                     *self = TrickOrTreat::DemandTreat {
   |                     ----- assignment requires that `house` is borrowed for `'a`
...
76 |                             house: &house,
   |                                    ^^^^^^ borrowed value does not live long enough
...
79 |                 }
   |                 - `house` dropped here while still borrowed

error[E0382]: borrow of moved value: `house`
  --> src/main.rs:76:36
   |
69 |                     let house = House {
   |                         ----- move occurs because `house` has type `House`, which does not implement the `Copy` trait
...
74 |                         house,
   |                         ----- value moved here
75 |                         demand_treat: DemandTreat {
76 |                             house: &house,
   |                                    ^^^^^^ value borrowed here after move
   |
note: if `House` implemented `Clone`, you could clone the value
  --> src/main.rs:4:1
   |
4  | struct House {
   | ^^^^^^^^^^^^ consider implementing `Clone` for this type
...
74 |                         house,
   |                         ----- you could clone this value

Some errors have detailed explanations: E0382, E0597.
For more information about an error, try `rustc --explain E0382`.
error: could not compile `cargorz58SU` (bin "cargorz58SU") due to 2 previous errors
}

..oh, right. Rust really doesn’t like structs that borrow themselves. We can’t even express this well in its type system: we can’t bind the lifetime of the DemandTreat to the lifetime of the TrickOrTreat, it has to come from an external type parameter.7. We can’t even construct TrickOrTreat::DemandTreat without the DemandTreat! What could we possibly do about this predicament?

…哦对了. Rust 确实不喜欢自引用的结构体. 我们甚至无法在类型系统中很好地表达这一点: 无法将 DemandTreat 的生命周期绑定到 TrickOrTreat 的生命周期, 必须依赖外部类型参数. 我们甚至无法在缺少 DemandTreat 的情况下构造 TrickOrTreat::DemandTreat! 这困境怎么破?

Well. We could just pass the ownership of the House into DemandTreat, and then have it return it once finished. (That is, change the signature from async fn demand_treat(house: &House) -> Result<Candy, ()> to async fn demand_treat(house: House) -> (House, Result<Candy, ()>).) That works for our simple example8, but it breaks if we’re borrowing the data ourselves, or if something else is also borrowing it at the same time as DemandTreat. Probably workable with enough elbow grease, but not great.

好吧. 我们可以直接把 House 的所有权传给 DemandTreat, 等它处理完后再返回. (也就是说, 将函数签名从 async fn demand_treat(house: &House) -> Result<Candy, ()> 改为 async fn demand_treat(house: House) -> (House, Result<Candy, ()>). ).在我们的简单示例8中可行, 但如果我们自己也在借用这些数据, 或者有其他东西和 DemandTreat 同时借用它时, 就会出问题. 虽然花点功夫或许能解决, 但终究不是个好办法.

We could try wrapping the DemandTreat in an Option.. that’d solve the construction paradox at least. But it wouldn’t do diddly to solve our lifetime problem.

我们可以尝试将 DemandTreat 包装在 Option 里… 至少能解决构造悖论. 但这对于解决生命周期问题毫无帮助.

We could try clone-ing the House.. but that assumes that it is cloneable9. We could get around that by wrapping the house in Arc-flavoured bubblewrap, but that assumes that we own it directly.9 Blech.

我们可以尝试克隆 House… 但这假设它是可克隆的10. 我们可以通过用 Arc 来绕过这个问题, 但这又假设我们直接拥有它9. 呃.

Well, that all sucks. Maybe there is something to that old “C” thing, after all. Y’know what. Clearly it’s the compiler that is wrong. How about we just use some raw pointers instead. Clearly, I can be trusted with raw pointers. Right?

唉, 这一切都糟透了. 或许那个古老的 “C” 语言确实有点道理. 你知道吗? 显然是编译器出了问题. 要不咱们干脆用裸指针算了. 显然, 我是可以信任裸指针的. 对吧?

#![allow(unused)]
fn main() {
use std::task::ready;

struct DemandTreat {
    house: *const House,
    current_house: u16,
}
impl SimpleFuture<Result<Candy, ()>> for DemandTreat {
    fn poll(&mut self) -> Poll<Result<Candy, ()>> {
        if self.current_house == unsafe { (*self.house).house_number } {
            Poll::Ready(Ok(Candy))
        } else {
            self.current_house += 1;
            Poll::Pending
        }
    }
}
struct PlayTrick {
    house: *const House,
}
impl SimpleFuture<()> for PlayTrick {
    fn poll(&mut self) -> Poll<()> { todo!() }
}

enum TrickOrTreat {
    Init,
    DemandTreat {
        house: House,
        demand_treat: DemandTreat,
    },
    PlayTrick {
        house: House,
        play_trick: PlayTrick,
    },
}
impl SimpleFuture<()> for TrickOrTreat {
    fn poll(&mut self) -> Poll<()> {
        loop {
            match self {
                TrickOrTreat::Init => {
                    *self = TrickOrTreat::DemandTreat {
                        house: House {
                            street: "Riksgatan".to_string(),
                            house_number: 3,
                        },
                        demand_treat: DemandTreat {
                            house: std::ptr::null(),
                            current_house: 0,
                        },
                    };
                    let TrickOrTreat::DemandTreat { house, demand_treat } = self else { unreachable!() };
                    demand_treat.house = house;
                }
                TrickOrTreat::DemandTreat { house, demand_treat } => {
                    match ready!(demand_treat.poll()) {
                        Ok(_) => return Poll::Ready(()),
                        Err(_) => todo!(),
                    }
                }
                _ => todo!(),
            }
        }
    }
}
}

And it works compiles! I hear that’s basically the same thing. Time to celebrate. Right?

而且它能编译通过! 我听说这基本上就是成功的意思. 是时候庆祝了, 对吧?

…right?

… 吧?

…perhaps not yet.11 As always, raw pointers come with a cost. First, we obviously lose the niceties of borrow checking. In fact, we arguably have a lifetime bug already!12 But there’s also a deeper problem in here. Pointers (and references) point at the absolute memory location. But once poll() has returned, whoever is running the future has full ownership. They’re free to move it around as they please.

……或许还不行. 11 一如既往, 裸指针是有代价的. 首先, 我们显然失去了借用检查的便利性. 事实上, 可以说我们已经存在一个生命周期错误了! 12 但这里还有一个更深层次的问题. 指针 (和引用).向的是绝对内存地址. 然而一旦 poll() 返回, 运行 future 的人就拥有了完全的所有权. 他们可以随意移动它.

#![allow(unused)]
fn main() {
let mut future = TrickOrTreat::Init;
future.poll();
// future.demand_treat.house points at future.house
// move future somewhere else
let mut future2 = future;
// future2.demand_treat.house *still* points at future.house, not future2.house!
future2.poll();
}

…oh dear.13 And you don’t even need to own it either, std::mem::swap and std::mem::replace are happy to move objects that are behind (mutable) references, as long as you have a valid object to replace them with:

…哦天哪. 13 而且你甚至不需要拥有它, std::mem::swapstd::mem::replace 很乐意为你移动位于(可变)引用后的对象, 只要你有一个有效的对象来替换它们:

#![allow(unused)]
fn main() {
let mut future = TrickOrTreat::Init;
future.poll();
let mut future2 = std::mem::replace(&mut future, TrickOrTreat::Init);
// future *is* now still a valid object, but not the one we meant to reference.
// And future.house definitely isn't valid, since we aren't on that branch of the enum.
// future *现在* 仍然是一个有效对象, 但不是我们想要引用的那个.
// 而且 future.house 绝对无效, 因为我们不在枚举的那个分支上.
future2.poll();
}

Welp. So how can we prevent ourselves from being moved, while still allowing other writes? We stick a Pin on that shit!

好吧. 那么如何在允许写入的同时防止自身被移动? 我们直接给它钉个 Pin!

Pinning, actually | 钉住!

A Pin wraps a mutable reference of some kind (&mut T, Box<T>, and so on), but restricts us (in the safe API) to reading and replacing the value entirely, without the ability to move things out of it (or to mutate only parts of them14).

Pin 包装了某种可变引用 (如 &mut TBox<T> 等). 但在安全 API 中限制我们只能整体读取或替换值, 而无法从中移出内容 (或仅修改其部分内容14).

(译者注: 显然, Pin 的对象应当是指针, Pin 一个值是没有意义的.)

It looks like this:

像这样:

#![allow(unused)]
fn main() {
use std::ops::{Deref, DerefMut};

// SAFETY: Don't access .0 directly
struct Pin<T>(T);

impl<T: Deref> Pin<T> {
    // SAFETY: `ptr` must never be moved after this function has been called
    unsafe fn new_unchecked(ptr: T) -> Self {
        Self(ptr)
    }

    fn get_ref(&self) -> &T::Target {
        &self.0
    }
}

impl<T: DerefMut> Pin<T> {
    // SAFETY: The returned reference must not be moved
    unsafe fn get_unchecked_mut(&mut self) -> &mut T::Target {
        &mut self.0
    }

    // Allow reborrowing Pin<OwnedPtr> as Pin<&mut T>
    fn as_mut(&mut self) -> Pin<&mut T::Target> {
        Pin(&mut self.0)
    }

    fn set(&mut self, value: T::Target) where T: DerefMut, T::Target: Sized {
        *self.0 = value;
    }
}

// As a convenience, `Deref` lets us call x.get_ref().y as x.y
impl<T: Deref> Deref for Pin<T> {
    type Target = T::Target;
    fn deref(&self) -> &Self::Target {
        self.get_ref()
    }
}
}

We can then create our object “normally” and then pin it (promising to uphold its requirements from that point onwards, but also gaining its self-referential powers):

我们可以先 “一如既往” 地创建对象, 然后将其固定 (承诺从此刻起遵守其要求, 同时也获得其自引用能力).

(译者注: 应当明确, Pin 是个约定, 约定遵循其要求, 而不是 Pin 本身去保证. Rust 的一个精妙之处在于此.)

#![allow(unused)]
fn main() {
struct Foo {
    bar: u64,
}
let mut foo = Foo { bar: 0 };
// Creating a pin is unsafe, because we need to promise that we won't use the original value directly anymore, even after the pin is dropped 创建 Pin 是不安全的, 需要使用者保证.
let mut foo = unsafe { Pin::new_unchecked(&mut foo) };
// Reading is safe
println!("=> {} (initial)", foo.bar);
// Replacing is safe 替换是安全的, 因为我们完整移动了值, 移动出来的值将会在离开作用域后被安全地销毁.
foo.set(Foo { bar: 1 });
println!("=> {} (replaced)", foo.bar);
// Arbitrary writing is unsafe 直接写入字段是不安全的
unsafe { foo.get_unchecked_mut().bar = 2; }
println!("=> {} (written)", foo.bar);
// We can still move if we use get_unchecked_mut(), but it's also unsafe! 通过 unsafe 方法, 还是可以获取到其可变引用的.
let old_foo = unsafe { std::mem::replace(foo.get_unchecked_mut(), Foo { bar: 3 }) };
println!("=> {} (moved)", old_foo.bar);
println!("=> {} (replacement)", foo.bar);
}
=> 0 (initial)
=> 1 (replaced)
=> 2 (written)
=> 2 (moved)
=> 3 (replacement)

Managing the self-reference itself is still as unsafe as ever, but by designing our API around to pin the state, we can make sure that whoever actually owns our state is forced to uphold our constraints. For example, for Future:

管理自引用本身依然和以往一样不安全, 但通过围绕 Pin 设计我们的 API, 可以确保实际持有我们状态的对象必须遵守约束条件. 例如, 对于 Future 而言:

#![allow(unused)]
fn main() {
// std::pin::Pin is special-cased, we can't use arbitrary types as receivers (`self`) yet in stable
// std::pin::Pin 属于特例. 目前在 stable Rust 中我们还不能将任意类型用作接收器 (`self` 类型)
use std::pin::Pin;

trait PinnedFuture<Output> {
    fn poll(self: Pin<&mut Self>) -> Poll<Output>;
}
}

There are also some APIs for pinning things safely. Boxes own their values and their targets are never moved15, so wrapping those is fine:

还有一些用于安全地 “固定” 数据的 API. Box 拥有其值, 且目标永远不会被移动15,因此这些封装是安全的:

#![allow(unused)]
fn main() {
impl<T> Box<T> {
    fn into_pin(self) -> Pin<Box<T>> {
        // SAFETY: `Box` owns its value and is never moved, so it will be dropped together with the `Pin`
        unsafe { Pin::new_unchecked(self) }
    }

    fn pin(value: T) -> Pin<Box<T>> {
        Self::new(value).into_pin()
    }
}
}

Finally, we can pin things on the stack! We don’t have a special type for “owned-place-on-the-stack”, and &mut T returns control to the owner once dropped, so that’s also not legal. Instead, we need to use the pin! macro to ensure that the original value can never be used:

终于, 我们可以在栈上固定数据了! 由于没有专门表示 “栈分配” 的类型, 而 &mut T 一旦被释放就会将控制权归还所有者, 为此, 我们需要使用 pin! 宏来确保原始值永远无法被使用(译者注: 本质是覆盖掉变量名):

#![allow(unused)]
fn main() {
struct Foo;

let foo = std::pin::pin!(Foo);

// Equivalent to:
let mut foo = Foo;
// SAFETY: `foo` is shadowed in its own scope, so it can never be accessed directly after this point
let foo = unsafe { Pin::new_unchecked(&mut foo) };
}

(std’s pin! does some special magic to allow it to be used as an expression, but the older futures::pin_mut! really did do this.)

(标准库的 pin! 通过一些特殊技巧使其可作为表达式使用, 但 futures::pin_mut! 更早地实现了这一点. )

Well, sometimes at least | 好吧, 至少有时候是这样

But if we start defining Future in terms of Pin.. won’t that add a whole bunch of (mental) overhead for the cases that don’t require pinning? Suddenly we need to worry about whether all of our Future-s are pinned correctly. That seems like a lot of work. We could provide separate UnpinnedFuture and PinnedFuture traits, but then we have to deal with defining how the two interact. Also not great.

但如果我们在定义 Future 时引入 Pin 的概念, 对于那些不需要 Pin 的情况, 岂不是平添了一堆(心智)负担? 突然间我们得操心所有 Future 是否正确使用 Pin. 这看起来工作量很大. 我们可以提供独立的 UnpinnedFuturePinnedFuture 特性, 但随之而来的是要定义两者如何交互. 这也不理想.

That’s why Rust provides the Unpin marker trait:

正因如此, Rust 提供了 Unpin 的标记用 trait:

#![allow(unused)]
fn main() {
// SAFETY: Only implement for types that can never contain references to themselves.
trait Unpin {}
}

It lets types opt out of pinning, letting you use Pin<&mut T> as if it was equivalent to &mut T as long as T is Unpin:

只要 TUnpin, 就可以像使用 &mut T 一样使用 Pin<&mut T>:

#![allow(unused)]
fn main() {
impl<T: Deref> Pin<T>
where
    T::Target: Unpin,
{
    fn new(ptr: T) -> Self {
        // SAFETY: `ptr` is unpinned
        unsafe { Self::new_unchecked(ptr) }
    }

    fn get_mut(&mut self) -> &mut T::Target where T: DerefMut {
        // SAFETY: `ptr` is unpinned
        unsafe { self.get_unchecked_mut() }
    }
}

// Convenience alias for get_mut()
impl<T: DerefMut> DerefMut for Pin<T>
where
    T::Target: Unpin,
{
    fn deref_mut(&mut self) -> &mut Self::Target {
        self.get_mut()
    }
}
}

We can then create and mutate pins as we please.. as long as we stick to Unpin-ned data:

只要类型是 Unpin 的, 我们就能随心所欲地创建或修改位于 Pin 背后的数据…

#![allow(unused)]
fn main() {
struct Foo {
    bar: u64,
}
impl Unpin for Foo {}

let mut foo = Foo { bar: 0 };
Pin::new(&mut foo).bar = 1;
foo.bar = 2;
}

And as a final nod to convenience.. Rust actually implements Unpin by default for new types, as long as they only contain values that are also Unpin. Since that’s going to exclude types that do contain self-references (*mut T is Unpin by itself), Rust provides the PhantomPinned type which does nothing except be !Unpin.16 For example:

最后, 为了进一步体现便利性, Rust 默认会为仅包含同样实现了 Unpin 的值的自定义类型自动实现 Unpin. 由于这仅会排除那些确实包含自引用 (*mut T 本身是 Unpin 的) 类型, Rust 提供了 PhantomPinned 类型(以手动)将自定义类型标记为 !Unpin.16 例如:

#![allow(unused)]
fn main() {
use std::marker::PhantomPinned;

struct ImplicitlyUnpin;
struct ExplicitlyNotUnpin(ImplicitlyUnpin, PhantomPinned);
struct ImplicitlyNotUnpin(ExplicitlyNotUnpin);
struct ExplicitlyUnpin(ImplicitlyNotUnpin);

impl Unpin for ExplicitlyUnpin {}

fn assert_unpin<T: Unpin>() {}
assert_unpin::<ImplicitlyUnpin>;
assert_unpin::<ExplicitlyUnpin>;
// Will fail, since these aren't unpinnable
assert_unpin::<ExplicitlyNotUnpin>;
assert_unpin::<ImplicitlyNotUnpin>;
}
error[E0277]: `PhantomPinned` cannot be unpinned
  --> src/main.rs:16:16
   |
16 | assert_unpin::<ExplicitlyNotUnpin>;
   |                ^^^^^^^^^^^^^^^^^^ within `ExplicitlyNotUnpin`, the trait `Unpin` is not implemented for `PhantomPinned`
   |
   = note: consider using the `pin!` macro
           consider using `Box::pin` if you need to access the pinned value outside of the current scope
note: required because it appears within the type `ExplicitlyNotUnpin`
  --> src/main.rs:6:8
   |
6  | struct ExplicitlyNotUnpin(ImplicitlyUnpin, PhantomPinned);
   |        ^^^^^^^^^^^^^^^^^^
note: required by a bound in `assert_unpin`
  --> src/main.rs:12:20
   |
12 | fn assert_unpin<T: Unpin>() {}
   |                    ^^^^^ required by this bound in `assert_unpin`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `cargomxhoMm` (bin "cargomxhoMm") due to 1 previous error

A little party projection never killed nobody… | 一点小小的派对投射从不会伤害任何人…

Let’s say we have a pinned Future like this:

假设我们有一个被固定的 Future 如下:

#![allow(unused)]
fn main() {
struct Timeout<F> {
    inner_future: F,
    elapsed_ticks: u64,
}

let timeout: Pin<&mut Timeout<InnerFuture>>;
}

The safe API on Pin only lets us replace our whole Timeout (via Pin::set), but that’s not super useful for us. We need to keep our old InnerFuture, that’s why we’re pinning it to begin with!

Pin 的安全API仅允许我们替换整个 Timeout (通过Pin::set). 但这对我们来说并不十分有用. 我们需要保留旧的 InnerFuture, 这正是我们最初 “固定” 它的原因!

To address this, we need to project our InnerFuture, temporarily splitting our struct into its individual fields while maintaining the pinning requirements.

为了解决这个问题, 我们需要对 InnerFuture 进行投射, 暂时将结构体拆分为其各个字段, 同时保持 “固定” 的要求.

But that raises another question; should .inner_future give a &mut InnerFuture or a Pin<&mut InnerFuture>? What about .elapsed_ticks? The short answer is.. we decide.

但这又引出了另一个问题: .inner_future 应该返回 &mut InnerFuture 还是 Pin<&mut InnerFuture>? .elapsed_ticks呢? 简短的答案是… 由我们决定.

From Rust’s perspective, either answer is valid as long as we obey the cardinal Pin rule that we cannot provide a regular &mut once we have produced a Pin for a given field.17

从 Rust 的角度来看, 只要遵守 Pin 的基本规则——一旦为某个字段生成了 Pin, 就不能再提供常规的 &mut 引用——两种答案都是有效的.17

From our perspective, we probably want inner_future to be Pin (since it’s also a Future), but elapsed_ticks doesn’t have any reason to be.

从我们的角度来看, 可能希望 inner_futurePin 的 (因为它也是一个 Future). 但 elapsed_ticks 没有理由需要是.

Hence, we should write down a single way to project access into each field. One way18 would be to write a method for each field:

因此, 我们应该为每个字段的访问投射确定一种统一的方式. 一种方式18是为每个字段编写一个方法:

#![allow(unused)]
fn main() {
impl<F> Timeout<F> {
    fn inner_future(self: Pin<&mut Self>) -> Pin<&mut F> {
        // SAFETY: `inner_future` is pinned structurally
        unsafe {
            Pin::new_unchecked(&mut self.get_unchecked_mut().inner_future)
        }
    }

    fn elapsed_ticks(self: Pin<&mut Self>) -> &mut u64 {
        // SAFETY: `elapsed_ticks` is _not_ pinned structurally
        unsafe {
            &mut self.get_unchecked_mut().elapsed_ticks
        }
    }
}
}

However, this doesn’t allow us to access multiple fields concurrently, since Rust doesn’t have a way to express “split borrows” in function signatures at the moment:

然而, 这不允许我们同时访问多个字段, 因为 Rust 目前无法在函数签名中做到 “分别借用”:

#![allow(unused)]
fn main() {
let mut timeout: Pin<&mut Timeout<InnerFuture>> = std::pin::pin!(Timeout { inner_future: InnerFuture, elapsed_ticks: 0 });
let inner_future = timeout.as_mut().inner_future();
let elapsed_ticks = timeout.as_mut().elapsed_ticks();
inner_future.poll();
*elapsed_ticks += 1;
}
error[E0499]: cannot borrow `timeout` as mutable more than once at a time
  --> src/main.rs:38:21
   |
37 | let inner_future = timeout.as_mut().inner_future();
   |                    ------- first mutable borrow occurs here
38 | let elapsed_ticks = timeout.as_mut().elapsed_ticks();
   |                     ^^^^^^^ second mutable borrow occurs here
39 | inner_future.poll();
   | ------------ first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `cargokXjM9v` (bin "cargokXjM9v") due to 1 previous error

Instead, we can build a single projection struct that projects access to all fields simultaneously:

相反, 我们可以构建一个单一的投射结构体, 同时实现对所有字段的访问投射:

#![allow(unused)]
fn main() {
struct TimeoutProjection<'a, F> {
    inner_future: Pin<&'a mut F>,
    elapsed_ticks: &'a mut u64,
}

impl<F> Timeout<F> {
    fn project(mut self: Pin<&mut Self>) -> TimeoutProjection<F> {
        // SAFETY: This function defines the canonical projection for each field
        unsafe {
            let this = self.get_unchecked_mut();
            TimeoutProjection {
                // SAFETY: `inner_future` is pinned structurally
                inner_future: Pin::new_unchecked(&mut this.inner_future),
                // SAFETY: `elapsed_ticks` is _not_ pinned structurally
                elapsed_ticks: &mut this.elapsed_ticks,
            }
        }
    }
}
}

And use it like so:

这样去使用:

#![allow(unused)]
fn main() {
let mut timeout: Pin<&mut Timeout<InnerFuture>> = std::pin::pin!(Timeout { inner_future: InnerFuture, elapsed_ticks: 0 });
let projection = timeout.project();
let inner_future = projection.inner_future;
let elapsed_ticks = projection.elapsed_ticks;
inner_future.poll();
*elapsed_ticks += 1;
}

Woohoo! This is fine, since we have one method that borrows all of Timeout, and produces one TimeoutProjection that is equivalent to it. It’s okay for the TimeoutProjection to borrow multiple things from the Timeout, as long as we (project()) know that those borrows are disjoint.19

太棒了! 这没问题, 因为我们有一个方法会借用整个 Timeout, 并生成一个与之等效的 TimeoutProjection. 只要 project() 方法能确保这些借用是互不重叠的, TimeoutProjection 从Timeout中借用多个字段也是完全可以的19.

But that’s still a bit tedious, having to effectively write each struct thrice20. Conveniently enough, There’s A Crate For That21. Our TimeoutProjection struct could be generated by pin-project like this:

不过这样还是有点繁琐, 相当于每个结构体要写三遍20. 幸运的是, 有个现成的库可以解决21. 我们的 TimeoutProjection 结构体可以通过 pin-project 这样生成:

#![allow(unused)]
fn main() {
// Generates `TimeoutProjection` and `Timeout::project()` as above
#[pin_project::pin_project(project = TimeoutProjection)]
struct Timeout<F> {
    #[pin] // Projected as `Pin<&mut F>`
    inner_future: F,
    // No `#[pin]`, projected as `&mut F`
    elapsed_ticks: u64,
}

let mut timeout: Pin<&mut Timeout<InnerFuture>> = std::pin::pin!(Timeout { inner_future: InnerFuture, elapsed_ticks: 0 });
let projection = timeout.project();
let inner_future = projection.inner_future;
let elapsed_ticks = projection.elapsed_ticks;
inner_future.poll();
*elapsed_ticks += 1;
}

Whew. We still have to call Project22, but at least we’re mostly back in familiar Rust territory again!

呼. 我们还得调用 Project22, 但至少大部分又回到了熟悉的 Rust 领域!

And finally, the same transformation works for enums as well:

最后, 同样的转换对枚举也适用:

#![allow(unused)]
fn main() {
#[pin_project::pin_project(project = TimeoutProjection)]
enum Timeout<F> {
    Working {
        #[pin]
        inner_future: F,
        elapsed_ticks: u64,
    },
    Expired,
}

// #[pin_project] is equivalent to:
enum ManualTimeoutProjection<'a, F> {
    Working {
        inner_future: Pin<&'a mut F>,
        elapsed_ticks: &'a mut u64,
    },
    Expired,
}
impl<F> Timeout<F> {
    fn manual_project(mut self: Pin<&mut Self>) -> ManualTimeoutProjection<F> {
        // SAFETY: This function defines the canonical projection for each field
        unsafe {
            match self.get_unchecked_mut() {
                Timeout::Working {
                    inner_future,
                    elapsed_ticks,
                } => ManualTimeoutProjection::Working {
                    // SAFETY: `inner_future` is pinned structurally
                    inner_future: Pin::new_unchecked(inner_future),
                    // SAFETY: `elapsed_ticks` is _not_ pinned structurally
                    elapsed_ticks,
                },
                Timeout::Expired => ManualTimeoutProjection::Expired,
            }
        }
    }
}
}

There is, however, one caveat to using pin-project: While Rust normally avoids implementing Unpin if any field is !Unpin, pin-project only considers #[pin]-ned fields. Normally this is enforced by the type system anyway (since you can’t call self: Pin methods otherwise), but if you use PhantomPinned then it must always be #[pin]-ned to be effective.

需要指出, 使用 pin-project 时有一个注意事项: 虽然 Rust 通常会在任何字段为 !Unpin 时避免实现 Unpin, 但 pin-project 仅考虑被 #[pin] 标记的字段. 通常情况下, 类型系统本身会强制执行这一点 (因为否则无法调用self: Pin方法). 但如果使用 PhantomPinned, 则必须始终用 #[pin] 标记之才能生效.

Onwards, to the beginning! | 走上正轨!

Okay, now we should finally have the tools to make TrickOrTreat safe to interact with!

好的, 现在我们终于有了与 TrickOrTreat 安全交互的工具了!

#![allow(unused)]
fn main() {
use std::{marker::PhantomPinned, task::ready};

struct DemandTreat {
    house: *const House,
    current_house: u16,
}
impl PinnedFuture<Result<Candy, ()>> for DemandTreat {
    fn poll(mut self: Pin<&mut Self>) -> Poll<Result<Candy, ()>> {
        if self.current_house == unsafe { (*self.house).house_number } {
            Poll::Ready(Ok(Candy))
        } else {
            self.current_house += 1;
            Poll::Pending
        }
    }
}
struct PlayTrick {
    house: *const House,
}
impl PinnedFuture<()> for PlayTrick {
    fn poll(self: Pin<&mut Self>) -> Poll<()> { todo!() }
}

#[pin_project::pin_project(project = TrickOrTreatProjection)]
enum TrickOrTreat {
    Init,
    DemandTreat {
        house: House,
        #[pin]
        demand_treat: DemandTreat,
        // SAFETY: self must be !Unpin because demand_treat references house
        #[pin]
        _pin: PhantomPinned,
    },
    PlayTrick {
        house: House,
        #[pin]
        play_trick: PlayTrick,
        // SAFETY: self must be !Unpin because play_trick references house
        #[pin]
        _pin: PhantomPinned,
    },
}
impl PinnedFuture<()> for TrickOrTreat {
    fn poll(mut self: Pin<&mut Self>) -> Poll<()> {
        loop {
            match self.as_mut().project() {
                TrickOrTreatProjection::Init => {
                    self.set(TrickOrTreat::DemandTreat {
                        house: House {
                            street: "Riksgatan".to_string(),
                            house_number: 3,
                        },
                        demand_treat: DemandTreat {
                            house: std::ptr::null(),
                            current_house: 0,
                        },
                        _pin: PhantomPinned,
                    });
                    let TrickOrTreatProjection::DemandTreat { house, mut demand_treat, .. } = self.as_mut().project() else { unreachable!() };
                    demand_treat.house = house;
                }
                TrickOrTreatProjection::DemandTreat { house, demand_treat, .. } => {
                    match ready!(demand_treat.poll()) {
                        Ok(_) => return Poll::Ready(()),
                        Err(_) => {
                            // We need to move the old house out of `self` before we replace it
                            let house = std::mem::replace(house, House {
                                street: String::new(),
                                house_number: 0,
                            });
                            self.set(TrickOrTreat::PlayTrick {
                                house,
                                play_trick: PlayTrick {
                                    // We still don't have the address of house-within-TrickOrTreat::PlayTrick
                                    house: std::ptr::null(),
                                },
                                _pin: PhantomPinned,
                            });
                            let TrickOrTreatProjection::PlayTrick { house, mut play_trick, .. } = self.as_mut().project() else { unreachable!() };
                            play_trick.house = house;
                        },
                    }
                }
                TrickOrTreatProjection::PlayTrick { play_trick, .. } => {
                    ready!(play_trick.poll());
                    return Poll::Ready(());
                },
            }
        }
    }
}
}

Caveats | 注意事项

I’m honestly still not sure about the best way to represent the self-reference “properly” in the type system.

老实说, 我仍不确定如何在类型系统中 “正确” 表达自引用.

TrickOrTreat is safe and self-contained (as long as you only construct ::Init), but DemandTreat and PlayTrick are not, since they contain unmanaged raw pointers that could end up dangling. We could use references instead, but I’m honestly not sure about whether &mut references could end up causing undefined behaviour due to aliasing. The series is ultimately not about showing what to do, but about explaining some of the magic that is usually hidden from view.

TrickOrTreat 可以安全地包含自身 (只要您仅构造 ::Init). 但 DemandTreatPlayTrick 则不然, 因为它们包含可能最终悬垂的未管理原始指针. 我们可以改用引用, 但我确实不确定 &mut 引用是否会因别名问题导致未定义行为. 这个系列最终不是为了展示该怎么做, 而是为了揭示那些通常隐藏在表象之下的魔法.

Going forward | 向前迈进

Well.. that took a bit longer than I had meant for it to. But now we’re finally through the basic building blocks of an async fn!

呃… 这比我预计的时间要长了些. 不过现在我们总算把异步函数的基本构件都过了一遍!

But an async fn alone isn’t all that useful, so next up I’d like to go over how we can use those primitives to run multiple Future-s simultaneously in the same thread! Isn’t that was asynchronicity was supposed to be all about, anyway?

但光有异步函数本身并不太实用, 所以接下来我想讲讲如何利用这些基础元素, 在同一个线程里同时运行多个Future! 这不正是异步本该实现的核心目标吗?


  1. If you already know what associated types are.. feel free to skip ahead. This chapter will still be here if you change your mind. 如果你知道关联类型是什么了, 跳过这段也没关系. ↩2

  2. Hopefully… 希望… ↩2

  3. And generics. 和泛型. ↩2

  4. They are both just registered as MyFuture: Future. 它们都为 MyFuture 实现了 Future. ↩2

  5. x could have a generic type, but all values. x 可以是泛型, 任意的值. ↩2

  6. No “multiple impls satisfying _: From<i32> found” errors here! 并没有 “有多个满足 _: From<i32> 的实现” 的错误. ↩2

  7. Or be 'static, which is even less helpful for us. 也可以是 'static 的, 但于事无补.

  8. In fact, it’s largely how Tokio 0.1.x worked back in the day. 实际上这就是之前 Tokio 0.1.x 版本的做法. ↩2

  9. It also requires us to pay the usual costs of reference counting and stack allocation. 这引入了引用计数和堆分配的性能损耗. (译者注: Arc 是堆分配.) ↩2

  10. And that it would be relatively cheap to do so. 这会很便宜 (操作廉价).

  11. Sorry. 抱歉. ↩2

  12. demand_treat is dropped after house, so if DemandTreat implements Drop then demand_treat.house will be pointing at an object that has already been dropped. demand_traithouse 后被释放, 如果 DemandTreat 实现 Drop, 则 demand_treat.house 将指向内容已被释放的对象. ↩2

  13. This might not actually crash, if the compiler is able to optimize the no-op move away! But semantically, it’s still nonsense. 可能不会崩溃, 如果编译器会优化掉无用的 move 操作. 但语义上是无意义的. ↩2

  14. Ignore the suspicious DerefMut implementation for now. We’ll get there eventually. 先忽略这可疑的 DerefMut 实现. 我们会讲到它的. ↩2

  15. Even if we move the Box itself, it still points at the same heap allocation in the same location. 即便移动了 Box, 但不影响其仍然指向相同的堆内存区域. ↩2

  16. A bit like how PhantomData<T> lets you take on the consequences of storing a type without actually storing anything. 有点像 PhantomData<T> 让你能 “存储” 类型而不实际存储任何东西. ↩2

  17. Unless the field is Unpin, of course. 当然, 除非字段 Unpin. ↩2

  18. Arguably, the obvious one. 啊, 最明显的一个. ↩2

  19. That we don’t borrow the same field twice. 由此不会二次借用同一个字段. ↩2

  20. The struct itself, the projection mirror, and the.. projection function itself. 结构体本身, 投射本身, 以及投射方法本身. ↩2

  21. At the time of writing, pin-project 1.1.10. 在撰文时是 pin-project 1.1.10. ↩2

  22. And keep track of whether utility functions are defined for &mut Timeout, Pin<&mut Timeout>, or TimeoutProjection. 注意方法是定义给 &mut Timeout, Pin<&mut Timeout>, 还是 TimeoutProjection 的. ↩2