Rust
已收录:本页完成领域概览规划清单中 Rust 的 4 项内容(所有权/借用与生命周期、trait 与泛型系统、错误处理、常用 crate 与 async 生态)。示例经本机 rustc 1.98(edition 2024)编译验证,第三方 crate 示例依赖需
cargo add(网络可用时直连 crates.io)。
本页按「语法要点 → 运行机制 → 示例 → 踩坑」组织:
所有权、借用与生命周期
所有权三规则(语法要点)
- Rust 中每个值有且仅有一个所有者(owner);
- 同一时刻值只能被一个变量拥有;
- 所有者离开作用域时,值被 drop(自动释放)。
与 Go/Java 的 GC、Python/JS 的引用计数不同:Rust 在编译期静态决定释放点,运行时零开销、无 GC 停顿。这也决定了「拷贝 vs 移动」的显式语义。
移动语义与 Copy(运行机制)
赋值、传参、返回默认按移动处理:所有权转移,原变量不可再用。标量等 Copy 类型例外——它们按位拷贝、原变量仍可用。
// 编译运行:rustc 1.98,cargo run
fn main() {
let s = String::from("hello");
let t = s; // String 非 Copy → 所有权移动给 t
// println!("{s}"); // E0382: borrow of moved value: `s`
println!("{t}"); // hello
let x = 42;
let y = x; // i32 是 Copy → x 仍可用
println!("x={x}, y={y}"); // x=42, y=42
}常用类型是否 Copy:
| Copy | 非 Copy(移动) |
|---|---|
| 全部标量(i32/u64/f32/bool/char) | String、Vec、HashMap 等堆类型 |
&T(不可变引用) | &mut T(可变引用不可 Copy) |
只含 Copy 的元组/数组((i32, i32)) | 含非 Copy 的元组/数组 |
| 自定义结构体显式派生才 Copy | 默认不 Copy,按需 #[derive(Clone)] |
需要保留原值且无法 Copy 时显式 clone()(深拷贝,需类型实现 Clone)。
借用规则(语法要点 + 编译期检查)
用 & 借用而非转移所有权。两条铁律(编译器保证,违反即编译错误):
- 同一时刻,一个值只能有一个
&mut可变借用,或多个&共享借用,二者不可混用; - 借用不能超过所有者的生命周期(杜绝悬垂引用)。
// 编译运行:rustc 1.98
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // 共享借用(不可变)
println!("{first}"); // 借用使用结束(NLL:非词法生命周期)
v.push(4); // ✅ first 已不再使用,可变借用才被允许
let r = &mut v; // 独占可变借用
r.push(5);
// let r2 = &mut v; // E0499: 不能同时有两个可变借用
println!("{:?}", v); // [1, 2, 3, 4, 5]
}NLL(non-lexical lifetimes)允许「借用提前结束」——上面 first 在最后使用后即可释放,v.push 才合法。
字符串与切片(运行机制)
String 拥有堆上字节;&str 是视图(借用,无所有权)。&String 可经 Deref 自动转为 &str。切片 &[T] 是数组/向量的视图:
// 编译运行:rustc 1.98
fn take_slice(xs: &[i32]) -> i32 {
xs.iter().sum()
}
fn main() {
let arr = [1, 2, 3, 4];
println!("{}", take_slice(&arr[1..3])); // 5
let v = vec![10, 20, 30];
println!("{}", take_slice(&v)); // 60(&Vec<T> 自动转 &[T])
let s = String::from("hello world");
let word = &s[..5]; // 视图,无拷贝
println!("{word}"); // hello
}生命周期标注(语法要点)
生命周期是编译期概念:标注 'a 说明「返回引用与哪个入参活得一样久」,不产生运行时开销。三种常见省略规则:入参各得一个生命周期、只有一个入参则返回引用的生命周期与之相同、方法中 &self 占一个。
// 编译运行:rustc 1.98
// longest 的返回引用绑定到较小的那个入参生命周期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}
struct Book<'a> { // 结构体存引用必须标注生命周期
title: &'a str,
}
fn main() {
let title = String::from("The Rust Book");
let book = Book { title: &title }; // title 活得比 book 久 → 合法
println!("{}: {}", longest("a", "bbb"), book.title);
}'static 指「整个程序运行期」:字符串字面量就是 &'static str;全局静态、Box::leak 产物亦属此类。注意 'static 是约束而非绝对寿命——泛型 T: 'static 只是说 T 不含借用(能一直持有),别和「值真的活到程序结束」混为一谈。
单线程共享:Rc + RefCell(踩坑前置知识)
需要「多个所有者 + 运行期可变」时用 Rc(引用计数)与 RefCell(内部可变性,借用检查推迟到运行期):
// 编译运行:rustc 1.98
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let shared = Rc::new(RefCell::new(0));
let a = Rc::clone(&shared); // 计数 +1,共享所有权
let b = Rc::clone(&shared);
*a.borrow_mut() += 1; // 运行期可变借用(RefCell 内)
*b.borrow_mut() += 1;
println!("count = {}", shared.borrow()); // 2(同时存在多个不可变借用可读)
}Rc 不是 Send,不能跨线程——多线程共享用 Arc(原子引用计数)+ Mutex(运行期互斥锁)。
踩坑记录
- 移动后仍使用 → 编译错 E0382:重构思路是「传引用」或提前
clone(明确成本); - 想遍历
Vec<String>又不想移动:用&v(迭代&String)或v.iter(),别用into_iter()吞掉所有权; - 借用冲突几乎都可通过缩小借用作用域解决:先算完不可变借用、再改(利用 NLL);实在跨函数就用
RefCell/Mutex,但那是把编译期检查推迟到运行期,需警惕运行期 panic(borrow 冲突时); &String能传&str参数,但反过来不行:&str没有String的能力,需要时.to_string();- 结构体/函数返回引用时生命周期标注报错:往往意味着「返回的引用不来自任何入参」(悬垂),得返回拥有所有权的类型(如
String)而不是&str; - 用
clone救急会掩盖所有权设计问题:数据流复杂时优先考虑重新设计为「数据一处拥有 + 各处借用」。
trait 与泛型系统
trait 定义与实现(语法要点)
trait 定义行为契约:类型显式 impl(与 Go 的隐式不同),可含默认方法。结构体/枚举为外部类型实现外部 trait 受孤儿规则限制(至少一方是本地类型)。
// 编译运行:rustc 1.98
trait Greet {
fn name(&self) -> &str;
fn greet(&self) -> String { // 默认方法:子类可覆写
format!("你好,{}!", self.name())
}
}
struct User { name: String }
impl Greet for User {
fn name(&self) -> &str { &self.name } // 默认 greet 自动生效
}
fn main() {
let u = User { name: "小明".into() };
println!("{}", u.greet()); // 你好,小明!
}常用派生宏(编译器生成实现):#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] 覆盖了绝大多数「样板实现」。
泛型与 trait bound(语法要点)
泛型函数与 impl:bounds 可用 T: Trait、where 子句或 impl Trait 参数三种写法;dyn Trait 用于动态分发(运行时查 vtable,类似接口指针)。
// 编译运行:rustc 1.98
trait Report {
fn report(&self) -> String;
}
impl Report for i32 {
fn report(&self) -> String { format!("值 {}", self) }
}
// 1) 泛型 + bound(编译期单态化,零开销)
fn print_r1<T: Report>(t: T) { println!("{}", t.report()); }
// 2) where 子句(多个 bound 更清晰)
fn print_r2<T>(t: T) where T: Report + Clone { println!("{}", t.report()); }
// 3) impl Trait 参数(语法糖,等价于泛型)
fn print_r3(t: impl Report) { println!("{}", t.report()); }
// 4) dyn Trait:运行期分发,接受任意实现了 Report 的引用
fn print_r4(t: &dyn Report) { println!("{}", t.report()); }
fn main() {
let v = 42i32;
print_r1(v);
print_r2(v.clone());
print_r3(v);
print_r4(&v);
// 三种写法的取舍:泛型体积大但零开销且可内联;dyn 体积小但每次调用间接跳转
}关联类型与标准 trait(运行机制)
Iterator 是关联类型的典型:type Item 在实现时确定,便于链式 API 自然表达。
// 编译运行:rustc 1.98
struct Evens { cur: i32 }
impl Iterator for Evens {
type Item = i32; // 关联类型:每个迭代元素类型
fn next(&mut self) -> Option<i32> {
self.cur += 2;
Some(self.cur)
}
}
fn main() {
let evens = Evens { cur: 0 };
let first_three: Vec<i32> = evens.take(3).collect();
println!("{:?}", first_three); // [2, 4, 6]
// 迭代器组合子链(惰性求值)
let s: i32 = (1..=10).filter(|x| x % 2 == 0).sum();
println!("{s}"); // 30
}标准库高频 trait 速查:
| trait | 作用 | 典型例子 |
|---|---|---|
Debug | {:?} 调试输出 | #[derive(Debug)] 后 println!("{:?}", v) |
Display | {} 用户可读输出 | 自定义错误类型必须实现 |
From / Into | 低成本类型转换 | let s: String = "x".into();、? 错误自动转换 |
Deref | 自动解引用 | &String → &str、&Vec<T> → &[T] |
AsRef | 参数多态(借用) | fn read(p: impl AsRef<Path>) |
Default | 零参构造 | vec![0; n] 相关、配置缺省值 |
Iterator / IntoIterator | 迭代与 for | for x in v 底层是 IntoIterator |
Eq/PartialEq/Ord/Hash | 相等、排序、哈希 | HashMap 键、sort()、去重 |
对象安全与 dyn 的限制(踩坑必读)
只有对象安全的 trait 才能做成 dyn:不能有泛型方法,不能返回 Self(如 Clone 的 fn clone(&self) -> Self 就做不了 dyn)。这是「Vec<Box<dyn Clone>> 为什么不行」的原因。
// trait Clone { fn clone(&self) -> Self; } // 返回 Self → 非对象安全
// let v: Vec<Box<dyn Clone>>; // ❌ 编译错:trait 非对象安全替代:不用 dyn,直接 #[derive(Clone)] 后用泛型;或自定义返回 Box<dyn ...> 的对象安全 trait。
踩坑记录
- 孤儿规则:想给
Vec<T>实现自己的 trait?Vec是外部类型、你的 trait 若是本地定义的可以;两边都外部则不行(用 newtype 包装绕开); - 泛型方法太多导致编译产物膨胀:性能分析时才需要关心,一般「泛型 + 内联」优先;
- trait 方法名与
ToString/Display冲突时用完全限定语法:<Type as Trait>::method(&x); - 误以为
&dyn Trait和impl Trait等价:前者堆/栈上放胖指针(数据指针 + vtable 指针)、运行期分发;后者编译期展开——类型不同不能互换,返回值位置只能写impl或Box<dyn>(impl Trait不能用于返回值多类型分支); - derive
Copy但类型含String→ 编译错:先想清楚是不是真需要 Copy(能 Copy 的类型必须全字段 Copy); Display忘记实现 →{}编译错;Debug忘记 →{:?}编译错:别把两者混写。
错误处理:Result / Option
Option:可选值(语法要点)
Option<T> = Some(T) | None。链式组合(map / and_then / unwrap_or)避免层层 match:
// 编译运行:rustc 1.98
fn main() {
// 读取环境变量 PORT,解析失败或缺失都回退默认端口
let port: u16 = std::env::var("PORT") // Result<String, VarError>
.ok() // → Option<String>
.and_then(|p| p.parse::<u16>().ok()) // 解析失败 → None
.unwrap_or(8080);
println!("port = {port}");
let maybe: Option<i32> = None;
let doubled = maybe.map(|x| x * 2); // None 保持 None
println!("{:?}", doubled); // None
}unwrap/expect 失败即 panic:只在「错误不可能发生」或原型验证时使用。
Result:可恢复错误(语法要点 + 传播)
Result<T, E> = Ok(T) | Err(E)。函数内用 ? 传播错误(自动 From 转换),由调用方统一处理:
// 编译运行:rustc 1.98
use std::fs;
/// 读取文件并解析首行为整数
fn first_line_number(path: &str) -> Result<i32, std::io::Error> {
let content = fs::read_to_string(path)?; // io::Error 直接返回
let line = content.lines().next().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "空文件")
})?;
line.trim()
.parse()
.map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "首行不是数字"))
}自定义错误类型(工程实践)
领域错误收敛为枚举:实现 Display + std::error::Error,再用 #[from] 让 ? 自动转换。生产代码用 thiserror 派生(见第四节)减少样板:
// 编译运行:rustc 1.98(标准库手写版)
use std::fmt;
#[derive(Debug)]
enum AppError {
NotFound { id: u64 },
Io(std::io::Error),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::NotFound { id } => write!(f, "记录 {id} 不存在"),
AppError::Io(e) => write!(f, "IO 错误: {e}"),
}
}
}
impl std::error::Error for AppError {} // 用源链支撑 {0}/source()
impl From<std::io::Error> for AppError { // ? 自动转换的桥梁
fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
fn load(id: u64) -> Result<String, AppError> {
// 假设此处发生 IO 错误 → ? 自动 From 成 AppError::Io
let data = std::fs::read_to_string(format!("db/{id}.txt"))?;
Ok(data)
}
fn main() -> Result<(), AppError> { // main 返回 Result:错误以 ExitCode 1 打印
let data = load(1)?;
println!("{data}");
Ok(())
}panic 与不可恢复错误(运行机制)
panic! 展开栈并清理(默认 unwind),可设 panic = "abort" 直接终止。RUST_BACKTRACE=1 打印调用栈。使用原则:
- 契约破坏(如索引越界、
unwrap的前置条件被违反)→ panic 可接受; - 用户输入/网络/文件等外部可预期失败 → 必须走
Result; expect("信息")比unwrap()好:panic 信息自带上下文。
踩坑记录
- 忽略
Result编译器警告(#[must_use]机制)不是 bug 但常是隐患:至少let _ = ...显式声明「我故意忽略」; ?自动From只对实现了 From 的方向生效:io::Error→ 自定义错误需手动impl From(或thiserror的#[from]);- 多层嵌套
Result<Result<...>>:用.and_then拍平,别用双重?链硬解; unwrap大量散布在库代码里 = 未来的 panic 地雷:库 API 一律返回Result,让调用方决定;?在main里返回Box<dyn Error>是最省事的脚手架写法,但要向上层「保真类型」时换具体错误枚举(见自定义错误示例);match写Err(_)吞掉错误细节 → 打日志时无信息可查:至少dbg!/.inspect_err先记录现场。
常用 crate 与 async 生态
工具链速查(语法要点)
| 命令 | 作用 | 常用组合 |
|---|---|---|
cargo new / init | 新项目/初始化 | --lib(库)、--vcs git |
cargo add serde | 添加依赖 | 默认最新版;--features、--dev |
cargo build / run | 构建/运行 | --release(优化)、--example x |
cargo check | 只查类型(快) | 开发循环主力 |
cargo test | 跑测试 | #[test]、cargo test -- --nocapture |
cargo clippy | lint 加强版 | CI 必跑,-D warnings 当错误 |
cargo fmt | 格式化 | 提交前必跑 |
cargo doc | 本地文档 | --open 浏览器打开 |
rustup toolchain/update | 工具链管理 | stable/nightly 切换 |
Cargo.toml 关键段:[dependencies]、[dev-dependencies](仅测试)、[features](可选编译特性)。二进制项目的 Cargo.lock 必须提交(锁定构建可复现);库项目通常忽略之。
常用 crate 速查表(工程选型)
| 领域 | crate | 说明 |
|---|---|---|
| 序列化 | serde + serde_json | derive 宏驱动,JSON 事实标准 |
| 错误处理 | thiserror | 库错误枚举派生(#[error(...)]、#[from]) |
| 错误聚合 | anyhow | 应用层 Result<T> = 任意错误(bail!、context) |
| CLI | clap | 参数解析 + 帮助/补全(derive 模式) |
| HTTP | reqwest | 同步/异步 HTTP 客户端 |
| 异步运行时 | tokio | async/await 运行时事实标准 |
| 并行计算 | rayon | 数据并行 par_iter(),一行把串行变并行 |
| 随机 | rand | 随机数/抽样 |
| 时间 | chrono(旧)time(新) | 日期时间 |
| 日志 | log + env_logger 或 tracing | 结构化追踪 |
| 正则 | regex | 编译期缓存优化 |
| 一次性初始化 | once_cell / std::sync::OnceLock | 全局惰性变量 |
序列化与错误派生示例(工程样板)
// 编译运行:需要 cargo add serde serde_json thiserror
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Point { x: i32, y: i32 }
#[derive(Error, Debug)]
enum ApiError {
#[error("资源 {id} 不存在")]
NotFound { id: u64 },
#[error(transparent)]
Io(#[from] std::io::Error), // ? 自动 From
#[error(transparent)]
Json(#[from] serde_json::Error), // ? 自动 From
}
fn main() -> Result<(), ApiError> {
let p = Point { x: 1, y: 2 };
let json = serde_json::to_string(&p)?; // {"x":1,"y":2}
println!("{json}");
let back: Point = serde_json::from_str(&json)?;
assert_eq!(p, back);
Ok(())
}async 生态入门(运行机制)
async fn 返回 Future,需运行时驱动(tokio)。#[tokio::main] 建运行时;spawn 并发执行;join! 并行等待多个 future;阻塞操作(文件 IO、CPU 密集)用 spawn_blocking,别堵住异步线程:
// 编译运行:需要 cargo add tokio --features "macros,rt-multi-thread,time"
use std::time::Duration;
use tokio::time::sleep;
async fn fetch_one(id: u32) -> u32 {
sleep(Duration::from_millis(id as u64 * 10)).await;
id * 2
}
#[tokio::main]
async fn main() {
// spawn:后台并发任务,await JoinHandle 取结果
let h = tokio::spawn(async { fetch_one(1).await });
let (a, b) = tokio::join!(fetch_one(2), fetch_one(3)); // 并行等待
println!("{} {} {}", h.await.unwrap(), a, b); // 2 4 6
// 阻塞场景:CPU 密集任务放线程池,避免占死 worker
let heavy = tokio::task::spawn_blocking(|| {
let mut n = 0u64;
for i in 1..=1_000_000 { n = n.wrapping_add(i); }
n
})
.await
.unwrap();
println!("{heavy}");
}async 编程口诀:等待用 await,并发用 join!/select!/spawn,阻塞用 spawn_blocking。
踩坑记录
- 忘配 tokio features:
#[tokio::main]报错 → 补macros,rt-multi-thread; - async fn 里同步阻塞(如
std::thread::sleep、大循环)会占死整个 worker 线程:换sleep().await或spawn_blocking; - 依赖版本冲突(两个 crate 需要同一库不同版本):
cargo update -p 包名或检查重复;被cargo tree排查依赖链; - 默认特性带来无谓依赖:用
default-features = false+ 显式 features 裁剪(如tokio只要time); Send/Sync跨线程限制:Rc/RefCell/裸指针不能跨spawn,报错指向「Future 不是 Send」时多半是捕获了非 Send 数据;- 忘了 Cargo.lock 提交 → 同事/CI 拿到不同版本依赖,偶现构建失败;
- 库代码别用
anyhow(它吞掉具体类型),库的错误类型用thiserror枚举对外提供可匹配性。
状态与参考
- 状态:已收录(2026-09-02 完成领域概览规划的 4 项主题)。
- 运行环境:示例基于 rustc 1.98 / edition 2024(本机实测);第三方 crate 示例需
cargo add serde serde_json thiserror与cargo add tokio --features "macros,rt-multi-thread,time"。 - 参考:Rust 程序设计语言(The Book)、Rust by Example、std 文档、tokio 指南。
下一步
- [ ] 补充「unsafe 与 FFI」专题(裸指针、
unsafe fn约定、绑定 C 库) - [ ] 对照「语言对比」规划,沉淀 Rust 与 Go/JS/TS/Python 在所有权与并发上的差异笔记
- [ ] 完善生态深入:
tower/axumWeb 服务、sqlx数据库层的实战示例
写作规范与页面规划请参阅领域概览。