04 所有权与借用(Rust 的“核心门槛”与核心价值)
本章目标
- 你能解释:为什么 Rust 不需要 GC 也能安全释放内存。
- 你能在真实代码里解决常见编译错误:move、借用冲突、生命周期相关提示。
- 你能理解
asset-light里两类典型“共享可变”场景:- UI 状态:
Signal<AppState>的.read()/.write() - DB 连接:
Mutex<Connection>+OnceLock<Database>
- UI 状态:
关键概念(先把话说清楚)
1) 所有权(Ownership)
- 每个值在任意时刻只有一个“拥有者”(owner)
- owner 离开作用域,值会被 drop(资源释放)
2) 移动(Move)
对“拥有资源”的类型(如 String、Vec<T>):
- 赋值 / 传参默认是 move
- move 之后,旧变量失效(防止“双重释放”)
3) 借用(Borrow)与引用(Reference)
&T:不可变借用(多读)&mut T:可变借用(单写)
Rust 的核心规则:
- 同一时间:
- 要么有任意多个
&T - 要么只有一个
&mut T
- 要么有任意多个
与 JS/Java 的直觉冲突点(你一定会遇到)
- “我只是传个参数,为啥变量就不能用了?”
因为你传的是 move(把所有权交出去了),不是引用。 - “我就想同时读写一下,为啥不行?”
因为 Rust 要在编译期保证没有数据竞争/悬垂引用。 - “我明明只是在 UI 里读 state,为啥又报借用冲突?”
因为.read()/.write()可能拿到内部借用句柄,作用域没结束就会冲突。
String vs &str(Rust 初学者最常见困惑)
String:拥有所有权、可增长、堆分配&str:字符串切片引用(通常指向String或静态字符串)
经验法则:
- API 接收参数时,优先用
&str(更通用、避免不必要的 clone) - 存储字段时,用
String(拥有数据,生命周期更简单)
在 asset-light 中的“共享可变”怎么做到安全
1) UI 全局状态:Signal<AppState>
文件:src/state/app_state.rs
provide_app_state()用use_context_provider注入一个Signal<AppState>- 组件内用
use_app_state()获取它 .read():拿到只读视图.write():拿到可写视图(通常是短作用域)
你在页面里经常看到类似写法:
let mut state = use_app_state();let assets = state.read().assets.clone();state.write().assets = assets;
为什么常见 clone()?
因为 UI 渲染往往需要拥有数据(或需要跨作用域),而借用规则不允许你把 &T 引用带出读取作用域。
学习阶段先接受 clone 的存在,等掌握后再优化。
2) DB 单例与连接:OnceLock<Database> + Mutex<Connection>
文件:src/db/mod.rs 与 src/db/connection.rs
OnceLock<Database>:保证数据库只初始化一次(全局单例)Database内部用Mutex<Connection>:- 互斥锁保证同一时间只有一个线程/调用在访问
Connection - 让“全局可变”在类型系统下变得安全
- 互斥锁保证同一时间只有一个线程/调用在访问
你会看到:
let conn = self.conn.lock().unwrap();
这行 unwrap() 在工程上可以进一步改进(见后续“错误处理”章节),但它体现了“锁拿到了才能继续用连接”的共享可变模式。
常见编译错误“翻译器”(带你读懂报错)
1) use of moved value
你把值 move 走了,然后又用旧变量。
解决思路(按优先级):
- 改成传引用:
fn foo(s: &str)/fn foo(v: &Vec<T>) - 必要时 clone:
let b = a.clone(); - 重新设计数据流:把 ownership 留在更高层
2) cannot borrow ... as mutable because it is also borrowed as immutable
同一个值同时存在不可变借用与可变借用。
解决思路:
- 缩小借用作用域:用代码块
{ ... }包住.read()的使用 - 先把数据拷贝出来再写:
let x = state.read().foo.clone(); state.write().foo = x; - 重构:拆成两个 signal/字段,减少冲突
3) 生命周期相关提示(先不硬啃)
学习期建议:
- 先用拥有类型(
String/Vec<T>)把功能做出来 - 当你开始做性能优化/减少 clone 时,再回头系统学习 lifetime 标注
本章练习(用真实代码练 borrow 思维)
练习 A:把 “读写同一个 state” 的作用域缩小
找一个页面(例如 src/pages/assets.rs)观察这种模式:
- 先
state.read()拿数据 - 后面
state.write()更新数据
尝试把读取逻辑包进一个更小的作用域块,确保你理解“借用结束点”:
{ let assets = state.read().assets.clone(); ... }- 再进行写入
目的不是改功能,而是让你建立“借用在何时结束”的直觉。
练习 B:识别哪些类型是 Copy,哪些会 move
在本项目里找几个字段:
Category(Copy:枚举,Clone, Copy)Uuid(非 Copy,通常 move / clone)Decimal(非 Copy,通常 clone 或引用)String(move 语义最典型)
你需要能解释:为什么 Category 可以随便传来传去,而 String 不行。
练习 C:理解为什么 DB 连接需要 Mutex
打开 src/db/connection.rs,回答:
- 如果没有
Mutex,我们如何在多个调用点安全地复用一个Connection? - 为什么 Rust 不允许“全局可变静态变量直接拿来改”?