C 里,拿到 T * 通常意味着“可以改”。问题是,C 不阻止你同时留下很多入口:
uint8_t *a = buf;
uint8_t *b = buf;
a[0] = 1;
printf("%u\n", b[0]);
这段代码本身没错,但在真实项目里,多个指针同时读写同一块数据,会让状态变化很难追踪。谁刚刚改过?谁还拿着旧视图?某个函数会不会在你读之前顺手改了?
Rust 的 &mut 不只是“可写指针”。它表示独占可写访问:
同一段时间里,同一份数据只能有一个可写入口。
这条规则一开始会让 C 程序员觉得束手束脚。适应以后,它其实是在逼你把“谁负责修改状态”写清楚。
&mut 代表临时拿走写权限
一个最小例子:
fn bump(value: &mut u32) {
*value += 1;
}
fn main() {
let mut count = 0_u32;
bump(&mut count);
println!("{count}");
}
count 仍然属于 main。bump 没有拿走所有权,只是临时拿到写权限。
可变借用存在时,原变量不能同时被别的地方随意访问:
let mut count = 0_u32;
let r = &mut count;
*r += 1;
// println!("{count}"); // 如果 r 后面还要用,这里不允许
*r += 1;
这不是因为编译器不懂指针,而是因为 Rust 不允许“一个地方正在写,另一个地方同时读或写同一份数据”。
可变借用和只读借用不能重叠
C 里 const T * 和 T * 可以指向同一块内存:
const uint8_t *view = buf;
uint8_t *writer = buf;
view 看起来只读,但 writer 仍然能修改底层数据。读到的值是否稳定,靠调用关系和人工约定。
Rust 会拒绝重叠的只读借用和可变借用:
let mut data = vec![1_u8, 2, 3];
let view = &data;
let writer = &mut data; // 不允许
println!("{}", view.len());
writer.push(4);
因为 view 还要使用,writer 不能同时出现。只读借用的含义不仅是“我不改”,也包括“在我使用期间,没有别人通过可变借用改同一份数据”。
如果先把只读借用用完,再修改,就可以:
let mut data = vec![1_u8, 2, 3];
println!("{}", data.len());
let writer = &mut data;
writer.push(4);
Rust 看的是借用实际使用范围。只读访问结束后,写权限可以重新借出去。
用作用域缩短可变借用
C 程序员经常把指针变量放在函数开头,然后一路传下去。Rust 里,可变借用最好尽量短:
let mut packet = vec![1_u8, 2, 3];
{
let payload = &mut packet[1..];
payload[0] = 9;
}
println!("{packet:?}");
花括号结束后,payload 消失,可变借用也结束。后面可以继续读 packet。
这种写法在处理 buffer 时很常见。先切出一段可写区域,改完马上结束借用:
fn fill_header(buf: &mut [u8], len: u16) {
let header = &mut buf[..2];
header.copy_from_slice(&len.to_be_bytes());
}
如果后面还要访问整块 buffer,可以显式缩短可变 slice 的范围:
fn encode(buf: &mut [u8], payload_len: u16) {
{
let header = &mut buf[..2];
header.copy_from_slice(&payload_len.to_be_bytes());
}
let total = buf.len();
println!("encoded {total} bytes");
}
不是所有地方都要加花括号。很多时候编译器能从最后一次使用推断借用结束。但当代码读起来不清楚时,短作用域是很实用的工程写法。
不能一边遍历一边改容器结构
C 里一边遍历数组一边修改内容通常没问题:
for (size_t i = 0; i < len; i++) {
data[i] ^= key;
}
Rust 中修改元素也很直接:
fn xor_in_place(data: &mut [u8], key: u8) {
for byte in data {
*byte ^= key;
}
}
这里 for byte in data 逐个拿到元素的可变借用,不会改变 slice 的长度和存储位置。
但一边遍历 Vec,一边 push,就不是同一回事:
let mut data = vec![1_u8, 2, 3];
for byte in &data {
if *byte == 2 {
data.push(4); // 不允许
}
}
遍历时 &data 是只读借用。push 需要可变借用,而且可能导致 Vec 重新分配内存。Rust 不允许这两个操作重叠。
常见改法是分两步:先算出要做什么,再修改容器。
let mut data = vec![1_u8, 2, 3];
let need_append = data.iter().any(|byte| *byte == 2);
if need_append {
data.push(4);
}
这不是绕编译器,而是把读阶段和写阶段分清楚。
split_at_mut 表示两段互不重叠
有时确实需要同时修改同一个 buffer 的两段。C 里会直接算两个指针:
uint8_t *left = buf;
uint8_t *right = buf + mid;
Rust 不能随便写两个 &mut 指向同一个 slice,因为编译器无法从普通索引里稳定判断两段一定不重叠。
标准库提供了表达不重叠的 API:
let mut data = [1_u8, 2, 3, 4];
let (left, right) = data.split_at_mut(2);
left[0] = 10;
right[0] = 30;
split_at_mut 的返回值类型是 (&mut [T], &mut [T])。它向类型系统表达:这两个可变 slice 来自同一个数组,但范围不重叠,所以可以同时存在。
这类 API 很重要。Rust 不是禁止同时改两块数据,而是要求你证明这两块数据不是同一块。
结构体字段可以分别借用
如果两个字段互不重叠,Rust 通常能看懂:
struct Stats {
rx: u64,
tx: u64,
}
let mut stats = Stats { rx: 0, tx: 0 };
let rx = &mut stats.rx;
let tx = &mut stats.tx;
*rx += 1;
*tx += 1;
rx 和 tx 是不同字段,可以同时有可变借用。
但如果通过函数把整个结构体借出去,就要按整个对象看待:
fn update_rx(stats: &mut Stats) {
stats.rx += 1;
}
let mut stats = Stats { rx: 0, tx: 0 };
let tx = &mut stats.tx;
// update_rx(&mut stats); // tx 还在使用时,不允许再借整个 stats
*tx += 1;
这时最简单的改法通常是调整顺序,或者让函数只接收它真正需要的字段:
fn update_rx(rx: &mut u64) {
*rx += 1;
}
let mut stats = Stats { rx: 0, tx: 0 };
update_rx(&mut stats.rx);
stats.tx += 1;
函数参数越接近真实需求,可变借用的范围越小,代码越容易组合。
方法里的 &mut self
Rust 方法如果要修改对象,通常写 &mut self:
struct Counter {
value: u32,
}
impl Counter {
fn bump(&mut self) {
self.value += 1;
}
fn value(&self) -> u32 {
self.value
}
}
调用时:
let mut counter = Counter { value: 0 };
counter.bump();
println!("{}", counter.value());
bump 调用期间,整个 counter 被可变借用。方法返回后,借用结束,可以继续只读访问。
如果一个方法接收 &mut self,它在 API 层面表达的是:调用期间我要独占修改这个对象。这个信号比 C 里的 struct Counter *self 更明确。
这一篇先记住什么
&mut 不是普通可写指针,而是独占写权限。
从 C 迁移到 Rust 时,最常见的调整不是加 mut,而是重新安排访问顺序:
先读完,再写
写的时候缩短借用范围
需要同时写两段,用 split_at_mut 这类不重叠 API
函数只借它真正要改的字段或 slice
这条规则会影响代码结构,但它换来的是更清楚的状态归属:任何时刻,谁在改这份数据,类型系统都能看见。
下一篇讲生命周期。可变借用解决“谁能在此刻读写”,生命周期解决“这个引用到底能活多久”。