跳转到主内容所有权:谁拥有,谁释放 | 物联网民工

所有权:谁拥有,谁释放

C 代码里早就有所有权。

只要一段内存需要 free,一个文件需要 fclose,一个句柄需要释放,就一定存在一个问题:谁负责最后把它释放掉。

C 的问题不是没有所有权,而是所有权通常不在类型里。它靠函数名、注释、调用约定和团队经验维护。Rust 的 ownership 做的事,就是把这些约定变成编译器能检查的规则。

这一篇只讲一个核心问题:谁拥有,谁释放;所有权移动以后,为什么原变量不能再用。

C 里所有权靠约定

看一个常见 C API:

uint8_t *make_packet(size_t *out_len);
void free_packet(uint8_t *packet);

调用方要知道几件事:

  • make_packet 返回的指针是不是要释放
  • 应该用 free 释放,还是用专门的 free_packet
  • 失败时返回 NULL 还是别的状态
  • out_len 在失败时是否有效
  • 释放以后还能不能继续访问这个指针

这些规则都不在 uint8_t * 里。

再看另一个 API:

void send_packet(const uint8_t *packet, size_t len);

它看起来只是借用数据。但它会不会保存这个指针?调用返回后,调用方能不能释放 buffer?如果文档没写清,调用方只能猜。

Rust 要解决的是这类边界不清。

Vec 表示拥有一段 buffer

Rust 里,如果函数创建并返回一段 buffer,通常直接返回 Vec<u8>:

fn make_packet(payload: &[u8]) -> Vec<u8> {
    let mut packet = Vec::new();
    packet.push(1);
    packet.push(payload.len() as u8);
    packet.extend_from_slice(payload);
    packet
}

Vec<u8> 表示调用方拿到了这段 buffer 的所有权。

let packet = make_packet(b"hello");
println!("{}", packet.len());

packet 离开作用域时,它拥有的内存会释放。调用方不用找一个配套的 free_packet。

第一层模型是:

拥有 Vec<u8> = 拥有这段动态分配的字节 buffer
变量离开作用域 = 释放它拥有的资源

这就是上一篇 Drop 背后的所有权规则。

move 表示所有权转移

如果把 Vec<u8> 传给一个接收 Vec<u8> 的函数,所有权会移动:

fn consume_packet(packet: Vec<u8>) {
    println!("len = {}", packet.len());
}

fn main() {
    let packet = make_packet(b"hello");
    consume_packet(packet);

    // println!("{}", packet.len()); // 这里不能再用
}

consume_packet(packet) 以后,packet 的所有权已经转移到函数参数 packet 里。函数结束时,参数离开作用域,buffer 被释放。

原来的变量不能再用,是为了避免 C 里最常见的错误之一:

free(packet);
printf("%zu\n", len);      /* 还好 */
printf("%u\n", packet[0]); /* use-after-free */

Rust 不允许你在所有权已经移走以后继续访问原变量。

这不是语法洁癖。它是在阻止“释放责任已经不在你这里了,但你还继续用”的情况。

Copy 类型不走这个规则

不是所有值传参后都会让原变量失效。

像整数、布尔值这类简单类型通常实现了 Copy:

fn print_id(id: u32) {
    println!("{id}");
}

fn main() {
    let id = 42u32;
    print_id(id);
    println!("{id}"); // 还能用
}

u32 被复制了一份,原来的 id 还在。

但 Vec<u8>、String、File 这类拥有资源的类型默认不会自动复制。因为复制它们不是简单复制几个字节那么轻松,还会带来“两个值谁负责释放同一份资源”的问题。

第一层可以这样看:

u32 / bool 等简单值:传参通常复制
Vec / String / File 等资源值:传参通常移动所有权

clone 是显式复制资源

如果你确实需要两份数据,可以显式 clone:

let a = String::from("device");
let b = a.clone();

println!("{a}");
println!("{b}");

clone 的意思不是“绕过所有权”,而是创建一份新的数据。a 和 b 各自拥有自己的内容,各自释放。

这和 C 里手写深拷贝类似:

char *b = strdup(a);

所以不要把 clone 当成消除编译错误的默认手段。它可能分配内存、复制数据,也可能改变性能特征。真正只是读取时,更应该借用。

只是使用,就借用

如果函数只是读取 packet,不应该接收 Vec<u8>,而应该接收 slice:

fn inspect_packet(packet: &[u8]) {
    println!("len = {}", packet.len());
}

fn main() {
    let packet = make_packet(b"hello");
    inspect_packet(&packet);
    println!("{}", packet.len()); // 还能继续用
}

&packet 表示借用。函数可以看这段数据,但不拥有它,也不负责释放。

这和 C 里 const uint8_t *buf, size_t len 的意图接近,但 Rust 把“只是借用”写得更明确。

这一篇不展开借用规则。只先记住 API 设计时的第一个判断:

函数要接管资源:接收 Vec<T> / String / File
函数只是读取:接收 &[T] / &str
函数要原地修改:接收 &mut [T] 或 &mut T

返回值也是所有权转移

C 里返回指针常常意味着“调用方要释放”,但不一定清楚:

char *build_name(void);

Rust 里返回拥有类型就很明确:

fn build_name() -> String {
    String::from("device-01")
}

调用方拿到 String,就拥有它。之后可以继续传递,也可以让它在作用域结束时释放。

let name = build_name();
println!("{name}");

如果返回的是借用,比如 &str,就意味着函数没有把数据所有权交出来。这个话题会牵涉生命周期,后面单独讲。

现在先抓住最简单的规则:返回 String / Vec<u8> 这类拥有类型,就是把所有权交给调用方。

这一篇先记住什么

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

谁负责 free / close          -> 谁拥有这个值
返回 malloc 出来的指针       -> 返回 Vec<T> / String / Box<T>
函数接管资源                 -> 参数接收拥有类型
函数只是读取                 -> 参数接收借用,如 &[u8] / &str
手写深拷贝                   -> clone
释放后继续访问               -> move 后原变量不可用

ownership 不是 Rust 发明的新问题。C 项目里一直有所有权,只是靠约定维护。

Rust 的变化是:所有权转移会让原变量失效,资源离开作用域会自动释放,复制资源必须显式写出来。

下一篇再讲借用。所有权回答“谁拥有”,借用回答“在不拿走所有权的情况下,谁能临时读或写”。