跳转到主内容借用:引用不是普通指针 | 物联网民工

借用:引用不是普通指针

上一篇讲所有权:谁拥有,谁释放。

但大多数函数并不想接管资源。它只是想临时看一眼数据,或者临时改一下数据。C 里通常传指针:

void inspect(const uint8_t *buf, size_t len);
void update(uint8_t *buf, size_t len);

Rust 里对应的是借用:

fn inspect(buf: &[u8]) {}
fn update(buf: &mut [u8]) {}

借用的核心不是“Rust 指针语法”,而是访问权限:不拿走所有权,只临时读或写。

这一篇只讲 &T 和 &mut T 的第一层工程含义,不展开生命周期标注。

C 指针经常表示临时使用

看一个 C 函数:

void print_packet(const uint8_t *buf, size_t len)
{
    for (size_t i = 0; i < len; i++) {
        printf("%02x ", buf[i]);
    }
}

调用方仍然拥有 buffer:

uint8_t packet[] = {1, 2, 3};
print_packet(packet, sizeof(packet));

这个函数只是临时读取,不负责释放,也不应该保存指针。但这些规则并不全在类型里:

  • 它会不会保存 buf
  • 调用期间是否有人修改 buffer
  • buf 和 len 是否匹配
  • 返回后调用方能不能继续使用

Rust 的借用把这些意图写得更明确。

&T 是共享只读借用

如果函数只是读取一个值,可以接收 &T:

fn print_name(name: &String) {
    println!("{name}");
}

fn main() {
    let name = String::from("sensor");
    print_name(&name);
    println!("{name}");
}

&name 表示借用 name,函数没有拿走所有权。调用结束后,name 还能继续用。

不过写 Rust API 时,更常见的是接收更通用的 &str:

fn print_name(name: &str) {
    println!("{name}");
}

对 buffer 来说,只读借用通常写成 slice:

fn checksum(buf: &[u8]) -> u16 {
    buf.iter().map(|b| *b as u16).sum()
}

第一层模型是:

&T    = 临时只读访问一个 T
&[u8] = 临时只读访问一段字节
&str  = 临时只读访问一段 UTF-8 文本

多个只读借用可以同时存在,因为它们不会修改数据。

&mut T 是独占可写借用

如果函数要修改数据,需要 &mut T:

fn reset_count(count: &mut u32) {
    *count = 0;
}

fn main() {
    let mut count = 10;
    reset_count(&mut count);
    println!("{count}");
}

&mut count 表示把 count 临时借出去,让函数可以修改。

对 buffer 来说:

fn xor_in_place(buf: &mut [u8], key: u8) {
    for byte in buf {
        *byte ^= key;
    }
}

&mut [u8] 表示独占可写访问一段字节。

“独占”是关键。Rust 不允许在同一段时间里同时存在一个可写借用和其他访问同一数据的借用。这样能避免 C 里常见的“有人在读,有人同时在改”的混乱。

可写借用期间,原变量不能随便用

看一个简单例子:

let mut data = vec![1, 2, 3];
let view = &mut data;

view.push(4);
// println!("{}", data.len()); // 在 view 还要使用时不允许

从 C 的角度看,这像是把一个可写指针交给了别人。在这段时间里,你不能同时假装自己还可以任意访问原对象。

这条规则阻止了很多别名问题:

uint8_t *a = buf;
uint8_t *b = buf;

a[0] = 1;
printf("%u\n", b[0]); /* 到底谁在改,谁在读? */

C 允许这种写法,但复杂项目里会让数据变化很难追踪。Rust 的 &mut 把“此刻谁有写权限”收紧了。

借用不释放资源

借用不是所有权转移。

fn inspect(data: &[u8]) {
    println!("{}", data.len());
}

fn main() {
    let packet = vec![1, 2, 3];
    inspect(&packet);
    println!("{}", packet.len());
}

inspect 只是借用 packet 的内容。它不能释放这段内存,也不能把它当作自己的资源长期保存。

如果函数接收 Vec<u8>:

fn consume(data: Vec<u8>) {
    println!("{}", data.len());
}

那就是拿走所有权。调用方传进去以后,原变量不能再用。

所以 API 设计时要先问:

只是读:&[u8] / &str / &T
需要改:&mut [u8] / &mut T
要接管资源:Vec<u8> / String / File

这个判断比记语法更重要。

借用不能超过拥有者

虽然这一篇不讲生命周期标注,但借用有一个基本限制:借用不能比被借用的值活得更久。

下面这种 C 代码很危险:

const char *bad_name(void)
{
    char buf[32] = "sensor";
    return buf;
}

函数返回后,buf 已经不存在,返回的指针变成悬垂指针。

Rust 会阻止类似情况。你不能返回指向局部 String 内部数据的引用:

fn bad_name() -> &str {
    let name = String::from("sensor");
    &name
}

这段代码无法通过编译。原因很朴素:name 在函数结束时释放,返回的引用会指向已经不存在的数据。

生命周期后面单独讲。这里先记住:借用只是临时访问,不能比拥有者活得更久。

这一篇先记住什么

C 和 Rust 的第一层对应关系是:

const T * 临时读取        -> &T
const uint8_t * + len     -> &[u8]
const char * 文本视图     -> &str 或 &CStr,取决于边界
T * 临时修改              -> &mut T
uint8_t * + len 临时修改  -> &mut [u8]
函数接管资源             -> 拿拥有类型,不是借用

借用回答的是:不拿走所有权时,谁能临时访问这份数据。

&T 是共享只读借用,&mut T 是独占可写借用。它们不是普通 C 指针的语法替换,而是把读写权限和临时访问边界写进类型。

下一篇再讲生命周期。生命周期要回答的是:这些借用到底能活多久,为什么不能返回指向局部变量的引用。