Rust 所有权详解

63 阅读10分钟

为什么

所有权三大规则(必须背熟)

  1. 每个值(value)在任意时刻都有且只有一个所有者(owner)
  2. 当所有者离开作用域(scope),值会被自动释放(drop)
  3. 值的所有权可以转移(move),但转移后原变量不能再用

上诉三个规则解决的问题:不会出现悬垂指针、重复释放、数据竞争等问题

常见场景

作用域与自动释放

fn main() {
  {
      let s = String::from("hello");
      // s 在这里拥有这段堆内存(String 的内容在堆上)
      println!("{}", s);
  } // <- 作用域结束:s 被 drop,堆内存自动释放
  // println!("{}", s); // ❌ 编译错误:s 已经不在作用域内
}

Move:所有权转移(最常见)

String 这类“拥有堆内存”的类型,默认赋值/传参会 move(转移所有权),不是拷贝

fn main() {
  let s1 = String::from("hello");
  let s2 = s1;
  // ↑ 所有权从 s1 转移到 s2
  // s1 现在“失效”,避免同一块堆内存被释放两次

  // println!("{}", s1); // ❌ 编译错误:borrow of moved value
  println!("{}", s2);    // ✅
}

为什么这么设计:如果允许 s1、s2 同时“以为自己拥有同一块堆内存”,作用域结束时会,发生double free。Rust 直接在编译期禁止。

Copy:栈上按位复制(不会 move)

像整数、布尔、浮点、char 以及它们的简单组合(只要实现了 Copy)赋值时是 copy

fn main() {
  let a = 10;     // i32 是 Copy
  let b = a;      // 发生按位复制(copy)
  println!("{a} {b}"); // ✅ a 依然可用
}

哪些常见类型是 Copy?

- 数字类型:i32, u64, f32...

- bool, char

- (i32, bool) 这种成员全是 Copy 的 tuple(元组)

- &T 引用本身通常也是 Copy(注意:复制的是“引用”,不是复制数据)

Clone:显式深拷贝(堆数据复制)

如果你确实想复制 String 的堆内容,用 clone()。

fn main() {
  let s1 = String::from("hello");
  let s2 = s1.clone();
  // ↑ 深拷贝:堆上复制一份新数据,s1 和 s2 各自拥有自己的堆内存

  println!("{s1} {s2}"); // ✅ 都可用
}

经验:默认 move,想复制就显式 clone,避免不小心带来性能成本。

函数传参:move vs borrow(借用)

传入 String:默认 move

fn take(s: String) {
  // s 拥有传进来的 String
  println!("{s}");
} // <- s drop,释放堆内存

fn main() {
  let s1 = String::from("hi");
  take(s1);          // move 进去
  // println!("{s1}"); // ❌ s1 已经被 move
}

传入 &String&str:借用(不转移所有权)

fn read_only(s: &String) {
  // s 是对 String 的不可变借用(read-only)
  println!("{s}");
}
fn main() {
  let s1 = String::from("hi");
  read_only(&s1);     // 只借用
  println!("{s1}");   // ✅ 所有权仍在 s1
}
// 更推荐写成 &str(更通用):

fn read_only_str(s: &str) {
  println!("{s}");
}

fn main() {
  let s = String::from("hello");
  read_only_str(&s);       // &String 会自动解引用成 &str(Deref coercion)
  read_only_str("world");  // 字面量本身就是 &str
}

借用规则(Borrowing Rules)

核心两条(同一时间、同一份数据):

1. 可以有任意多个不可变借用(&T)

2. 或者只能有一个可变借用(&mut T

3. 可变借用与不可变借用不能同时存在

多个不可变借用:允许

fn main() {
  let s = String::from("hello");
  let r1 = &s; // 不可变借用
  let r2 = &s; // 另一个不可变借用
  println!("{r1} {r2}"); // ✅
}

可变借用:同一时间只能一个

fn main() {
  let mut s = String::from("hello");
  let r1 = &mut s;   // 可变借用:独占
  r1.push_str("!");
  println!("{r1}");
  // let r2 = &mut s; // ❌ 如果 r1 还在使用期间,再借用会报错
}

不可变借用与可变借用不能同时存在

fn main() {
  let mut s = String::from("hello");
  let r1 = &s;        // 不可变借用开始
  // let r2 = &mut s; // ❌ 同时想要可变借用:不允许
  println!("{r1}");   // r1 用到这里为止
}

返回值与所有权:move out / 借用返回

返回 String:把所有权交给调用者

fn make() -> String {
  let s = String::from("made");
  s // 返回:所有权 move 给调用者;这里不 drop
}

fn main() {
  let s = make();
  println!("{s}");
}

想返回引用:必须保证引用指向的数据活得足够久(生命周期)

下面这个是典型不允许的:

fn bad_ref() -> &str {
  let s = String::from("oops");
  // &s[..] 指向 s 的内部数据,但 s 函数结束就被 drop
  // 返回它会产生悬垂引用
  // &s[..]
  todo!()
}

正确方式通常是:

返回拥有所有权的值(String),或让引用来自调用者传入的数据(生命周期参数)

fn first_word<'a>(s: &'a str) -> &'a str {
  // 返回的切片引用来自参数 s,因此生命周期跟着 s
  match s.find(' ') {
      Some(i) => &s[..i],
      None => s,
  }
}

fn main() {
  let s = String::from("hello world");
  let w = first_word(&s);
  println!("{w}");
}

切片(Slice)与所有权:引用视图,不拥有数据

fn main() {
  let s = String::from("hello");
  let part = &s[0..2]; // &str 切片:借用 s 的一部分
  println!("{part}");

  // s.push('!');
  // ❌ 如果 part 仍在使用,push 可能触发扩容并搬移堆内存,
  // 导致 part 悬垂
}

容器与所有权:Vec / String 里装的东西是谁的

Vec 拥有它里面的 T

fn main() {
  let v = vec![String::from("a"), String::from("b")];
  // v 拥有两个 String
  // let x = v[0]; // ❌ 不能把 String 直接 move 出去
  //(索引返回引用视图的语义)
  let x = v[0].clone(); // ✅ 如果要拿一个独立 String,用 clone
  println!("{x}");

  // 更常见:用引用访问
  let r = &v[1];
  println!("{r}");
}

如果你确实要“拿走”元素,通常用 remove / pop(会改变 Vec)

fn main() {
  let mut v = vec![String::from("a"), String::from("b")];
  let x = v.remove(0); // ✅ move 出元素,同时 Vec 调整
  println!("{x}");
}

结构体字段与 move:部分 move(很常见的坑)

#[derive(Debug)]
struct User {
  name: String,
  age: u32,
}

fn main() {
  let u = User {
      name: String::from("Alice"),
      age: 20,
  };

  let name = u.name; // move 出 name 字段(String 非 Copy)
  // println!("{u:?}"); // ❌ u 已被“部分 move”,整体不能再用
  println!("{name}");

  // u.age 其实是 Copy,但由于 u 已部分 move,
  // 直接用 u.age 也会受限制
}

解决思路:

  1. 要么借用:let name = &u.name;
  2. 要么 clone:let name = u.name.clone();
  3. 要么用解构并只 move 需要的字段(并接受 u 之后不可用)

共享所有权(进阶):Rc/Arc + 内部可变性 RefCell/Mutex

本质

把“谁负责释放这个值”从编译期的唯一拥有者,改成运行时的引用计数/锁管理

实现

当你需要“多个地方同时拥有同一份数据”时,基础所有权规则不够,需要用标准库提供的

智能指针:

  1. 单线程共享所有权:Rc
  2. 多线程共享所有权:Arc
  3. 运行时借用检查(单线程):RefCell
  4. 多线程可变共享:Mutex / RwLock
Rc:单线程共享所有权(引用计数)
  • - Rc::clone(&rc)只增加引用计数,不复制数据
  • - 不能跨线程:Rc 不是 Send/Sync
  • - 只解决“多个 owner”,不解决可变共享
use std::rc::Rc;

fn main() {
  let s = Rc::new(String::from("shared"));
  let a = Rc::clone(&s); // 计数 +1
  let b = Rc::clone(&s); // 计数 +1
  // Rc<T> 支持像引用一样解引用读取
  println!("{}", a);
  println!("{}", b);
  // 当 a/b/s 都离开作用域后,引用计数归零,底层 String 才 drop
}
Rc + RefCell:单线程“可变共享”(经典组合)

因为 Rc 只能给出 &T(共享引用),你拿不到 &mut T;要想在多 owner 情况下修改数据,需要内部可变性

- RefCell:把借用检查从编译期推迟到运行时,规则仍然是:任意多个不可变借用或一个可变借用;但违反时会 panic

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
  // 多个 owner 共享同一个 Vec<i32>,并允许修改
  let data: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(vec![1, 2, 3]));

  let a = Rc::clone(&data);
  let b = Rc::clone(&data);

  // 可变借用:borrow_mut() 返回一个“借用守卫”,离开作用域才释放可变借用
  {
      let mut v = a.borrow_mut(); // 运行时检查:此刻必须没有其它借用
      v.push(4);
  } // v 在这里 drop,可变借用结束

  // 不可变借用:borrow()
  {
      let v = b.borrow(); // 运行时检查:此刻必须没有可变借用
      println!("{:?}", *v);
  }

  // 典型坑:同一作用域里借用守卫没 drop,就再借用会 panic
  // let _r1 = data.borrow_mut();
  // let _r2 = data.borrow_mut(); // ❌ 运行时 panic: already borrowed
}

选型建议(单线程):

- 只读共享:Rc

- 共享且要改:Rc<RefCell>

- 只改“Copy 小值”(如计数):Rc<Cell>(比 RefCell 更轻)

循环引用:Rc 的大坑,用 Weak 解决

Rc 是引用计数:如果 A 持有 B,B 又持有 A(强引用),计数永远不归零 → 内存泄漏(不会 drop)

解决:把“指回去”的那条边做成 弱引用Weak。

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
  // parent 不拥有孩子,避免环:Weak 不增加强计数
  parent: RefCell<Weak<Node>>,
  // children 拥有子节点:Rc 增加强计数
  children: RefCell<Vec<Rc<Node>>>,
  name: String,
}

fn main() {
  let root = Rc::new(Node {
      parent: RefCell::new(Weak::new()),
      children: RefCell::new(vec![]),
      name: "root".into(),
  });

  let child = Rc::new(Node {
      parent: RefCell::new(Weak::new()),
      children: RefCell::new(vec![]),
      name: "child".into(),
  });

  // 建立 root -> child(强引用)
  root.children.borrow_mut().push(Rc::clone(&child));

  // 建立 child -> root(弱引用),避免环
  *child.parent.borrow_mut() = Rc::downgrade(&root);

  // 需要访问 parent 时:upgrade() 把 Weak 尝试变成 Rc
  if let Some(p) = child.parent.borrow().upgrade() {
      println!("parent = {}", p.name);
  }
}

要点:

- Weak::upgrade() 返回 Option<Rc>:因为 parent 可能已经释放了

- 图结构(尤其双向边)几乎必用 Weak 来断环

Arc:多线程共享所有权(原子引用计数)

Arc 和 Rc 类似,但引用计数是原子的,可跨线程。

- Arc本身线程安全,但 T 是否能在线程间共享还要看 T: Send + Sync

- 仍然只解决“多个 owner”,不解决可变共享

use std::sync::Arc;
use std::thread;

fn main() {
  let s = Arc::new(String::from("hello"));

  let t1 = {
      let s = Arc::clone(&s);
      thread::spawn(move || {
          println!("{}", s);
      })
  };

  let t2 = {
      let s = Arc::clone(&s);
      thread::spawn(move || {
          println!("{}", s);
      })
  };

  t1.join().unwrap();
  t2.join().unwrap();
}
Arc<Mutex>:多线程“可变共享”(最常见)

- Mutex:同一时刻只允许一个线程修改/读取(独占)

- lock() 返回 MutexGuard,它 drop 时自动解锁(RAII)

- 典型坑:死锁锁持有太久poisoning(中毒)

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
  let counter = Arc::new(Mutex::new(0u64));
  let mut handles = vec![];
  for _ in 0..4 {
      let counter = Arc::clone(&counter);
      handles.push(thread::spawn(move || {
          for _ in 0..100_000 {
              // 加锁(可能阻塞)
              let mut guard = counter.lock().unwrap();
              *guard += 1;
              // guard 在这里离开作用域自动解锁
          }
      }));
  }
  for h in handles {
      h.join().unwrap();
  }
  println!("counter = {}", *counter.lock().unwrap());
}

实战建议:

- 把临界区写小:尽量让 MutexGuard 尽快 drop(用 {} 块包起来)

- 多把锁要固定顺序上锁,避免死锁

- unwrap() 在锁中毒时会 panic;更稳妥是处理 PoisonError

Arc<RwLock>:读多写少场景

- 多个读锁可并发,写锁独占

- 读多写少常更快;但写竞争时也可能更慢(具体看负载)

use std::sync::{Arc, RwLock};
use std::thread;

fn main() {
  let data = Arc::new(RwLock::new(vec![1, 2, 3]));
  let r = {
      let data = Arc::clone(&data);
      thread::spawn(move || {
          let guard = data.read().unwrap(); // 共享读锁
          println!("read = {:?}", *guard);
      })
  };
  let w = {
      let data = Arc::clone(&data);
      thread::spawn(move || {
          let mut guard = data.write().unwrap(); // 独占写锁
          guard.push(4);
      })
  };
  r.join().unwrap();
  w.join().unwrap();
}
无锁共享:Arc<Atomic*>(计数/标志位等)

当共享的数据是简单数值/状态机片段时,用原子类型避免锁开销:

- AtomicUsize, AtomicBool 等

- 需要理解内存序(Ordering);不确定就先用 SeqCst(简单但可能慢)

use std::sync::{
  atomic::{AtomicUsize, Ordering},
  Arc,
};
use std::thread;

fn main() {
  let n = Arc::new(AtomicUsize::new(0));

  let mut handles = vec![];
  for _ in 0..4 {
      let n = Arc::clone(&n);
      handles.push(thread::spawn(move || {
          for _ in 0..100_000 {
              n.fetch_add(1, Ordering::SeqCst);
          }
      }));
  }

  for h in handles {
      h.join().unwrap();
  }

  println!("n = {}", n.load(Ordering::SeqCst));
}
总结(选型速记

- 单线程、只读共享:Rc

- 单线程、共享且要改:Rc<RefCell>(或 Rc<Cell>)

- 多线程、只读共享:Arc

- 多线程、共享且要改:Arc<Mutex>(简单通用)或 Arc<RwLock>(读多写少)

- 多线程、简单计数/标志:Arc<Atomic*>

- 有双向/环状引用:Weak 断环(Rc/Arc 都适用:Weak / sync::Weak)