04
所有权 Ownership:Rust 的灵魂
Ownership · Move · Borrowing · Slice
这一章是整本书最重要的一章。所有权就是 Rust 的身份证制度——每个值都有唯一的主人,主人走了值就销毁。理解了它,Rust 就入门了一半;不理解,编译器会骂你到怀疑人生。
论所有权三条铁律
① 每个值有且仅有一个所有者。像独生子女,值只归一个变量管。
② 值在所有者离开作用域时被丢弃(drop)。花括号 { } 一关,里面的值自动清理,这就是 RAII,没有 GC。
③ 同一时刻只能有一个所有者。把值交给别人,自己就没了——这叫移动 Move。
移动 Move:把值给别人,自己就不能用了
fn main() {
let s1 = String::from("hello"); // String 在堆上,s1 是主人
let s2 = s1; // 所有权 move 给 s2
// println!("{}", s1); // 编译报错!value borrowed here after move
println!("{}", s2); // s2 用没问题
}
栈上的数据(指针+长度+容量这三个数)被复制了一份,但堆上真正的字符数据没有复制——两个指针指向同一块堆内存很危险,所以 Rust 干脆让 s1 失效。这就是"移动语义":浅拷贝 + 旧变量失效。
Copy vs Clone:自动拷 vs 手动拷
fn main() {
// Copy:栈上简单类型,赋值是真拷贝,旧变量还能用
let x = 5;
let y = x;
println!("{} {}", x, y); // x 还能用!整数自动 Copy
// Clone:需要深拷贝堆数据时,显式调用 .clone()
let s1 = String::from("hi");
let s2 = s1.clone(); // 真把堆上数据也复制一份
println!("{} {}", s1, s2); // 两个都能用,因为是两份独立数据
}
| 会自动 Copy | 说明 |
|---|---|
| 所有整数 / 浮点 / bool / char | 纯栈上小数据,复制没成本 |
| 只含 Copy 类型的 tuple / 数组 | 如 (i32, f64) 是 Copy;(String, i32) 不是 |
函数与所有权:传进去 move,传出来 move 回来
fn takes_ownership(s: String) {
println!("拿到了 {}", s);
} // s 在这里离开作用域,被 drop,内存释放
fn gives_back() -> String {
String::from("还给你")
}
fn main() {
let s = String::from("hello");
takes_ownership(s); // s 被 move 进函数,此后 s 不能用了
let s2 = gives_back(); // 函数返回值 move 给 s2
println!("{}", s2);
}
借用 Borrowing:借书不拿走
论与其来回 move,不如借着用
每次把 String move 进函数再 move 回来太啰嗦。引用就是"借书不拿走"——你给函数一个 &s(引用),函数能读,但所有权还是你的。& 是不可变引用(只读),&mut 是可变引用(可写)。
fn calculate_length(s: &String) -> usize {
s.len() // 借用,不拿走所有权
} // 引用离开作用域,但因为不是所有者,不会 drop s
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // 传引用,s 还在
println!("{} 的长度是 {}", s, len);
let mut s2 = String::from("hi");
let r = &mut s2;
r.push_str("!");
println!("{}", r);
}
$ cargo run
hello 的长度是 5
hi!
借用三规则(背下来)
①同一时刻,要么有一个可变引用,要么有多个不可变引用,二者不可兼得。②引用必须始终有效(不能悬垂)。③编译器用 NLL(非词法生命周期)自动判断"引用最后一次用在哪",用完就释放,不用等块结束。
记住这个比喻:只读引用是"多人围观",可变引用是"独占施工"——施工时不许围观,围观时不许施工。这正是 Rust 在编译期消灭数据竞争的底层原理。
切片 Slice:引用集合的一部分,不拥有数据
fn main() {
let s = String::from("hello world");
let hello: &str = &s[0..5]; // 字符串切片,借用 s 的一部分
let arr = [1, 2, 3, 4];
let part: &[i32] = &arr[1..3]; // 数组切片 [2, 3]
println!("{} {:?}", hello, part);
}
$ cargo run
hello [2, 3]
字符串切片必须落在 UTF-8 字符边界
&s[0..1] 对"你好"这种多字节中文会直接 panic,因为切到了一个字符的中间。Rust 是 UTF-8,不能按索引取字符,要遍历用 .chars()。