Reading

Great Ideas in Rust


程序读入一行 alice,42,把它解析成名字和分数,再交给后台任务处理。这几步很简单,但数据在其中经历了几次交接:解析出的名字可以直接引用原来的文本吗?读取下一行时,输入缓冲区能否复用?如果后台任务还没结束,谁负责保留它需要的数据?

Rust 把这些关系放进了类型和函数接口。一个函数可以临时借用文本,也可以接管它;解析结果可以依赖输入,也可以拥有独立的存储。这些选择决定了数据能够怎样流动,编译器则检查每一次使用是否遵守约定。

Ownership:先决定谁负责这份数据

从赋值到责任转移

假设程序读到一行文本,准备把它放进待处理任务。Python 或 Java 中,两次赋值通常意味着两个名字引用同一个对象;C++ 的 std::string 赋值通常产生独立副本,也可以显式 move。Rust 需要让“谁最终释放字符串的缓冲区”保持明确,于是对 String 这样的类型,按值赋值默认转移 ownership:

rust
let line = String::from("alice,42");
let queued = line;
println!("{queued}");

这里没有复制一份字符串内容。queued 接过这份值,line 不再可用于访问它;在后面追加 println!("{line}") 会被拒绝。The Rust Book 的 ownership 章节 (The Rust Project Developers, n.d.) 用这个规则解释了为什么两个局部变量不会分别释放同一份字符串存储。

move 是所有权变化,不应理解成“一定搬动堆上的字节”。它可能涉及值表示的移动,也可能被优化掉。Rust 也没有一个普遍适用的“moved-from 对象仍然有效,只是内容未指定”的使用方式;被移出的值必须先重新初始化,才能再次使用。

整数这样的 Copy 类型例外:按值赋值会复制,原值仍可使用。Copy 允许隐式的按位复制,不等于“一定只占一个机器字”,大数组也可能是 Copy。需要主动复制时,可以调用 clone(),但其含义由类型决定:String::clone() 复制文本,而后面会遇到的 Arc::clone() 只增加一个共享所有权句柄。审查代码时,不能把所有 clone() 都当成深拷贝。

Ownership 管的不只是内存

如果任务拥有一个文件,它也应负责文件句柄的释放;如果某个局部值代表已经取得的锁,这个值结束时应释放锁。与 C++ 的 RAII 一样,Rust 用 Drop (The Rust Project Developers, n.d.a) 将清理绑定到值的销毁:正常离开作用域、提前返回时,相关资源会按规则清理。

这里仍有工程边界。进程 abort、主动泄漏或引用计数环,都可能绕过你期望的清理;析构也不是适合报告所有失败的接口。数据库提交、需要检查结果的写入或关闭,应有显式返回 Result 的操作,不能仅凭“最后会 drop”就宣称成功。

因此,设计数据结构时可以先画出所有权关系:谁拥有输入缓冲区,谁拥有解析结果,谁拥有文件和任务。按值传参通常意味着把这份责任交给函数,而不是默认给函数一个共享别名。

Borrowing:临时使用,不必接管

如果只想计算这一行有多少字节,函数没有理由夺走字符串。让它临时访问数据,并限制这次访问的权限,就得到了 borrowing。

接口形状调用方交出了什么适合表达什么
fn inspect(text: &str)文本的共享借用临时查看字符串内容
fn normalize(text: &mut String)字符串的独占借用临时修改,结束后仍归调用方
fn enqueue(text: String)字符串的所有权接收方可以保存、转交或销毁它

String 拥有可增长的 UTF-8 缓冲区;&str 是对一段 UTF-8 文本的借用视图,不拥有内容。读取函数通常接受 &str,这样既能接收整个 String 的视图,也能接收其中一段或字符串字面量。类似地,Vec<T> 拥有元素,而 &[T] 借用一段连续元素。owned value 负责存储的寿命,borrowed view 提供对其中数据的临时访问。

为什么读的时候不能随便写

现在从容器中取第一条记录,然后继续添加记录:

rust
let mut lines = vec![String::from("alice,42")];
let first = &lines[0];
lines.push(String::from("bob,17"));
println!("{first}");

这段代码预期不能编译。push 可能使 Vec 重新分配存储,把里面的 String 值移到新位置,先前借到的元素引用就可能失效。即使你知道这次容量足够,push 的接口仍要求独占借用整个 Vec;编译器根据这个接口检查,不会为这一处使用另立容量证明。

Rust 的基础借用规则 (The Rust Project Developers, n.d.b) 是:对同一片有重叠的数据,在相关借用有效期间,可以有多个共享访问,或者一个独占访问。&T 与 &mut T 最好先读成“共享”与“独占”;后面还会看到,部分类型可以在共享借用下通过受控接口修改内部状态。

如果需求只是先打印再添加记录,把 println! 移到 push 之前即可。没有后续使用时,这个借用可以在最后一次使用后结束,不必等到右花括号。如果需求确实是保存一份旧记录再修改容器,复制它也合理,但那是主动选择快照语义。

这里的约束也适用于单线程程序。问题首先是别名能否在访问期间保持有效,并不只是在防止两个线程同时写。

编译器需要能看见的理由

Rust 可以拒绝实际上安全、但当前代码没有表达出充分理由的操作。例如,想同时修改数组里两个不同位置,程序员知道索引不相等,不代表两个普通索引表达式会自动获得两个独占借用。split_at_mut (The Rust Project Developers, n.d.c) 这样的安全 API 把切片分成不重叠的两段,让这个理由体现在返回类型和接口保证中。

遇到类似错误,先问“能不能把独立的数据拆开,或者缩短借用的范围”。把整个对象包装成共享可变状态,通常会引入比原问题更多的运行时约束。

Lifetime:把依赖关系写进接口

解析 alice,42 时,名字已经在输入缓冲区里。如果只临时查看记录,再分配一个字符串似乎没有必要:可以让结果中的名字直接借用输入的一部分。代价是,这个解析结果不能脱离输入独立存在。

rust
struct Record<'a> {
    name: &'a str,
    score: u32,
}

fn parse_record(line: &str) -> Option<Record<'_>> {
    let (name, score) = line.split_once(',')?;
    Some(Record {
        name,
        score: score.parse().ok()?,
    })
}

Option 表示可能没有结果,? 让失败提前返回。这里的 'a 表达 Record 中含有一个借用,Record<'_> 让编译器按这个函数的输入推断关联。返回值中的 name 指向 line 提供的文本,因此使用它时,原文本必须仍然有效,而且不能发生与该借用冲突的修改。

The Rust Book 的 lifetime 章节 (The Rust Project Developers, n.d.c) 强调,标注描述引用之间的关系,不会改变对象实际活多久。给这里增加 'a,不能让函数内部创建的局部 String 活过函数返回。

借用结果很适合“解析后立即统计”。如果要把记录存入长期缓存,或交给生命周期独立的后台任务,就应该重新审视表示:让记录拥有 String,或让后台任务拥有完整输入并在任务内部解析。零拷贝减少分配,却把数据之间的寿命绑定在一起;它是一种取舍,不是所有接口的目标。

尤其不要为了“一个对象把所有东西都装起来”,轻易设计同时拥有 String、又让字段引用该字符串内部的结构。这会涉及自引用和移动后的有效性问题。很多情况下,保存偏移范围、在访问时生成切片,或者让结果直接拥有小块文本,都更容易维护。

'static 为什么经常出现在后台任务里

&'static str 表示引用指向的文本可在程序运行期间保持有效,字符串字面量是常见例子。T: 'static 则是对类型中借用的限制:它不能依赖比 'static 更短的外部借用。一个拥有自己内容的普通 String 就能满足这个约束,仍然可以在任务结束时立即销毁。

后台任务可能比创建它的函数活得久,所以 API 经常要求传入的值满足 T: 'static。Tokio 的 spawning 文档 (Tokio Contributors, n.d.) 专门解释了这个区别。遇到这类错误,通常要检查谁拥有任务的数据,而不是把借用标成 'static,更不应该默认通过泄漏内存来解决。

字符串中的字节与字符

Rust 的 String 和 str 保证 UTF-8 有效性 (The Rust Project Developers, n.d.c)。len() 返回字节数,不能写 text[0] 来取得“第一个字符”。按范围切片需要落在 UTF-8 边界上,否则会 panic。

按字节协议解析时,可以看 as_bytes();按 Unicode scalar value 遍历时,可以用 chars()。用户眼里的一个字符还可能包含多个 scalar value,例如组合字符和部分 emoji,不能默认用 chars().count() 就得到显示字符数。

这也说明为什么 API 的类型很重要:&[u8] 承诺的是字节,&str 额外承诺这些字节是有效 UTF-8。

类型建模:让调用方知道可能发生什么

刚才的解析函数把所有失败都变成了 None。它适合只关心“这行能不能解析”的小工具,但一旦需要告诉用户失败原因,信息就不够了。缺少分隔符、名字为空、分数格式不对,需要分别表示:

rust
#[derive(Debug, PartialEq)]
enum ParseError {
    MissingSeparator,
    EmptyName,
    InvalidScore,
}

#[derive(Debug, PartialEq)]
struct OwnedRecord {
    name: String,
    score: u32,
}

fn parse_owned(line: &str) -> Result<OwnedRecord, ParseError> {
    let (name, score) = line
        .split_once(',')
        .ok_or(ParseError::MissingSeparator)?;
    if name.is_empty() {
        return Err(ParseError::EmptyName);
    }
    let score = score.parse::<u32>()
        .map_err(|_| ParseError::InvalidScore)?;
    Ok(OwnedRecord { name: name.to_owned(), score })
}

这里名字的复制是有意的:结果需要独立保存。三种解析失败则分别进入错误类型,调用方可以决定跳过、报告或累计错误。这份示例的输入约定是一个非空名字、一个逗号和一个可解析的 u32;它不是支持引号与转义的完整 CSV parser。

Result<T, E> (The Rust Project Developers, n.d.b) 有 Ok(T) 和 Err(E) 两种情况。这里的 ? 在成功时取出值,在失败时提前返回错误,并按需要使用错误转换。它没有异常那种隐式跨层展开的控制流,但也没有处理掉错误:恢复、补偿和日志仍需在合适的边界决定。

Enum 可以携带数据

假如任务有排队、完成、失败三种状态,用一个状态字符串加两个可空字段,很容易造出“排队中却有结果”“成功却没有结果”的组合。让每个状态只携带自己需要的数据,就能消除这些组合:

rust
enum JobState {
    Queued,
    Finished { accepted: usize },
    Failed { reason: String },
}

fn describe(state: &JobState) -> String {
    match state {
        JobState::Queued => String::from("queued"),
        JobState::Finished { accepted } => format!("accepted: {accepted}"),
        JobState::Failed { reason } => format!("failed: {reason}"),
    }
}

Rust 的 enum (The Rust Project Developers, n.d.a) 是“若干种形状之一”,不只是给整数命名。match 要覆盖所有可能分支;像这里一样不写兜底分支,将来增加状态时,编译器就会提醒描述逻辑也需要更新。Option<T> 和 Result<T, E> 是同一个设计思想的日常应用。

类型也可以区分 UserId 与 JobId,即使底层都是整数;还可以让验证函数把普通输入转换成字段私有的 ValidatedRecord,让后续步骤只接收已验证值。关键是保持构造入口受控。若任何地方都能随意创建“已验证”对象,这个名字本身并不提供保证。

上述 JobState 只限制单个状态的形状,并没有证明状态迁移是否合法。如果还要保证任务不能执行两次,可以让执行方法接收 self,消耗待执行值并返回结果,或进一步用不同类型表示执行前后。先根据实际错误风险决定约束强度,不必把每个业务流程都写成复杂的类型系统练习。

Panic 是另一条路径

对失败的 Result 调用 unwrap() 或 expect() 会 panic。它们适合某些测试、样例,或确有不变量支持的内部断言;不适合把用户输入错误变成服务崩溃。尤其不要用 unwrap_or_default() 让一次读取失败看起来像“读取成功但没有记录”,除非业务确实把这两种情况视为相同。

panic 也不意味着“整个进程必然马上退出”:具体行为受 unwind/abort 配置,以及线程或任务的边界影响。工程上仍应把预期失败放进显式结果里,而不是把 panic 当作常规恢复协议。

Trait:描述能力,同时选择抽象成本

现在解析结果可以输出到文件,也可以发送给另一种接收方。两者都提供写入能力,可以用 trait 描述这个共同的行为。它与 Java 的接口相似,不要求实现者共享字段或继承同一个基类:

rust
trait Sink {
    fn write(&mut self, line: &str) -> std::io::Result<()>;
}

fn emit<S: Sink>(sink: &mut S, line: &str) -> std::io::Result<()> {
    sink.write(line)
}

fn emit_dyn(sink: &mut dyn Sink, line: &str) -> std::io::Result<()> {
    sink.write(line)
}

两个版本都表达“借用接收方并允许它改变内部写入状态”,区别在于如何找到实现。泛型版本在编译时知道具体类型,通常通过 monomorphization 生成相应代码,有利于内联,但可能增加编译工作与代码体积。dyn Sink 使用运行时分发,让调用点不必知道具体类型;&dyn Trait 本身不要求一次堆分配,Box<dyn Trait> 才进一步选择了拥有堆上对象的表示。The Rust Book 的 trait object 章节 (The Rust Project Developers, n.d.f) 讨论了这两类分发方式。

因此,看到 impl Trait、泛型参数和 dyn Trait 时,应关注实现类型是在编译时确定,还是需要运行时异构。返回位置的 impl Trait 隐藏一个确定的具体类型,不是允许任意返回几种不同类型;也不是每个 trait 都满足作为 trait object 使用的条件。

Rust 常说的 zero-cost abstraction,应理解为抽象有机会编译成直接实现相同工作所需的代码,而不是每个泛型、迭代器或 trait 调用都免费。一次 collect::<Vec<_>>() 仍然要构造容器,锁仍有同步代价,引用计数仍需要更新。

还要留意 coherence 和 orphan rules (The Rust Project Developers, n.d.c):不能随意为外部类型实现外部 trait。一个常见做法是定义自己的 newtype 包装外部类型,然后在自己的类型上实现所需行为。这既让实现归属清楚,也避免不同依赖对同一类型提供互相冲突的解释。

迭代器与闭包:同一套所有权规则的延伸

对一个 Vec<T>,iter() 产生共享借用的元素,iter_mut() 产生独占借用的元素,而对拥有的 Vec<T> 调用 into_iter() 会消耗容器并逐个交出元素所有权。AI 把其中一种改成另一种时,可能已经改变后续代码能否使用原容器。

闭包也会根据使用方式借用或取得捕获值。move 让闭包按值捕获所需变量;如果捕获的变量本来就是引用,得到的仍是引用,不会自动拥有引用背后的数据。

Fn、FnMut、FnOnce (The Rust Project Developers, n.d.a) 分别允许通过共享访问、独占访问、消耗闭包来调用。它们不是三个互斥类别:实现 Fn 的闭包也实现 FnMut 和 FnOnce。接受 FnOnce 的 API 只要求调用一次,不表示传入的每个闭包天生只能调用一次;是否需要消耗捕获值,要看闭包体。

共享与并发:让权限跨过线程边界

前面的数据都有一个明确的 owner。现在两个任务需要读取同一份配置,把它完整复制两份未必合适。于是可以引入共享 ownership:Rc<T> 用于单线程引用计数,Arc<T> 使用原子引用计数,适合满足相应约束的跨线程共享。

每个 Arc 句柄本身仍是一个有 owner 的值,多个句柄共同保持内部值存活。根据 Arc 文档 (The Rust Project Developers, n.d.a),克隆句柄不会复制内部配置;最后一个强引用被释放时,内部值被销毁。强引用环不会自动回收,必要时用 Weak 表达不负责保活的关系。

但“可以共享存活”还没有回答“如何修改”。如果共享的是统计计数,仍需要选择同步协议:用 atomic 更新独立计数,用 Mutex 保护多个字段的一致性,或把状态交给一个任务独占、其他任务发消息请求修改。

把所有权、借用检查与同步分开

类型主要解决的问题它没有自动解决什么
Box<T>独占拥有堆上的值共享与同步
Rc<T>单线程共享 ownership并发访问与可变借用
Arc<T>使用原子计数共享 ownership内部数据的任意并发修改
RefCell<T>将共享场景中的借用检查放到运行时跨线程同步;冲突借用仍会失败
Mutex<T>用锁串行化对内部数据的访问死锁、锁粒度和业务原子性

RefCell、Mutex 等类型提供 interior mutability:即使通过共享借用,也可以在满足其协议的前提下改变内部值。标准库的 cell 文档 (The Rust Project Developers, n.d.h) 将这与普通 &mut T 的修改方式区分开来。RefCell::borrow_mut() 遇到冲突会 panic,try_borrow_mut() 则允许显式处理失败;它没有把冲突变成安全的同时访问。

所以 Arc<Mutex<T>> 可以是合理设计,但它同时引入了共享寿命和互斥访问两套机制。若只是为了修复局部变量借用错误而层层包装,需要重新问:这里真的有多个长期 owner 吗?真的有并发写入吗?

Send 与 Sync 把线程安全条件变成类型约束

Rust 用 Send 表达值可以安全转移到另一个线程,用 Sync 表达共享引用可以安全跨线程使用,也就是 T: Sync 时 &T: Send。大多数时候,编译器根据成员类型自动推导这些性质 (The Rust Project Developers, n.d.e)。

这解释了为什么“外面包一个 Arc”不一定能通过编译。Arc 保证引用计数更新的同步,不会替内部类型增加同步机制;例如 Arc<RefCell<T>> 仍不能据此获得安全的多线程共享。

在底层 unsafe 实现正确的前提下,safe Rust 的这些约束排除了 data race,但不排除所有 race condition。假设两个线程各自在锁内读取库存,解锁后判断,随后再次加锁扣减:每次访问都受锁保护,仍可能因为检查与更新分离而卖出超过库存的商品。把哪几步作为一个事务,仍是设计者的责任。

对于记录处理任务,另一个自然设计是通过 channel 把 OwnedRecord 交给唯一的消费者。它能让结果状态保持单一 owner,但也引入队列容量、背压、消费者退出和失败恢复问题。所有权清楚之后,这些业务问题才更容易逐项讨论。

Async:暂停点也属于资源生命周期

如果后台处理还要等待网络响应,用线程阻塞等待可能不是所需的调度方式。Rust 的 async fn 将这段工作表示为一个 future,由 executor 推进。调用它会得到 future,而不会立刻执行函数体;轮询时才推进,遇到尚未就绪的等待则保存状态并让出执行机会 (The Rust Project Developers, n.d.f)。这与 Python 的 coroutine 有可迁移的直觉,但不能把 future 直接当成一条已启动的后台线程。

语言提供 Future 与 async/.await,并没有让所有应用共享一个隐式全局调度器。采用 Tokio 之类的 runtime 时,I/O、定时器、任务创建及相关依赖,需要与该 runtime 的使用方式匹配。

暂停意味着什么值得具体想一次:如果解析结果借用了输入,并且暂停之后还要继续用,输入必须保留下来;如果持有一个锁,那么暂停期间锁也可能一直被持有。编译器会检查其中的内存与类型约束,却不会替你保证其他任务能及时拿到锁。

实际审查时,有三个边界尤其重要:

  1. 任务取得了什么数据。 async move 可以把 owned value 放进 future,但捕获借用时仍只得到借用。像 Tokio 的 spawn 这样可能把任务放到其他线程、且允许任务独立存活的 API,会施加 Send 与 'static 等约束;不是所有 future、所有 executor 都有相同要求。
  2. 等待时仍持有什么。 对短暂的内存更新,可以用普通锁并在 .await 前结束 guard 的作用域;确需跨等待持锁时,要选择支持这一用法的异步锁,并考虑串行化的影响。Tokio 的 shared state 教程 (Tokio Contributors, n.d.a) 解释了这项选择。同步文件操作、阻塞式库调用和长时间 CPU 计算也不会因为外面写了 async 就自动变成非阻塞,应安排到合适的执行位置。
  3. 被取消时已经发生了什么。 尚未完成的 future 可能被 drop;Tokio 的 select! (Tokio Contributors, n.d.a) 就有丢弃未选中分支 future 的场景。局部资源的销毁不会撤销已经发送的请求,也不会自动恢复外部系统的状态。涉及写入、付款或任务确认时,需要明确幂等、重试和补偿语义。

还要区分 future 与任务句柄:根据 Tokio 的 JoinHandle 文档,丢弃句柄会分离任务,而不是取消它。选择取消方式时,应核对具体 API 的契约。

什么时候才需要深入理解 Pin

某些 future 在开始运行后,内部可能保存指向自身状态的引用;如果再把相应值移动到另一个地址,引用会失效。Pin (The Rust Project Developers, n.d.k) 提供的是限制通过指针移动其所指值的契约,不是新的 owner,也不是“把所有东西变成不可变”。对 Unpin 类型,这种额外移动限制并不必要。

普通业务代码经常由 .await 和库提供的封装处理这些细节。只有手写 future、实现底层异步抽象或处理自引用结构时,才需要深入到 pinning 的证明责任。看到 AI 为了修复类型错误加入 Pin::new_unchecked,应该先要求解释为什么需要它,以及谁保证值不会被非法移动。

Unsafe:哪些承诺改由实现者负责

Rust 的集合、操作系统接口和 FFI 最终都需要处理底层操作。unsafe 允许执行编译器无法完整证明安全的操作,但不会取消内存有效性、别名等要求,也不会关闭普通引用的 borrow checking (The Rust Project Developers, n.d.m)。

一个典型例子是前面的 split_at_mut:实现需要依据边界和不重叠条件构造两个独占切片,调用方则通过安全接口获得这项能力。好的边界让调用方不用重复证明底层细节。

因此,审查 unsafe 时需要一份具体理由:地址是否有效且对齐,内存是否已经初始化,别名是否冲突,谁保证引用存活,FFI 的布局与释放协议是否一致。仅仅加上 // SAFETY: this is safe 并没有提供任何证据。

对普通业务功能,先使用现成的安全抽象。引入自定义 unsafe 应有具体需求和可审查的不变量;测试、Miri 或 sanitizer 能发现部分问题,但有限测试不能替代这份证明。依赖库中的不健全安全封装也可能破坏 safe 调用方,因此“我的文件里没有 unsafe”不等于整个调用链已经得到验证。

与 AI 协作时,审查哪些决定

先确定接口,再生成实现

同一处 borrow checker 报错,可以通过复制数据、缩短借用,或改成共享 ownership 来消除。AI 生成的修改即使能通过编译,也可能改变原来的数据关系。例如,把解析结果中的 &str 改成 String,会使结果独立于输入,但也增加了复制;换成 Arc 则会让多处代码共同决定数据的寿命。

对于前面的记录处理器,可以先确定两个边界:解析阶段临时借用输入,长期保存的结果拥有自己的名字。这样,parse_record 与 parse_owned 就分别对应明确的用途。失败也有独立的约定:非法行返回结构化错误,由调用方决定是否跳过,解析器本身不把失败伪装成成功。

让 AI 在这些接口下补全实现,审查时就有了具体依据。以后加入后台消费者,需要重新决定的是数据在哪个边界交出去、队列满了怎么办、失败如何报告;增加几个 async 关键字并不能替代这些决定。

让编译错误暴露设计问题

把完整错误和相关类型交给 AI,并要求它先说明冲突,再修改。常见报错可以先这样解读:

现象先问的问题可能合理的修复方向
使用了 moved value两处使用真的需要同时拥有吗?改为临时借用、调整操作顺序,或明确复制
共享与独占借用冲突原来的视图在修改之后还需要吗?缩短借用、拆分数据,或保存快照
返回引用不够长寿返回值依赖的存储由谁保留?让调用方拥有输入,或返回 owned value
不满足 Send / Sync哪个成员不能跨线程,是否真的要跨线程?调整任务边界、共享方式或执行模型
不满足 'static任务是否借用了创建者的局部数据?转移数据 ownership,或使用保证寿命的 scoped API

有意复制小字符串经常比复杂的 lifetime 设计更划算;同样,确实共享的配置就应该有共享表示。判断依据是程序语义和成本,而不是把 clone、Arc 或 lifetime 标注当成好坏代码的关键词。

把编译器当作第一轮审查

在项目指定的工具链、目标平台和 feature 配置下,让 AI 实际运行验证,不能只给出“应该能编译”的判断。一组常用的基础命令是:

bash
cargo fmt --check
cargo check --locked
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked

这里假设项目已经生成并提交适用的 Cargo.lock。Cargo 区分声明依赖约束的 Cargo.toml 与记录解析结果的 Cargo.lock (The Rust Project Developers, n.d.b);--locked 要求使用现有锁定结果,若必须修改锁文件则失败。新项目首次生成或有意更新依赖时,需要把这种变化单独处理。若要验证发布产物,再补充对应的构建。

这些命令只覆盖所选 target 和启用的 features。不要机械添加 --all-features,因为有些 feature 组合本来就互斥;应按项目支持的组合验证。Clippy 的建议也需要结合上下文判断,不能把一连串 allow 当作完成审查。

还需要读懂项目的边界:crate 是 Rust 的编译单元,Cargo package 可以包含库和可执行目标,workspace 可以协调多个 package。edition 决定一组语言兼容规则,不是第三方库版本。AI 使用某个 API 前,应核对当前工具链、依赖版本及 feature,尤其不要把记忆中的库接口当成项目实际可用的接口。

通过检查后,人的注意力可以集中到更高一层:错误是否丢失上下文,失败有没有伪装成成功,锁是否覆盖完整事务,取消后能否重试,输出是否与需求一致。压测和 profiling 则用于确认分配、锁竞争和执行时间,不能从“Rust”或“zero-cost”几个字推导性能结论。

延伸阅读

The Rust Book 的 ownership、错误处理、泛型与并发章节提供了更完整的示例。异步任务的数据寿命、共享状态与取消行为,可以继续参考前面引用的 Tokio 教程;具体类型的保证和限制则以对应 API 文档为准。

References

The Rust Project Developers. (n.d.a). Arc. doc.rust-lang.org
The Rust Project Developers. (n.d.b). Cargo.toml vs Cargo.lock. doc.rust-lang.org
The Rust Project Developers. (n.d.c). Closures. doc.rust-lang.org
The Rust Project Developers. (n.d.d). Defining an Enum. doc.rust-lang.org
The Rust Project Developers. (n.d.e). Drop. doc.rust-lang.org
The Rust Project Developers. (n.d.f). Extensible Concurrency with Send and Sync. doc.rust-lang.org
The Rust Project Developers. (n.d.g). Futures and the Async Syntax. doc.rust-lang.org
The Rust Project Developers. (n.d.h). Implementations: Trait Implementation Coherence. doc.rust-lang.org
The Rust Project Developers. (n.d.i). Recoverable Errors with Result. doc.rust-lang.org
The Rust Project Developers. (n.d.j). References and Borrowing. doc.rust-lang.org
The Rust Project Developers. (n.d.k). std::cell. doc.rust-lang.org
The Rust Project Developers. (n.d.l). std::pin. doc.rust-lang.org
The Rust Project Developers. (n.d.m). Storing UTF-8 Encoded Text with Strings. doc.rust-lang.org
The Rust Project Developers. (n.d.n). Unsafe Rust. doc.rust-lang.org
The Rust Project Developers. (n.d.o). Using Trait Objects to Abstract over Shared Behavior. doc.rust-lang.org
The Rust Project Developers. (n.d.p). Validating References with Lifetimes. doc.rust-lang.org
The Rust Project Developers. (n.d.q). Vec::split_at_mut. doc.rust-lang.org
The Rust Project Developers. (n.d.r). What Is Ownership? doc.rust-lang.org
Tokio Contributors. (n.d.a). Select. tokio.rs
Tokio Contributors. (n.d.b). Shared State. tokio.rs
Tokio Contributors. (n.d.c). Spawning. tokio.rs

Cite this post

@misc{pu2026csnotesrustideas,
  author = {Pu, Fanyi},
  title  = {Great Ideas in Rust},
  year   = {2026},
  month  = {10},
  url    = {https://pufanyi.com/blog/cs/notes/rust-ideas}
}