跳转到主内容可变借用 | 物联网民工

可变借用

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

这条规则会影响代码结构,但它换来的是更清楚的状态归属:任何时刻,谁在改这份数据,类型系统都能看见。

下一篇讲生命周期。可变借用解决“谁能在此刻读写”,生命周期解决“这个引用到底能活多久”。