楼层: 首页/ 软件技术/ Rust 语言基础/ 所有权 Ownership:Rust 的灵魂
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
会自动 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()。