< 返回版块

tbgwe 发表于 2025-11-13 10:13

Tags:rust悬垂引用规则恐怖到设计者可能都无法解释下面出现的情况

let a = Rc::new(RefCell::new(&String::from("tbg"))); let s=RefCell::new(String::from("qaz")). borrow(); 这两个语句都造成了悬垂引用,但是一个语句报错,一个语句不报错,为什么?所有的AI大模型全部答错,请大佬看看哪个会在编译期报错,哪个不会在编译期报错,帮我解答一下,感谢!!!

评论区

写评论
作者 tbgwe 2025-11-16 01:06

你是对的,,我现在终于弄明白了,包括函数调用返回内部数据引用为什么直接报错,因为函数就相当于一个供所有开发人员的接口,编译器要保证返回的值至少在后续可以使用,这是一种设计理念,不管谁调用这个函数,不管后续是否使用,至少让调用者使用时是一定有效的,所有必须保守限定不能对函数内局部数据进行引用返回;还有对一些常量进行直接引用或嵌套在容器中直接引用,后续对其绑定的变量进行访问,也不会报错,这些常量包括数值,字符,布尔值,以及上述类型组合形成的数组,元组类型,因为这些类型默认是静态数据,在整个程序生命周期都有效,,当然对临时值直接进行引用绑定变量,会延长该临时值生命周期,,,

--
👇
xiaoyaou: 这样的话,那前面的理解就错了。

let s = RefCell::new(String::from("qaz")).borrow();

这里实际也是涉及了NLL(非词法生命周期),只不过由于Ref<'_, T>需要析构,改变了s最后使用的位置,所以才造成的生命周期冲突。不绑定s变量的话,语句结束作用域就结束了,也就没有冲突问题了

--
👇

let s = RefCell::new(String::from("qaz")).borrow();

这里会直接报错,关键点在于变量s是类型RefCell<'_, T>的值,它只是一个含有特定生命周期的值类型,不涉及非词法生命周期,所以其生命周期就是其作用域。基于类型系统的生命周期约束,这里要求临时变量的生命周期在变量s的作用域内始终有效,显然是不可能的。因此,只要返回值被绑定到变量,就无法通过借用检查,而在把s改为_或者直接移除s绑定后,就能恢复正常。

Mayrixon 2025-11-14 16:12

let s = &String::from("something");触发了Temporary Lifetime Extension,The Rust Reference中记录了相关的规则,但是也声明了这些规则在修改中。

这篇文章来自于Rust语言团队成员,有更详细的介绍,甚至例子就是String::from()

xiaoyaou 2025-11-14 11:43

这样的话,那前面的理解就错了。

let s = RefCell::new(String::from("qaz")).borrow();

这里实际也是涉及了NLL(非词法生命周期),只不过由于Ref<'_, T>需要析构,改变了s最后使用的位置,所以才造成的生命周期冲突。不绑定s变量的话,语句结束作用域就结束了,也就没有冲突问题了

--
👇

let s = RefCell::new(String::from("qaz")).borrow();

这里会直接报错,关键点在于变量s是类型RefCell<'_, T>的值,它只是一个含有特定生命周期的值类型,不涉及非词法生命周期,所以其生命周期就是其作用域。基于类型系统的生命周期约束,这里要求临时变量的生命周期在变量s的作用域内始终有效,显然是不可能的。因此,只要返回值被绑定到变量,就无法通过借用检查,而在把s改为_或者直接移除s绑定后,就能恢复正常。

xiaoyaou 2025-11-14 11:27

确实是Drop隐含的析构行为影响了生命周期范围

struct Ref<'a, T> {
    ptr: *const T,
    _marker: PhantomData<&'a ()>,
}

fn as_ref<'a, T: Debug>(t: &'a Test<T>) -> Ref<'a, T> {
    Ref {
        ptr: &t.t,
        _marker: PhantomData,
    }
}

let x: Ref<'_, i32> = as_ref(&Test { t: 5 });

测试代码里,Ref没有实现Drop时,x能够正常绑定,只是不能继续使用; 为它实现Drop后:

impl<'a, T> Drop for Ref<'a, T> {
    fn drop(&mut self) {
        // 什么也不需要做
    }
}

变量x就会在它作用域末尾插入drop调用,从而需要把x的生命周期拉到了整个作用域的范围, 因为正常的Drop是要求引用完全有效的,不过编译器也开了一个口子:

unsafe impl<#[may_dangle] 'a, T> Drop for Ref<'a, T> {
    fn drop(&mut self) {
        // 允许'a生命周期在这里悬垂,因为不需要使用到它
        // 可以做其他统计工作
    }
}

这样的话,即使实现了Dropx也能正常绑定,因为它的析构drop不再依赖指定的生命周期了

作者 tbgwe 2025-11-14 01:47

哈哈,这个想法我前段时间也是这么认为的,以为其实本质都是使用了变量就会报错,不管是隐式还是显式,都可能是因为使用才会报错,没使用编译器就暂时睁一只眼闭一只眼,,,后来我今天发现这个思路也有问题,就比如如果是函数返回其内部引用数据,外部变量接收它,其实这也相当于悬垂引用了,但是外部变量未使用就不会报错,但实际情况是函数返回引用内部数据类型就直接报错了,根本不会再外部变量使用时再报错,,我现在最新的理解是,,比如let a = RefCell::new(&String::new())这个例子,并没有生命周期延长,因为如果延长,后续使用变量a,就不会报错,实际情况是使用它就报错,那么只能说明这个例子在写出后,编译器并没有进行生命周期检查,因为这个类型RefCell并没有生命周期标签,编译器压根就不知道其内部T的情况,就不会对内部T进行检查,而 let a = RefCell::new(32).borrow()返回Ref<'-,T>类型带有生命周期标签,编译器就能根据这个标签立即对其内部进行检查,所以写出这个代码就会立即检查报错,,,而函数返回引用类型几乎都要带生命周期标签,编译器能立即进行检查,所以不需要后续外部接收变量读写才报错,,类似容器类型包裹内部引用类型的,因为都不带生命周期标签,编译器都无法探测内部T生命周期,就无法检查,所以一开始不报错,但是一旦后续进行读写,就能发现数据销毁了,所以再读写时才报错,,,我现在的理解就是但凡一个容器类型嵌套引用数据,因容器本身不带生命周期标签,编译器都不会检查内部引用数据生命周期,只有后续使用才会发现问题,一个类型带有生命周期标签,编译器能立即识别检查,马上报错,,,,而对于临时值引用生命周期延长,多半是类似于栈上的直接引用,例如let s=&String::from("tbg");后续使用s也不会报错

👇
TinusgragLin: 我之前翻 Reference 的时候有点印象,感觉和这里提到的 Temporary Lifetime Extension 有关,let s=&String::from("tbg"); 能过应该就是符合它的规则,编译器进行了生命周期扩展,而函数入参(包括 self/&self/&mut self 这些)处不是,所以像 let a = RefCell::new(&String::new()); a;let a = RefCell::new(32).borrow() 都会报错,这两个中前一个需要使用 a 才报错,大概是因为完全没有使用的垂悬引用被认为是安全的,而后一个例子应该是因为 RefCell::borrow 返回的 Ref 类型实现了 Drop,所以在最后面有一个隐藏的对 a 的使用,所以直接就报错了。

xiaoyaou 2025-11-13 18:18

临时生命周期和借用检查这块确实乱的一比,不过应该还不至于太恐怖吧哈哈哈。

简单说下我的理解吧,这里需要引入两个概念:非词法生命周期(non-lexical lifetime)和临时生命周期延长(temporary lifetime extension)。


非词法生命周期:在该规则出现之前,像let a = &b;这样的本地变量持有某个引用,借用时间都会持续到变量离开作用域为止,这显然会大大限制借用的多样性和灵活性。所以有了非词法生命周期,就是为了限制借用只持续到变量的最后一次使用即可,按需检查。

临时生命周期延长:正常情况下,临时变量的作用域和生命周期都是限制在当前语句(Statement)结束前有效。然而凡事都有例外,毕竟“偷懒”才是发展的一大动力:如果只是想要多用一会临时变量,却发现每次不得不强制多写几个let语句显式绑定仅临时使用变量,这显然还是“过于繁琐”了。

所以有了临时生命周期延长,就是为了在某些场景下可以方便地延长临时变量有效期。想要触发这个规则就要满足特定要求,毕竟“临时生命周期”终究还是“不稳定”的,所以这就需要有一个可靠的“锚点”来绑定这个临时生命周期。而这个锚点就是栈上的位置(placement):既“临时”又“稳定”。


下面解释一下你说的代码:

let a = Rc::new(RefCell::new(&String::from("tbg"))); // 编译通过
// a.borrow(); // temporary value dropped while borrowed

这里就是普通的临时变量的使用,临时创建了一个字符串及其引用,语句结束后就失效了,同时a也不再有效,所以后续再使用a就会不满足借用检查,这里涉及了非词法生命周期:a只能最后一次出现在当前语句,后续的使用会导致引用生命周期与临时变量的生命周期发生冲突。

let s = RefCell::new(String::from("qaz")).borrow();

这里会直接报错,关键点在于变量s是类型RefCell<'_, T>的值,它只是一个含有特定生命周期的值类型,不涉及非词法生命周期,所以其生命周期就是其作用域。基于类型系统的生命周期约束,这里要求临时变量的生命周期在变量s的作用域内始终有效,显然是不可能的。因此,只要返回值被绑定到变量,就无法通过借用检查,而在把s改为_或者直接移除s绑定后,就能恢复正常。

let s=&String::from("tbg");
let len = s.len(); // Ok

这里是有效且能正常使用变量s的,这就是前面提到的临时生命周期延长,临时变量有效期延长至绑定“锚点”的整个作用域内。这里的“锚点”顾名思义,是需要能够 真正锚定 临时变量不失效的:

struct Wrap<'a> {
    t: &'a String
}

let a = &String::from(""); // a.len() is Ok
let b = Wrap { t: &String::from("")}; // b.t.len() is Ok
let c = [&String::from("")]; // c[0].len() is Ok
let d = (0, &String::from("")); // d.1.len() is Ok

// 以上都是直接锚定在当前栈上,都满足扩展要求

fn wrong(r: &String) -> &String {
    r
}
let a = wrong(&String::from("")); // a.len() is Err
let b = Wrap { t: wrong(&String::from(""))}; // b.t.len() is Err
...
// 这里通过wrong形参传递临时变量引用,但形参显然不够“稳定”,无法满足扩展要求

关于临时变量这块的,可以看看这篇文章,网上也有不少中文版的

--
👇
tbgwe: 这个claude解释存在问题哈,,而且有致命漏洞,就像它说的,借用类型立即检查生命周期,我现在给个反例,let s=&String::from("tbg");就这个语句,s是借用类型吧,那么按照它说的,就应该进行立即检查,这个语句按理说就会报错,因为悬垂引用了,又是借用类型,就直接应该报错,但是语句没报错,而且后续还能读写s,,所以rust的借用检查规则不是向它解释得那么简单

TinusgragLin 2025-11-13 18:18

我之前翻 Reference 的时候有点印象,感觉和这里提到的 Temporary Lifetime Extension 有关,let s=&String::from("tbg"); 能过应该就是符合它的规则,编译器进行了生命周期扩展,而函数入参(包括 self/&self/&mut self 这些)处不是,所以像 let a = RefCell::new(&String::new()); a;let a = RefCell::new(32).borrow() 都会报错,这两个中前一个需要使用 a 才报错,大概是因为完全没有使用的垂悬引用被认为是安全的,而后一个例子应该是因为 RefCell::borrow 返回的 Ref 类型实现了 Drop,所以在最后面有一个隐藏的对 a 的使用,所以直接就报错了。

作者 tbgwe 2025-11-13 14:55

我觉得这个生命周期检查存在很多对悬垂引用不一致的检查规则,导致有些情况报错,有些情况不报错,为了以后写代码的规范,但凡涉及到涉嫌悬垂引用,不管编译器报不报错,都主动尽量不写临时值,即使写了临时值,尽量不要直接对临时值进行引用

xiaoyaou 2025-11-13 14:08

哎呦喂,你们回复的时候能不能简化一下引用[笑哭]。社区站点功能本来就简陋,你们还一直套娃,我滚轮都转冒烟了要,差的没分清哪是哪

--
👇
tbgwe: 这个claude解释存在问题哈,,而且有致命漏洞,就像它说的,借用类型立即检查生命周期,我现在给个反例,let s=&String::from("tbg");就这个语句,s是借用类型吧,那么按照它说的,就应该进行立即检查,这个语句按理说就会报错,因为悬垂引用了,又是借用类型,就直接应该报错,但是语句没报错,而且后续还能读写s,,所以rust的借用检查规则不是向它解释得那么简单

作者 tbgwe 2025-11-13 12:01

这个claude解释存在问题哈,,而且有致命漏洞,就像它说的,借用类型立即检查生命周期,我现在给个反例,let s=&String::from("tbg");就这个语句,s是借用类型吧,那么按照它说的,就应该进行立即检查,这个语句按理说就会报错,因为悬垂引用了,又是借用类型,就直接应该报错,但是语句没报错,而且后续还能读写s,,所以rust的借用检查规则不是向它解释得那么简单

--
👇
iduanyingjie: claude生成的啊

--
👇
tbgwe: 大佬,谢谢,有点明白了,这个知识你是从哪里找到的

--
👇
iduanyingjie: # Rust 借用检查:三种情况对比分析

概述

本文档通过三个具体例子,深入分析 Rust 编译器的借用检查机制,揭示所有权类型借用类型在生命周期检查上的根本差异。


情况对比表

情况 代码 编译结果 原因
情况1 let _a = Rc::new(RefCell::new(&String::from("tbg"))); ✅ 通过 所有权类型,未使用不检查
情况2 let _a = Rc::new(RefCell::new(&String::from("tbg")));<br>println!("{}", *_a.borrow()); ❌ 报错 使用时检查,发现悬垂引用
情况3 let _s = RefCell::new(String::from("qaz")).borrow(); ❌ 报错 借用类型,存在即要求生命周期

情况1:所有权类型 + 未使用

代码

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

fn main() {
    let _a = Rc::new(RefCell::new(&String::from("tbg")));
    // 没有任何使用 _a 的代码
}

编译结果

编译通过(但存在潜在的悬垂引用)

详细分析

let _a = Rc::new(RefCell::new(&String::from("tbg")));
//                             |<--临时值-->|-- 在此销毁
//       |<--------Rc 拥有 RefCell---------->|
//
// 类型:Rc<RefCell<&String>>

为什么通过?

  1. Rc::new() 返回 Rc<RefCell<&String>>,是一个拥有所有权的值
  2. Rc 本身是有效的,它成功分配了内存并存储了 RefCell
  3. 虽然内部的 &String 指向已销毁的临时值,但编译器采用按需检查策略
  4. 因为 _a 从未被使用,编译器将其视为死代码,不进行深度借用检查

关键点

  • 所有权类型(RcBoxVec 等)在未使用时不触发内部借用检查
  • 这是编译器的优化策略,允许死代码存在

情况2:所有权类型 + 使用

代码

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

fn main() {
    let _a = Rc::new(RefCell::new(&String::from("tbg")));
    println!("{}", *_a.borrow());  // 尝试使用
}

编译结果

编译失败

error[E0716]: temporary value dropped while borrowed
 --> src/main.rs:5:36
  |
5 |     let _a = Rc::new(RefCell::new(&String::from("tbg")));
  |                                    ^^^^^^^^^^^^^^^^^^^  - temporary value is freed at the end of this statement
  |                                    |
  |                                    creates a temporary value which is freed while still in use
6 |     println!("{}", *_a.borrow());
  |                     -- borrow later used here

详细分析

let _a = Rc::new(RefCell::new(&String::from("tbg")));
//                             |<--临时值-->|-- 在此销毁
println!("{}", *_a.borrow());
//              ^^ 尝试访问已销毁的临时值 → 悬垂引用!

为什么报错?

  1. *_a.borrow() 尝试解引用并访问内部的 &String
  2. 编译器必须验证这个引用是否有效
  3. 检查发现 &String 指向的 String::from("tbg") 已在第5行结束时销毁
  4. 这会导致未定义行为,编译器必须阻止

对比情况1

  • 情况1:不使用 → 死代码 → 不检查
  • 情况2:使用 → 必须检查 → 发现悬垂引用 → 报错

正确写法

let s = String::from("tbg");           // s 拥有 String
let _a = Rc::new(RefCell::new(&s));    // RefCell 借用 s
println!("{}", *_a.borrow());          // s 还活着,引用有效

情况3:借用类型的生命周期冲突

代码

use std::cell::RefCell;

fn main() {
    let _s = RefCell::new(String::from("qaz")).borrow();
    // 即使不使用 _s,也会报错
}

编译结果

编译失败

error[E0716]: temporary value dropped while borrowed
 --> src/main.rs:4:14
  |
4 |     let _s = RefCell::new(String::from("qaz")).borrow(); 
  |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^         - temporary value is freed at the end of this statement
  |              |
  |              creates a temporary value which is freed while still in use
5 | }
  | - borrow might be used here, when `_s` is dropped and runs the destructor for type `Ref<'_, String>`

详细分析

let _s = RefCell::new(String::from("qaz")).borrow();
//       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^
//       临时 RefCell                       返回 Ref<'a, String>
//       |<--在此销毁-->|
//                                         |<--_s 存活到作用域结束-->|
//
// 类型:Ref<'_, String>  (注意生命周期参数)

为什么报错?(关键!)

这与情况1和2完全不同:

  1. Ref<'a, String> 本身就是一个借用类型
  2. RefRefCell 的借用守卫(borrow guard),它持有对 RefCell 的运行时借用
  3. Ref 的存在本身就要求 RefCell 必须保持有效
  4. 这不是"是否使用"的问题,而是类型系统的约束

生命周期冲突

临时 RefCell 的生命周期:|<---第4行--->|
_s (Ref) 的生命周期:     |<---第4行到第5行--->|
                          ^^^^^^^^ 冲突!

与情况1的本质区别

特性 情况1: _a 情况3: _s
类型性质 Rc<...> 所有权类型 Ref<'a, ...> 借用类型
是否"使用" 未使用内部值 类型本身就是借用
检查时机 按需检查(使用时) 立即检查(创建时)
生命周期 Rc 自己管理生命周期 'a 表示依赖外部生命周期

正确写法

let cell = RefCell::new(String::from("qaz"));  // cell 拥有 RefCell
let _s = cell.borrow();                        // _s 借用 cell
// cell 活着 → _s 有效

核心概念总结

1. 所有权类型 vs 借用类型

// 所有权类型(拥有数据)
let owned: Rc<T> = ...;        // 自己管理生命周期
let owned: Box<T> = ...;       // 自己管理生命周期
let owned: Vec<T> = ...;       // 自己管理生命周期

// 借用类型(依赖其他数据)
let borrowed: &T = ...;        // 依赖被引用对象的生命周期
let borrowed: Ref<'a, T> = ...; // 依赖 RefCell 的生命周期
let borrowed: &mut T = ...;    // 依赖被引用对象的生命周期

2. 编译器的检查策略

所有权类型未使用 → 死代码 → 不深度检查内部借用
                   ↓
所有权类型被使用 → 检查使用点 → 验证内部借用有效性
                   ↓
借用类型(无论是否使用) → 立即检查 → 验证被借用对象生命周期

3. 生命周期依赖关系

// 情况1和2:Rc 不依赖外部
Rc<RefCell<&String>>
└─ Rc 拥有 RefCell
   └─ RefCell 包含 &String(悬垂,但未使用时不检查)

// 情况3:Ref 依赖外部
Ref<'a, String>
 ^^ 这个生命周期参数表示:Ref 的有效期不能超过被借用的 RefCell

实践建议

✅ 推荐做法

// 1. 让被借用对象有足够长的生命周期
let s = String::from("data");
let _a = Rc::new(RefCell::new(&s));

// 2. 或者直接存储所有权类型,避免引用
let _a = Rc::new(RefCell::new(String::from("data")));

// 3. 确保 RefCell 在借用期间存活
let cell = RefCell::new(String::from("data"));
let _s = cell.borrow();

❌ 常见错误

// 错误1:临时值 + 引用
let _a = Rc::new(RefCell::new(&String::from("temp")));
println!("{}", *_a.borrow());  // 悬垂引用

// 错误2:临时 RefCell + borrow
let _s = RefCell::new(String::from("temp")).borrow();  // 生命周期冲突

// 错误3:作用域问题
let _s = {
    let cell = RefCell::new(String::from("temp"));
    cell.borrow()  // cell 离开作用域,_s 悬垂
};

类比总结

用现实世界的类比理解这三种情况:

情况 类比 结果
情况1 你买了个保险箱(Rc),里面放了张欠条(&String),但从不打开看 ✅ 没人管
情况2 你买了个保险箱,里面放了张欠条,现在打开看→发现欠条指向已倒闭的公司 ❌ 无效!
情况3 你借了个临时保险箱,拿到钥匙(Ref)但保险箱立即被拿走了 ❌ 钥匙没用了!

延伸阅读

  • 借用检查器:理解 Rust 如何在编译期防止悬垂引用
  • 智能指针RcBoxRefCell 的内存管理机制
  • 生命周期参数:显式标注引用的有效期
  • 内部可变性RefCell 提供的运行时借用检查

总结

三种情况的核心差异在于:

  1. 情况1:所有权类型 + 未使用 = 死代码,不检查
  2. 情况2:所有权类型 + 使用 = 检查使用点,发现悬垂引用
  3. 情况3:借用类型 = 类型本身要求生命周期,立即检查

记住:借用类型的存在本身就是一种"使用",它必然要求被借用对象有效!

作者 tbgwe 2025-11-13 11:36

我突然又有一个疑问,,借用类型也有问题,,比如let s=&String::from("tbg");就这个语句,这是借用类型吧,是不是就应该立即检查,直接报错,,但是编译通过了,,而且后续读写s都能直接通过编译,这是为什么啊

--
👇
tbgwe: 大佬,谢谢,有点明白了,这个知识你是从哪里找到的

--
👇
iduanyingjie: # Rust 借用检查:三种情况对比分析

概述

本文档通过三个具体例子,深入分析 Rust 编译器的借用检查机制,揭示所有权类型借用类型在生命周期检查上的根本差异。


情况对比表

情况 代码 编译结果 原因
情况1 let _a = Rc::new(RefCell::new(&String::from("tbg"))); ✅ 通过 所有权类型,未使用不检查
情况2 let _a = Rc::new(RefCell::new(&String::from("tbg")));<br>println!("{}", *_a.borrow()); ❌ 报错 使用时检查,发现悬垂引用
情况3 let _s = RefCell::new(String::from("qaz")).borrow(); ❌ 报错 借用类型,存在即要求生命周期

情况1:所有权类型 + 未使用

代码

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

fn main() {
    let _a = Rc::new(RefCell::new(&String::from("tbg")));
    // 没有任何使用 _a 的代码
}

编译结果

编译通过(但存在潜在的悬垂引用)

详细分析

let _a = Rc::new(RefCell::new(&String::from("tbg")));
//                             |<--临时值-->|-- 在此销毁
//       |<--------Rc 拥有 RefCell---------->|
//
// 类型:Rc<RefCell<&String>>

为什么通过?

  1. Rc::new() 返回 Rc<RefCell<&String>>,是一个拥有所有权的值
  2. Rc 本身是有效的,它成功分配了内存并存储了 RefCell
  3. 虽然内部的 &String 指向已销毁的临时值,但编译器采用按需检查策略
  4. 因为 _a 从未被使用,编译器将其视为死代码,不进行深度借用检查

关键点

  • 所有权类型(RcBoxVec 等)在未使用时不触发内部借用检查
  • 这是编译器的优化策略,允许死代码存在

情况2:所有权类型 + 使用

代码

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

fn main() {
    let _a = Rc::new(RefCell::new(&String::from("tbg")));
    println!("{}", *_a.borrow());  // 尝试使用
}

编译结果

编译失败

error[E0716]: temporary value dropped while borrowed
 --> src/main.rs:5:36
  |
5 |     let _a = Rc::new(RefCell::new(&String::from("tbg")));
  |                                    ^^^^^^^^^^^^^^^^^^^  - temporary value is freed at the end of this statement
  |                                    |
  |                                    creates a temporary value which is freed while still in use
6 |     println!("{}", *_a.borrow());
  |                     -- borrow later used here

详细分析

let _a = Rc::new(RefCell::new(&String::from("tbg")));
//                             |<--临时值-->|-- 在此销毁
println!("{}", *_a.borrow());
//              ^^ 尝试访问已销毁的临时值 → 悬垂引用!

为什么报错?

  1. *_a.borrow() 尝试解引用并访问内部的 &String
  2. 编译器必须验证这个引用是否有效
  3. 检查发现 &String 指向的 String::from("tbg") 已在第5行结束时销毁
  4. 这会导致未定义行为,编译器必须阻止

对比情况1

  • 情况1:不使用 → 死代码 → 不检查
  • 情况2:使用 → 必须检查 → 发现悬垂引用 → 报错

正确写法

let s = String::from("tbg");           // s 拥有 String
let _a = Rc::new(RefCell::new(&s));    // RefCell 借用 s
println!("{}", *_a.borrow());          // s 还活着,引用有效

情况3:借用类型的生命周期冲突

代码

use std::cell::RefCell;

fn main() {
    let _s = RefCell::new(String::from("qaz")).borrow();
    // 即使不使用 _s,也会报错
}

编译结果

编译失败

error[E0716]: temporary value dropped while borrowed
 --> src/main.rs:4:14
  |
4 |     let _s = RefCell::new(String::from("qaz")).borrow(); 
  |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^         - temporary value is freed at the end of this statement
  |              |
  |              creates a temporary value which is freed while still in use
5 | }
  | - borrow might be used here, when `_s` is dropped and runs the destructor for type `Ref<'_, String>`

详细分析

let _s = RefCell::new(String::from("qaz")).borrow();
//       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^
//       临时 RefCell                       返回 Ref<'a, String>
//       |<--在此销毁-->|
//                                         |<--_s 存活到作用域结束-->|
//
// 类型:Ref<'_, String>  (注意生命周期参数)

为什么报错?(关键!)

这与情况1和2完全不同:

  1. Ref<'a, String> 本身就是一个借用类型
  2. RefRefCell 的借用守卫(borrow guard),它持有对 RefCell 的运行时借用
  3. Ref 的存在本身就要求 RefCell 必须保持有效
  4. 这不是"是否使用"的问题,而是类型系统的约束

生命周期冲突

临时 RefCell 的生命周期:|<---第4行--->|
_s (Ref) 的生命周期:     |<---第4行到第5行--->|
                          ^^^^^^^^ 冲突!

与情况1的本质区别

特性 情况1: _a 情况3: _s
类型性质 Rc<...> 所有权类型 Ref<'a, ...> 借用类型
是否"使用" 未使用内部值 类型本身就是借用
检查时机 按需检查(使用时) 立即检查(创建时)
生命周期 Rc 自己管理生命周期 'a 表示依赖外部生命周期

正确写法

let cell = RefCell::new(String::from("qaz"));  // cell 拥有 RefCell
let _s = cell.borrow();                        // _s 借用 cell
// cell 活着 → _s 有效

核心概念总结

1. 所有权类型 vs 借用类型

// 所有权类型(拥有数据)
let owned: Rc<T> = ...;        // 自己管理生命周期
let owned: Box<T> = ...;       // 自己管理生命周期
let owned: Vec<T> = ...;       // 自己管理生命周期

// 借用类型(依赖其他数据)
let borrowed: &T = ...;        // 依赖被引用对象的生命周期
let borrowed: Ref<'a, T> = ...; // 依赖 RefCell 的生命周期
let borrowed: &mut T = ...;    // 依赖被引用对象的生命周期

2. 编译器的检查策略

所有权类型未使用 → 死代码 → 不深度检查内部借用
                   ↓
所有权类型被使用 → 检查使用点 → 验证内部借用有效性
                   ↓
借用类型(无论是否使用) → 立即检查 → 验证被借用对象生命周期

3. 生命周期依赖关系

// 情况1和2:Rc 不依赖外部
Rc<RefCell<&String>>
└─ Rc 拥有 RefCell
   └─ RefCell 包含 &String(悬垂,但未使用时不检查)

// 情况3:Ref 依赖外部
Ref<'a, String>
 ^^ 这个生命周期参数表示:Ref 的有效期不能超过被借用的 RefCell

实践建议

✅ 推荐做法

// 1. 让被借用对象有足够长的生命周期
let s = String::from("data");
let _a = Rc::new(RefCell::new(&s));

// 2. 或者直接存储所有权类型,避免引用
let _a = Rc::new(RefCell::new(String::from("data")));

// 3. 确保 RefCell 在借用期间存活
let cell = RefCell::new(String::from("data"));
let _s = cell.borrow();

❌ 常见错误

// 错误1:临时值 + 引用
let _a = Rc::new(RefCell::new(&String::from("temp")));
println!("{}", *_a.borrow());  // 悬垂引用

// 错误2:临时 RefCell + borrow
let _s = RefCell::new(String::from("temp")).borrow();  // 生命周期冲突

// 错误3:作用域问题
let _s = {
    let cell = RefCell::new(String::from("temp"));
    cell.borrow()  // cell 离开作用域,_s 悬垂
};

类比总结

用现实世界的类比理解这三种情况:

情况 类比 结果
情况1 你买了个保险箱(Rc),里面放了张欠条(&String),但从不打开看 ✅ 没人管
情况2 你买了个保险箱,里面放了张欠条,现在打开看→发现欠条指向已倒闭的公司 ❌ 无效!
情况3 你借了个临时保险箱,拿到钥匙(Ref)但保险箱立即被拿走了 ❌ 钥匙没用了!

延伸阅读

  • 借用检查器:理解 Rust 如何在编译期防止悬垂引用
  • 智能指针RcBoxRefCell 的内存管理机制
  • 生命周期参数:显式标注引用的有效期
  • 内部可变性RefCell 提供的运行时借用检查

总结

三种情况的核心差异在于:

  1. 情况1:所有权类型 + 未使用 = 死代码,不检查
  2. 情况2:所有权类型 + 使用 = 检查使用点,发现悬垂引用
  3. 情况3:借用类型 = 类型本身要求生命周期,立即检查

记住:借用类型的存在本身就是一种"使用",它必然要求被借用对象有效!

iduanyingjie 2025-11-13 11:34

claude生成的啊

--
👇
tbgwe: 大佬,谢谢,有点明白了,这个知识你是从哪里找到的

--
👇
iduanyingjie: # Rust 借用检查:三种情况对比分析

概述

本文档通过三个具体例子,深入分析 Rust 编译器的借用检查机制,揭示所有权类型借用类型在生命周期检查上的根本差异。


情况对比表

情况 代码 编译结果 原因
情况1 let _a = Rc::new(RefCell::new(&String::from("tbg"))); ✅ 通过 所有权类型,未使用不检查
情况2 let _a = Rc::new(RefCell::new(&String::from("tbg")));<br>println!("{}", *_a.borrow()); ❌ 报错 使用时检查,发现悬垂引用
情况3 let _s = RefCell::new(String::from("qaz")).borrow(); ❌ 报错 借用类型,存在即要求生命周期

情况1:所有权类型 + 未使用

代码

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

fn main() {
    let _a = Rc::new(RefCell::new(&String::from("tbg")));
    // 没有任何使用 _a 的代码
}

编译结果

编译通过(但存在潜在的悬垂引用)

详细分析

let _a = Rc::new(RefCell::new(&String::from("tbg")));
//                             |<--临时值-->|-- 在此销毁
//       |<--------Rc 拥有 RefCell---------->|
//
// 类型:Rc<RefCell<&String>>

为什么通过?

  1. Rc::new() 返回 Rc<RefCell<&String>>,是一个拥有所有权的值
  2. Rc 本身是有效的,它成功分配了内存并存储了 RefCell
  3. 虽然内部的 &String 指向已销毁的临时值,但编译器采用按需检查策略
  4. 因为 _a 从未被使用,编译器将其视为死代码,不进行深度借用检查

关键点

  • 所有权类型(RcBoxVec 等)在未使用时不触发内部借用检查
  • 这是编译器的优化策略,允许死代码存在

情况2:所有权类型 + 使用

代码

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

fn main() {
    let _a = Rc::new(RefCell::new(&String::from("tbg")));
    println!("{}", *_a.borrow());  // 尝试使用
}

编译结果

编译失败

error[E0716]: temporary value dropped while borrowed
 --> src/main.rs:5:36
  |
5 |     let _a = Rc::new(RefCell::new(&String::from("tbg")));
  |                                    ^^^^^^^^^^^^^^^^^^^  - temporary value is freed at the end of this statement
  |                                    |
  |                                    creates a temporary value which is freed while still in use
6 |     println!("{}", *_a.borrow());
  |                     -- borrow later used here

详细分析

let _a = Rc::new(RefCell::new(&String::from("tbg")));
//                             |<--临时值-->|-- 在此销毁
println!("{}", *_a.borrow());
//              ^^ 尝试访问已销毁的临时值 → 悬垂引用!

为什么报错?

  1. *_a.borrow() 尝试解引用并访问内部的 &String
  2. 编译器必须验证这个引用是否有效
  3. 检查发现 &String 指向的 String::from("tbg") 已在第5行结束时销毁
  4. 这会导致未定义行为,编译器必须阻止

对比情况1

  • 情况1:不使用 → 死代码 → 不检查
  • 情况2:使用 → 必须检查 → 发现悬垂引用 → 报错

正确写法

let s = String::from("tbg");           // s 拥有 String
let _a = Rc::new(RefCell::new(&s));    // RefCell 借用 s
println!("{}", *_a.borrow());          // s 还活着,引用有效

情况3:借用类型的生命周期冲突

代码

use std::cell::RefCell;

fn main() {
    let _s = RefCell::new(String::from("qaz")).borrow();
    // 即使不使用 _s,也会报错
}

编译结果

编译失败

error[E0716]: temporary value dropped while borrowed
 --> src/main.rs:4:14
  |
4 |     let _s = RefCell::new(String::from("qaz")).borrow(); 
  |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^         - temporary value is freed at the end of this statement
  |              |
  |              creates a temporary value which is freed while still in use
5 | }
  | - borrow might be used here, when `_s` is dropped and runs the destructor for type `Ref<'_, String>`

详细分析

let _s = RefCell::new(String::from("qaz")).borrow();
//       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^
//       临时 RefCell                       返回 Ref<'a, String>
//       |<--在此销毁-->|
//                                         |<--_s 存活到作用域结束-->|
//
// 类型:Ref<'_, String>  (注意生命周期参数)

为什么报错?(关键!)

这与情况1和2完全不同:

  1. Ref<'a, String> 本身就是一个借用类型
  2. RefRefCell 的借用守卫(borrow guard),它持有对 RefCell 的运行时借用
  3. Ref 的存在本身就要求 RefCell 必须保持有效
  4. 这不是"是否使用"的问题,而是类型系统的约束

生命周期冲突

临时 RefCell 的生命周期:|<---第4行--->|
_s (Ref) 的生命周期:     |<---第4行到第5行--->|
                          ^^^^^^^^ 冲突!

与情况1的本质区别

特性 情况1: _a 情况3: _s
类型性质 Rc<...> 所有权类型 Ref<'a, ...> 借用类型
是否"使用" 未使用内部值 类型本身就是借用
检查时机 按需检查(使用时) 立即检查(创建时)
生命周期 Rc 自己管理生命周期 'a 表示依赖外部生命周期

正确写法

let cell = RefCell::new(String::from("qaz"));  // cell 拥有 RefCell
let _s = cell.borrow();                        // _s 借用 cell
// cell 活着 → _s 有效

核心概念总结

1. 所有权类型 vs 借用类型

// 所有权类型(拥有数据)
let owned: Rc<T> = ...;        // 自己管理生命周期
let owned: Box<T> = ...;       // 自己管理生命周期
let owned: Vec<T> = ...;       // 自己管理生命周期

// 借用类型(依赖其他数据)
let borrowed: &T = ...;        // 依赖被引用对象的生命周期
let borrowed: Ref<'a, T> = ...; // 依赖 RefCell 的生命周期
let borrowed: &mut T = ...;    // 依赖被引用对象的生命周期

2. 编译器的检查策略

所有权类型未使用 → 死代码 → 不深度检查内部借用
                   ↓
所有权类型被使用 → 检查使用点 → 验证内部借用有效性
                   ↓
借用类型(无论是否使用) → 立即检查 → 验证被借用对象生命周期

3. 生命周期依赖关系

// 情况1和2:Rc 不依赖外部
Rc<RefCell<&String>>
└─ Rc 拥有 RefCell
   └─ RefCell 包含 &String(悬垂,但未使用时不检查)

// 情况3:Ref 依赖外部
Ref<'a, String>
 ^^ 这个生命周期参数表示:Ref 的有效期不能超过被借用的 RefCell

实践建议

✅ 推荐做法

// 1. 让被借用对象有足够长的生命周期
let s = String::from("data");
let _a = Rc::new(RefCell::new(&s));

// 2. 或者直接存储所有权类型,避免引用
let _a = Rc::new(RefCell::new(String::from("data")));

// 3. 确保 RefCell 在借用期间存活
let cell = RefCell::new(String::from("data"));
let _s = cell.borrow();

❌ 常见错误

// 错误1:临时值 + 引用
let _a = Rc::new(RefCell::new(&String::from("temp")));
println!("{}", *_a.borrow());  // 悬垂引用

// 错误2:临时 RefCell + borrow
let _s = RefCell::new(String::from("temp")).borrow();  // 生命周期冲突

// 错误3:作用域问题
let _s = {
    let cell = RefCell::new(String::from("temp"));
    cell.borrow()  // cell 离开作用域,_s 悬垂
};

类比总结

用现实世界的类比理解这三种情况:

情况 类比 结果
情况1 你买了个保险箱(Rc),里面放了张欠条(&String),但从不打开看 ✅ 没人管
情况2 你买了个保险箱,里面放了张欠条,现在打开看→发现欠条指向已倒闭的公司 ❌ 无效!
情况3 你借了个临时保险箱,拿到钥匙(Ref)但保险箱立即被拿走了 ❌ 钥匙没用了!

延伸阅读

  • 借用检查器:理解 Rust 如何在编译期防止悬垂引用
  • 智能指针RcBoxRefCell 的内存管理机制
  • 生命周期参数:显式标注引用的有效期
  • 内部可变性RefCell 提供的运行时借用检查

总结

三种情况的核心差异在于:

  1. 情况1:所有权类型 + 未使用 = 死代码,不检查
  2. 情况2:所有权类型 + 使用 = 检查使用点,发现悬垂引用
  3. 情况3:借用类型 = 类型本身要求生命周期,立即检查

记住:借用类型的存在本身就是一种"使用",它必然要求被借用对象有效!

作者 tbgwe 2025-11-13 11:30

大佬,谢谢,有点明白了,这个知识你是从哪里找到的

--
👇
iduanyingjie: # Rust 借用检查:三种情况对比分析

概述

本文档通过三个具体例子,深入分析 Rust 编译器的借用检查机制,揭示所有权类型借用类型在生命周期检查上的根本差异。


情况对比表

情况 代码 编译结果 原因
情况1 let _a = Rc::new(RefCell::new(&String::from("tbg"))); ✅ 通过 所有权类型,未使用不检查
情况2 let _a = Rc::new(RefCell::new(&String::from("tbg")));<br>println!("{}", *_a.borrow()); ❌ 报错 使用时检查,发现悬垂引用
情况3 let _s = RefCell::new(String::from("qaz")).borrow(); ❌ 报错 借用类型,存在即要求生命周期

情况1:所有权类型 + 未使用

代码

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

fn main() {
    let _a = Rc::new(RefCell::new(&String::from("tbg")));
    // 没有任何使用 _a 的代码
}

编译结果

编译通过(但存在潜在的悬垂引用)

详细分析

let _a = Rc::new(RefCell::new(&String::from("tbg")));
//                             |<--临时值-->|-- 在此销毁
//       |<--------Rc 拥有 RefCell---------->|
//
// 类型:Rc<RefCell<&String>>

为什么通过?

  1. Rc::new() 返回 Rc<RefCell<&String>>,是一个拥有所有权的值
  2. Rc 本身是有效的,它成功分配了内存并存储了 RefCell
  3. 虽然内部的 &String 指向已销毁的临时值,但编译器采用按需检查策略
  4. 因为 _a 从未被使用,编译器将其视为死代码,不进行深度借用检查

关键点

  • 所有权类型(RcBoxVec 等)在未使用时不触发内部借用检查
  • 这是编译器的优化策略,允许死代码存在

情况2:所有权类型 + 使用

代码

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

fn main() {
    let _a = Rc::new(RefCell::new(&String::from("tbg")));
    println!("{}", *_a.borrow());  // 尝试使用
}

编译结果

编译失败

error[E0716]: temporary value dropped while borrowed
 --> src/main.rs:5:36
  |
5 |     let _a = Rc::new(RefCell::new(&String::from("tbg")));
  |                                    ^^^^^^^^^^^^^^^^^^^  - temporary value is freed at the end of this statement
  |                                    |
  |                                    creates a temporary value which is freed while still in use
6 |     println!("{}", *_a.borrow());
  |                     -- borrow later used here

详细分析

let _a = Rc::new(RefCell::new(&String::from("tbg")));
//                             |<--临时值-->|-- 在此销毁
println!("{}", *_a.borrow());
//              ^^ 尝试访问已销毁的临时值 → 悬垂引用!

为什么报错?

  1. *_a.borrow() 尝试解引用并访问内部的 &String
  2. 编译器必须验证这个引用是否有效
  3. 检查发现 &String 指向的 String::from("tbg") 已在第5行结束时销毁
  4. 这会导致未定义行为,编译器必须阻止

对比情况1

  • 情况1:不使用 → 死代码 → 不检查
  • 情况2:使用 → 必须检查 → 发现悬垂引用 → 报错

正确写法

let s = String::from("tbg");           // s 拥有 String
let _a = Rc::new(RefCell::new(&s));    // RefCell 借用 s
println!("{}", *_a.borrow());          // s 还活着,引用有效

情况3:借用类型的生命周期冲突

代码

use std::cell::RefCell;

fn main() {
    let _s = RefCell::new(String::from("qaz")).borrow();
    // 即使不使用 _s,也会报错
}

编译结果

编译失败

error[E0716]: temporary value dropped while borrowed
 --> src/main.rs:4:14
  |
4 |     let _s = RefCell::new(String::from("qaz")).borrow(); 
  |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^         - temporary value is freed at the end of this statement
  |              |
  |              creates a temporary value which is freed while still in use
5 | }
  | - borrow might be used here, when `_s` is dropped and runs the destructor for type `Ref<'_, String>`

详细分析

let _s = RefCell::new(String::from("qaz")).borrow();
//       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^
//       临时 RefCell                       返回 Ref<'a, String>
//       |<--在此销毁-->|
//                                         |<--_s 存活到作用域结束-->|
//
// 类型:Ref<'_, String>  (注意生命周期参数)

为什么报错?(关键!)

这与情况1和2完全不同:

  1. Ref<'a, String> 本身就是一个借用类型
  2. RefRefCell 的借用守卫(borrow guard),它持有对 RefCell 的运行时借用
  3. Ref 的存在本身就要求 RefCell 必须保持有效
  4. 这不是"是否使用"的问题,而是类型系统的约束

生命周期冲突

临时 RefCell 的生命周期:|<---第4行--->|
_s (Ref) 的生命周期:     |<---第4行到第5行--->|
                          ^^^^^^^^ 冲突!

与情况1的本质区别

特性 情况1: _a 情况3: _s
类型性质 Rc<...> 所有权类型 Ref<'a, ...> 借用类型
是否"使用" 未使用内部值 类型本身就是借用
检查时机 按需检查(使用时) 立即检查(创建时)
生命周期 Rc 自己管理生命周期 'a 表示依赖外部生命周期

正确写法

let cell = RefCell::new(String::from("qaz"));  // cell 拥有 RefCell
let _s = cell.borrow();                        // _s 借用 cell
// cell 活着 → _s 有效

核心概念总结

1. 所有权类型 vs 借用类型

// 所有权类型(拥有数据)
let owned: Rc<T> = ...;        // 自己管理生命周期
let owned: Box<T> = ...;       // 自己管理生命周期
let owned: Vec<T> = ...;       // 自己管理生命周期

// 借用类型(依赖其他数据)
let borrowed: &T = ...;        // 依赖被引用对象的生命周期
let borrowed: Ref<'a, T> = ...; // 依赖 RefCell 的生命周期
let borrowed: &mut T = ...;    // 依赖被引用对象的生命周期

2. 编译器的检查策略

所有权类型未使用 → 死代码 → 不深度检查内部借用
                   ↓
所有权类型被使用 → 检查使用点 → 验证内部借用有效性
                   ↓
借用类型(无论是否使用) → 立即检查 → 验证被借用对象生命周期

3. 生命周期依赖关系

// 情况1和2:Rc 不依赖外部
Rc<RefCell<&String>>
└─ Rc 拥有 RefCell
   └─ RefCell 包含 &String(悬垂,但未使用时不检查)

// 情况3:Ref 依赖外部
Ref<'a, String>
 ^^ 这个生命周期参数表示:Ref 的有效期不能超过被借用的 RefCell

实践建议

✅ 推荐做法

// 1. 让被借用对象有足够长的生命周期
let s = String::from("data");
let _a = Rc::new(RefCell::new(&s));

// 2. 或者直接存储所有权类型,避免引用
let _a = Rc::new(RefCell::new(String::from("data")));

// 3. 确保 RefCell 在借用期间存活
let cell = RefCell::new(String::from("data"));
let _s = cell.borrow();

❌ 常见错误

// 错误1:临时值 + 引用
let _a = Rc::new(RefCell::new(&String::from("temp")));
println!("{}", *_a.borrow());  // 悬垂引用

// 错误2:临时 RefCell + borrow
let _s = RefCell::new(String::from("temp")).borrow();  // 生命周期冲突

// 错误3:作用域问题
let _s = {
    let cell = RefCell::new(String::from("temp"));
    cell.borrow()  // cell 离开作用域,_s 悬垂
};

类比总结

用现实世界的类比理解这三种情况:

情况 类比 结果
情况1 你买了个保险箱(Rc),里面放了张欠条(&String),但从不打开看 ✅ 没人管
情况2 你买了个保险箱,里面放了张欠条,现在打开看→发现欠条指向已倒闭的公司 ❌ 无效!
情况3 你借了个临时保险箱,拿到钥匙(Ref)但保险箱立即被拿走了 ❌ 钥匙没用了!

延伸阅读

  • 借用检查器:理解 Rust 如何在编译期防止悬垂引用
  • 智能指针RcBoxRefCell 的内存管理机制
  • 生命周期参数:显式标注引用的有效期
  • 内部可变性RefCell 提供的运行时借用检查

总结

三种情况的核心差异在于:

  1. 情况1:所有权类型 + 未使用 = 死代码,不检查
  2. 情况2:所有权类型 + 使用 = 检查使用点,发现悬垂引用
  3. 情况3:借用类型 = 类型本身要求生命周期,立即检查

记住:借用类型的存在本身就是一种"使用",它必然要求被借用对象有效!

iduanyingjie 2025-11-13 11:05

Rust 借用检查:三种情况对比分析

概述

本文档通过三个具体例子,深入分析 Rust 编译器的借用检查机制,揭示所有权类型借用类型在生命周期检查上的根本差异。


情况对比表

情况 代码 编译结果 原因
情况1 let _a = Rc::new(RefCell::new(&String::from("tbg"))); ✅ 通过 所有权类型,未使用不检查
情况2 let _a = Rc::new(RefCell::new(&String::from("tbg")));<br>println!("{}", *_a.borrow()); ❌ 报错 使用时检查,发现悬垂引用
情况3 let _s = RefCell::new(String::from("qaz")).borrow(); ❌ 报错 借用类型,存在即要求生命周期

情况1:所有权类型 + 未使用

代码

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

fn main() {
    let _a = Rc::new(RefCell::new(&String::from("tbg")));
    // 没有任何使用 _a 的代码
}

编译结果

编译通过(但存在潜在的悬垂引用)

详细分析

let _a = Rc::new(RefCell::new(&String::from("tbg")));
//                             |<--临时值-->|-- 在此销毁
//       |<--------Rc 拥有 RefCell---------->|
//
// 类型:Rc<RefCell<&String>>

为什么通过?

  1. Rc::new() 返回 Rc<RefCell<&String>>,是一个拥有所有权的值
  2. Rc 本身是有效的,它成功分配了内存并存储了 RefCell
  3. 虽然内部的 &String 指向已销毁的临时值,但编译器采用按需检查策略
  4. 因为 _a 从未被使用,编译器将其视为死代码,不进行深度借用检查

关键点

  • 所有权类型(RcBoxVec 等)在未使用时不触发内部借用检查
  • 这是编译器的优化策略,允许死代码存在

情况2:所有权类型 + 使用

代码

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

fn main() {
    let _a = Rc::new(RefCell::new(&String::from("tbg")));
    println!("{}", *_a.borrow());  // 尝试使用
}

编译结果

编译失败

error[E0716]: temporary value dropped while borrowed
 --> src/main.rs:5:36
  |
5 |     let _a = Rc::new(RefCell::new(&String::from("tbg")));
  |                                    ^^^^^^^^^^^^^^^^^^^  - temporary value is freed at the end of this statement
  |                                    |
  |                                    creates a temporary value which is freed while still in use
6 |     println!("{}", *_a.borrow());
  |                     -- borrow later used here

详细分析

let _a = Rc::new(RefCell::new(&String::from("tbg")));
//                             |<--临时值-->|-- 在此销毁
println!("{}", *_a.borrow());
//              ^^ 尝试访问已销毁的临时值 → 悬垂引用!

为什么报错?

  1. *_a.borrow() 尝试解引用并访问内部的 &String
  2. 编译器必须验证这个引用是否有效
  3. 检查发现 &String 指向的 String::from("tbg") 已在第5行结束时销毁
  4. 这会导致未定义行为,编译器必须阻止

对比情况1

  • 情况1:不使用 → 死代码 → 不检查
  • 情况2:使用 → 必须检查 → 发现悬垂引用 → 报错

正确写法

let s = String::from("tbg");           // s 拥有 String
let _a = Rc::new(RefCell::new(&s));    // RefCell 借用 s
println!("{}", *_a.borrow());          // s 还活着,引用有效

情况3:借用类型的生命周期冲突

代码

use std::cell::RefCell;

fn main() {
    let _s = RefCell::new(String::from("qaz")).borrow();
    // 即使不使用 _s,也会报错
}

编译结果

编译失败

error[E0716]: temporary value dropped while borrowed
 --> src/main.rs:4:14
  |
4 |     let _s = RefCell::new(String::from("qaz")).borrow(); 
  |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^         - temporary value is freed at the end of this statement
  |              |
  |              creates a temporary value which is freed while still in use
5 | }
  | - borrow might be used here, when `_s` is dropped and runs the destructor for type `Ref<'_, String>`

详细分析

let _s = RefCell::new(String::from("qaz")).borrow();
//       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^
//       临时 RefCell                       返回 Ref<'a, String>
//       |<--在此销毁-->|
//                                         |<--_s 存活到作用域结束-->|
//
// 类型:Ref<'_, String>  (注意生命周期参数)

为什么报错?(关键!)

这与情况1和2完全不同:

  1. Ref<'a, String> 本身就是一个借用类型
  2. RefRefCell 的借用守卫(borrow guard),它持有对 RefCell 的运行时借用
  3. Ref 的存在本身就要求 RefCell 必须保持有效
  4. 这不是"是否使用"的问题,而是类型系统的约束

生命周期冲突

临时 RefCell 的生命周期:|<---第4行--->|
_s (Ref) 的生命周期:     |<---第4行到第5行--->|
                          ^^^^^^^^ 冲突!

与情况1的本质区别

特性 情况1: _a 情况3: _s
类型性质 Rc<...> 所有权类型 Ref<'a, ...> 借用类型
是否"使用" 未使用内部值 类型本身就是借用
检查时机 按需检查(使用时) 立即检查(创建时)
生命周期 Rc 自己管理生命周期 'a 表示依赖外部生命周期

正确写法

let cell = RefCell::new(String::from("qaz"));  // cell 拥有 RefCell
let _s = cell.borrow();                        // _s 借用 cell
// cell 活着 → _s 有效

核心概念总结

1. 所有权类型 vs 借用类型

// 所有权类型(拥有数据)
let owned: Rc<T> = ...;        // 自己管理生命周期
let owned: Box<T> = ...;       // 自己管理生命周期
let owned: Vec<T> = ...;       // 自己管理生命周期

// 借用类型(依赖其他数据)
let borrowed: &T = ...;        // 依赖被引用对象的生命周期
let borrowed: Ref<'a, T> = ...; // 依赖 RefCell 的生命周期
let borrowed: &mut T = ...;    // 依赖被引用对象的生命周期

2. 编译器的检查策略

所有权类型未使用 → 死代码 → 不深度检查内部借用
                   ↓
所有权类型被使用 → 检查使用点 → 验证内部借用有效性
                   ↓
借用类型(无论是否使用) → 立即检查 → 验证被借用对象生命周期

3. 生命周期依赖关系

// 情况1和2:Rc 不依赖外部
Rc<RefCell<&String>>
└─ Rc 拥有 RefCell
   └─ RefCell 包含 &String(悬垂,但未使用时不检查)

// 情况3:Ref 依赖外部
Ref<'a, String>
 ^^ 这个生命周期参数表示:Ref 的有效期不能超过被借用的 RefCell

实践建议

✅ 推荐做法

// 1. 让被借用对象有足够长的生命周期
let s = String::from("data");
let _a = Rc::new(RefCell::new(&s));

// 2. 或者直接存储所有权类型,避免引用
let _a = Rc::new(RefCell::new(String::from("data")));

// 3. 确保 RefCell 在借用期间存活
let cell = RefCell::new(String::from("data"));
let _s = cell.borrow();

❌ 常见错误

// 错误1:临时值 + 引用
let _a = Rc::new(RefCell::new(&String::from("temp")));
println!("{}", *_a.borrow());  // 悬垂引用

// 错误2:临时 RefCell + borrow
let _s = RefCell::new(String::from("temp")).borrow();  // 生命周期冲突

// 错误3:作用域问题
let _s = {
    let cell = RefCell::new(String::from("temp"));
    cell.borrow()  // cell 离开作用域,_s 悬垂
};

类比总结

用现实世界的类比理解这三种情况:

情况 类比 结果
情况1 你买了个保险箱(Rc),里面放了张欠条(&String),但从不打开看 ✅ 没人管
情况2 你买了个保险箱,里面放了张欠条,现在打开看→发现欠条指向已倒闭的公司 ❌ 无效!
情况3 你借了个临时保险箱,拿到钥匙(Ref)但保险箱立即被拿走了 ❌ 钥匙没用了!

延伸阅读

  • 借用检查器:理解 Rust 如何在编译期防止悬垂引用
  • 智能指针RcBoxRefCell 的内存管理机制
  • 生命周期参数:显式标注引用的有效期
  • 内部可变性RefCell 提供的运行时借用检查

总结

三种情况的核心差异在于:

  1. 情况1:所有权类型 + 未使用 = 死代码,不检查
  2. 情况2:所有权类型 + 使用 = 检查使用点,发现悬垂引用
  3. 情况3:借用类型 = 类型本身要求生命周期,立即检查

记住:借用类型的存在本身就是一种"使用",它必然要求被借用对象有效!

1 共 14 条评论, 1 页