首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >23-Rust 教程 - 内部可变性

23-Rust 教程 - 内部可变性

作者头像
LarryLan
发布2026-05-20 10:24:50
发布2026-05-20 10:24:50
1040
举报

内部可变性

RefCell 和 Cell:我虽然是 immutable,但我能改!

🎬 引入

Rust 的所有权规则里有条铁律:不可变引用不能修改数据

但有一天,你写了个结构体:

代码语言:javascript
复制
struct Cache {
    data: Vec<String>,
}

impl Cache {
    fn get(&self, key: &str) -> Option<&String> {
        // 只是想查个缓存...
        // 但顺便想记录一下访问次数...
        // 等等,self 是不可变借用,不能改啊!
    }
}

你: "我就想记个访问次数,咋就不行了?"

Rust: "规则就是规则,&self 不能改!"

你: "可这数据明明是我自己的啊!"

这时候,内部可变性登场了。它就像 Rust 规则里的"后门":外面看着是不可变的,但里面偷偷能改。

今天咱们就来看看这个"精神分裂"的玩法。

📌 核心概念

什么是内部可变性?

外部不可变,内部可变。

就像个存钱罐

  • 外面看:存钱罐本身不能变(还是那个存钱罐)
  • 里面看:钱可以存进去、取出来(内部数据在变)
代码语言:javascript
复制
use std::cell::RefCell;

struct Counter {
    value: RefCell<i32>,  // 内部可变
}

impl Counter {
    fn new() -> Self {
        Counter {
            value: RefCell::new(),
        }
    }
    
    fn increment(&self) {  // &self 不可变借用
        *self.value.borrow_mut() += ;  // 但里面能改!
    }
    
    fn get(&self) -> i32 {
        *self.value.borrow()
    }
}

fn main() {
    let counter = Counter::new();
    counter.increment();  // &self 调用
    counter.increment();  // 还能调用
    println!("计数:{}", counter.get());  // 输出:2
}

看到了吗? increment 用的是 &self(不可变借用),但居然能修改 value!这就是内部可变性的魔法。

借用检查器:编译时 vs 运行时

Rust 的借用检查通常在编译时进行:

代码语言:javascript
复制
fn main() {
    let mut x = ;
    let r1 = &x;
    let r2 = &x;
    // let m1 = &mut x;  // ❌ 编译错误!
}

RefCell 把检查挪到了运行时

代码语言:javascript
复制
use std::cell::RefCell;

fn main() {
    let x = RefCell::new();
    
    let r1 = x.borrow();  // 不可变借用
    let r2 = x.borrow();  // 还可以
    
    // let m1 = x.borrow_mut();  // ❌ 运行时 panic!
    
    println!("r1={}, r2={}", *r1, *r2);
}

编译时检查 vs 运行时检查:

特性

编译时检查

运行时检查 (RefCell)

检查时机

编译阶段

程序运行

性能

零开销

有小开销

错误处理

编译错误

panic

灵活性

严格

灵活

RefCell:运行时借用的核心

RefCell 的人设: "编译时我管不了,运行时我说了算。"

核心方法:

  • borrow() → 不可变借用(返回 Ref<T>
  • borrow_mut() → 可变借用(返回 RefMut<T>
  • get_mut() → 直接获取可变引用(需要 &mut self
代码语言:javascript
复制
use std::cell::RefCell;

fn main() {
    let data = RefCell::new();
    
    // 不可变借用
    let r = data.borrow();
    println!("值:{}", *r);
    // r 离开作用域,借用结束
    
    // 可变借用
    let mut m = data.borrow_mut();
    *m = ;
    // m 离开作用域,借用结束
    
    println!("新值:{}", *data.borrow());  // 输出:10
}

借用规则(运行时检查):

  • 可以有多个不可变借用
  • 但只能有一个可变借用
  • 可变借用时不能有其他借用
代码语言:javascript
复制
use std::cell::RefCell;

fn main() {
    let data = RefCell::new();
    
    let r1 = data.borrow();
    let r2 = data.borrow();  // ✅ 多个不可变
    
    // let m = data.borrow_mut();  // ❌ panic!
    
    drop(r1);
    drop(r2);
    
    let m = data.borrow_mut();  // ✅ 现在可以了
    *m = ;
}

Cell:简单值的内部可变

Cell 的人设: "我只存简单类型,但我也能改。"

RefCell vs Cell:

  • RefCell<T> → 返回借用,可以多次访问
  • Cell<T> → 拷贝值,适合简单类型
代码语言:javascript
复制
use std::cell::Cell;

fn main() {
    let value = Cell::new();
    
    value.set();  // 修改
    let v = value.get();  // 获取(拷贝)
    
    println!("值:{}", v);  // 输出:10
}

Cell 的限制:

  • 只能存 Copy 类型(数字、布尔等)
  • 不能借用,只能拷贝
代码语言:javascript
复制
use std::cell::Cell;

fn main() {
    let c = Cell::new();
    
    // ❌ 不能借用
    // let r = c.borrow();
    
    // ✅ 只能拷贝
    let v = c.get();
    c.set();
}

RefCell + Rc:共享 + 可变

这就是上篇树形结构里的组合:

代码语言:javascript
复制
use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct TreeNode {
    value: i32,
    children: RefCell<Vec<Rc<TreeNode>>>,
}

fn main() {
    let root = Rc::new(TreeNode {
        value: ,
        children: RefCell::new(vec![]),
    });
    
    // 可以修改 children,即使 root 是不可变的
    root.children.borrow_mut().push(
        Rc::new(TreeNode {
            value: ,
            children: RefCell::new(vec![]),
        })
    );
    
    println!("根节点:{:?}", root);
}

为什么需要这个组合?

  • Rc → 共享所有权(多个人拥有)
  • RefCell → 内部可变(共享时还能修改)

💻 代码示例

示例 1:RefCell 基本使用

代码语言:javascript
复制
use std::cell::RefCell;

fn main() {
    let data = RefCell::new(vec![, , ]);
    
    // 不可变借用读取
    println!("原始:{:?}", data.borrow());
    
    // 可变借用修改
    data.borrow_mut().push();
    data.borrow_mut().push();
    
    println!("修改后:{:?}", data.borrow());
    // 输出:[1, 2, 3, 4, 5]
}

示例 2:Cell 的简单可变

代码语言:javascript
复制
use std::cell::Cell;

fn main() {
    let counter = Cell::new();
    
    for _ in .. {
        let current = counter.get();
        counter.set(current + );
    }
    
    println!("计数:{}", counter.get());  // 输出:5
}

示例 3:结构体中的 RefCell

代码语言:javascript
复制
use std::cell::RefCell;

struct User {
    name: String,
    login_count: RefCell<u32>,  // 内部可变
}

impl User {
    fn new(name: &str) -> Self {
        User {
            name: name.to_string(),
            login_count: RefCell::new(),
        }
    }
    
    fn login(&self) {  // &self 不可变
        let mut count = self.login_count.borrow_mut();
        *count += ;
        println!("{} 第 {} 次登录", self.name, *count);
    }
    
    fn get_login_count(&self) -> u32 {
        *self.login_count.borrow()
    }
}

fn main() {
    let user = User::new("Larry");
    user.login();  // &self 调用
    user.login();
    user.login();
    
    println!("总登录次数:{}", user.get_login_count());
}
// 输出:
// Larry 第 1 次登录
// Larry 第 2 次登录
// Larry 第 3 次登录
// 总登录次数:3

示例 4:运行时借用检查 panic

代码语言:javascript
复制
use std::cell::RefCell;

fn main() {
    let data = RefCell::new();
    
    let r1 = data.borrow();
    let r2 = data.borrow();
    
    // 这时候再要可变借用就会 panic
    let result = std::panic::catch_unwind(|| {
        let mut m = data.borrow_mut();  // ❌ panic!
        *m = ;
    });
    
    if result.is_err() {
        println!("捕获到 panic:同时存在不可变和可变借用!");
    }
    
    // 释放所有借用后可以正常操作
    drop(r1);
    drop(r2);
    *data.borrow_mut() = ;
    println!("新值:{}", *data.borrow());
}

示例 5:Weak 打破循环引用

代码语言:javascript
复制
use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Option<Weak<Node>>>,  // 弱引用父节点
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let root = Rc::new(Node {
        value: ,
        parent: RefCell::new(None),
        children: RefCell::new(vec![]),
    });
    
    let child = Rc::new(Node {
        value: ,
        parent: RefCell::new(Some(Rc::downgrade(&root))),  // 弱引用
        children: RefCell::new(vec![]),
    });
    
    // 添加子节点
    root.children.borrow_mut().push(Rc::clone(&child));
    
    println!("root 计数:{}", Rc::strong_count(&root));  // 1
    println!("child 计数:{}", Rc::strong_count(&child));  // 1
    
    // 可以通过弱引用访问父节点
    if let Some(parent) = child.parent.borrow().upgrade() {
        println!("child 的父节点值:{}", parent.value);
    }
}

Weak 的特点:

  • 不影响引用计数
  • 需要 upgrade() 获取 Rc
  • 如果 Rc 已经释放,upgrade() 返回 None

错误示例:借用冲突

代码语言:javascript
复制
use std::cell::RefCell;

fn main() {
    let data = RefCell::new();
    
    let r = data.borrow();
    let m = data.borrow_mut();  // ❌ 运行时 panic!
    
    println!("值:{}", *m);
}

运行时错误:

代码语言:javascript
复制
thread 'main' panicked at library/std/src/cell.rs:1631:25:
already borrowed: BorrowMutError

翻译: "已经有人在借了,你还要可变借用?想都别想!"

🐛 常见坑点

坑点 1:忘记释放借用

代码语言:javascript
复制
use std::cell::RefCell;

fn main() {
    let data = RefCell::new(vec![, , ]);
    
    let r = data.borrow();
    // 忘记 drop(r)
    
    let m = data.borrow_mut();  // ❌ panic! r 还在借用
    
    // ✅ 正确做法
    {
        let r = data.borrow();
        println!("{:?}", *r);
    }  // r 在这里释放
    
    let m = data.borrow_mut();  // ✅ 现在可以了
}

坑点 2:在循环中借用

代码语言:javascript
复制
use std::cell::RefCell;

fn main() {
    let data = RefCell::new(vec![, , , , ]);
    
    // ❌ 错误:借用跨越循环
    let borrowed = data.borrow();
    for i in .. {
        if i ==  {
            data.borrow_mut().clear();  // ❌ panic!
        }
    }
    
    // ✅ 正确:借用限制在作用域内
    for i in .. {
        if i ==  {
            data.borrow_mut().clear();
        } else {
            println!("{}", data.borrow()[i]);
        }
    }
}

坑点 3:Cell 存非 Copy 类型

代码语言:javascript
复制
use std::cell::Cell;

fn main() {
    // ❌ 不能存 String(非 Copy)
    // let c = Cell::new(String::from("hello"));
    
    // ✅ 只能存 Copy 类型
    let c = Cell::new();
    let c2 = Cell::new(true);
}

🎯 实战案例

案例 1:缓存系统

代码语言:javascript
复制
use std::cell::RefCell;
use std::collections::HashMap;

struct Cache {
    data: RefCell<HashMap<String, String>>,
}

impl Cache {
    fn new() -> Self {
        Cache {
            data: RefCell::new(HashMap::new()),
        }
    }
    
    fn get_or_insert(&self, key: &str, value: &str) -> &str {
        // 先检查有没有
        {
            let cache = self.data.borrow();
            if let Some(v) = cache.get(key) {
                return v;
            }
        }
        
        // 没有就插入
        {
            let mut cache = self.data.borrow_mut();
            cache.insert(key.to_string(), value.to_string());
        }
        
        // 返回刚插入的值
        let cache = self.data.borrow();
        cache.get(key).unwrap()
    }
    
    fn stats(&self) -> usize {
        self.data.borrow().len()
    }
}

fn main() {
    let cache = Cache::new();
    
    println!("{}", cache.get_or_insert("name", "Larry"));
    println!("{}", cache.get_or_insert("name", "Larry"));  // 命中缓存
    println!("{}", cache.get_or_insert("age", "25"));
    
    println!("缓存条目数:{}", cache.stats());
}

案例 2:观察者模式

代码语言:javascript
复制
use std::cell::RefCell;
use std::rc::Rc;

trait Observer {
    fn update(&self, value: i32);
}

struct Subject {
    value: i32,
    observers: RefCell<Vec<Rc<dyn Observer>>>,
}

impl Subject {
    fn new(value: i32) -> Self {
        Subject {
            value,
            observers: RefCell::new(vec![]),
        }
    }
    
    fn attach(&self, observer: Rc<dyn Observer>) {
        self.observers.borrow_mut().push(observer);
    }
    
    fn set_value(&self, value: i32) {
        self.value = value;
        // 通知所有观察者
        for observer in self.observers.borrow().iter() {
            observer.update(value);
        }
    }
}

struct Logger;

impl Observer for Logger {
    fn update(&self, value: i32) {
        println!("日志:值变为 {}", value);
    }
}

fn main() {
    let subject = Rc::new(Subject::new());
    let logger = Rc::new(Logger);
    
    subject.attach(logger);
    subject.set_value();
    subject.set_value();
}
// 输出:
// 日志:值变为 10
// 日志:值变为 20

🧠 思维导图

23-内部可变性
23-内部可变性

📝 小结

核心要点:

  1. 内部可变性:外面不可变,里面偷偷改,Rust 的"后门"
  2. RefCell:运行时借用检查,灵活但有 panic 风险
  3. Cell:简单类型的内部可变,只能拷贝不能借用
  4. RefCell + Rc:共享所有权的还能修改,树形结构必备
  5. Weak:弱引用打破循环引用,防止内存泄漏

选择指南:

  • 需要借用 → RefCell
  • 只是简单值 → Cell
  • 共享 + 可变 → Rc<RefCell<T>>
  • 循环引用 → 用 Weak

下篇预告:

内部可变性玩够了,咱们来聊聊 Rust 的高级 Trait。关联类型是啥?默认泛型参数怎么用?特征对象又是啥黑科技?下篇一起揭开 Trait 的高级玩法!

互动问题:

你觉得内部可变性是 Rust 的"漏洞"还是"特性"?有没有被运行时 panic 坑过?评论区聊聊!

🔗 参考资料

  • Rust Book - Interior Mutability
  • std::cell::RefCell
  • std::cell::Cell
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Larry的Hub 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 内部可变性
    • 🎬 引入
    • 📌 核心概念
      • 什么是内部可变性?
      • 借用检查器:编译时 vs 运行时
      • RefCell:运行时借用的核心
      • Cell:简单值的内部可变
      • RefCell + Rc:共享 + 可变
    • 💻 代码示例
      • 示例 1:RefCell 基本使用
      • 示例 2:Cell 的简单可变
      • 示例 3:结构体中的 RefCell
      • 示例 4:运行时借用检查 panic
      • 示例 5:Weak 打破循环引用
      • 错误示例:借用冲突
    • 🐛 常见坑点
      • 坑点 1:忘记释放借用
      • 坑点 2:在循环中借用
      • 坑点 3:Cell 存非 Copy 类型
    • 🎯 实战案例
      • 案例 1:缓存系统
      • 案例 2:观察者模式
    • 🧠 思维导图
    • 📝 小结
    • 🔗 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档