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 的变化是:所有权转移会让原变量失效,资源离开作用域会自动释放,复制资源必须显式写出来。
下一篇再讲借用。所有权回答“谁拥有”,借用回答“在不拿走所有权的情况下,谁能临时读或写”。