使用RefCell<T>时遇到奇怪的编译期借用错误
环境与代码背景
- 使用Ratatui库开发程序,rustc版本1.77.2
- 核心代码片段:
self.terminal().draw(|frame| self.render_frame(frame))?;
- 细节说明:
terminal()是接收&self的getter方法,最初直接返回结构体中的Terminal实例,但返回&mut Terminal会触发借用错误- 改为将Terminal字段用
RefCell<Terminal>包裹,terminal()返回RefMut<Terminal>,内部调用self.terminal.borrow_mut() draw是Ratatui的屏幕绘制方法,闭包中调用的self.render_frame需要&mut self作为参数
遇到的编译错误
error[E0502]: cannot borrow
selfas mutable because it is also borrowed as immutable
--> src/tui.rs:75:38
|
75 | self.terminal().draw(|frame| self.render_frame(frame))?;
| --------------- ^^^^^^^ ---- - ... and the immutable borrow might be used here, when that temporary is dropped and runs the destructor for typeRefMut<'_, ratatui::Terminal<ratatui::backend::CrosstermBackend<Stdout>>>
| | | |
| | | second borrow occurs due to use ofselfin closure
| | mutable borrow occurs here
| immutable borrow occurs here
| a temporary with access to the immutable borrow is created here ...
疑问
原本以为RefMut是拥有所有权的类型,不受编译期借用检查器管控,只要render_frame里不调用terminal()就不会有问题,但实际触发了编译错误。想明确:
- 对RefCell/RefMut的使用哪里理解有误?
- 如何修复这个错误?
问题解析与解决方案
错误原因
虽然RefCell的借用规则是运行时检查,但RefMut<'a, T>包含生命周期'a,这个生命周期会绑定到self的借用上。调用self.terminal()时,编译器会推断RefMut的生命周期与&self的借用周期一致;而闭包中尝试获取&mut self时,RefMut的临时值还未销毁(直到整个draw方法执行完毕),这就触发了编译期的借用冲突——不可变借用未结束时,无法获取可变借用。
修复方案
方案1:拆分操作,提前持有RefMut
把terminal()的调用拆出来,用局部变量持有RefMut,让编译器明确生命周期范围,避免闭包中借用self的冲突:
let mut terminal = self.terminal(); terminal.draw(|frame| self.render_frame(frame))?;
方案2:分离render_frame所需状态
如果render_frame不需要访问Terminal相关字段,可将这部分状态拆分到单独结构体中,闭包仅借用子结构体的可变引用,避免与Terminal的借用冲突:
// 调整结构示例 struct App { terminal: RefCell<Terminal<CrosstermBackend<Stdout>>>, ui_state: RefCell<UiState>, // 存放render_frame需要的状态 } struct UiState { // 示例状态字段 counter: u32, } impl UiState { fn render(&mut self, frame: &mut Frame) { // 渲染逻辑 } } impl App { fn draw(&self) -> Result<(), Box<dyn Error>> { let mut terminal = self.terminal(); terminal.draw(|frame| { let mut ui_state = self.ui_state.borrow_mut(); ui_state.render(frame); })?; Ok(()) } }
方案3:使用UnsafeCell(谨慎使用)
如果能严格保证render_frame不会访问Terminal字段,可使用UnsafeCell绕过编译期检查,但需自行保证运行时借用安全:
struct App { terminal: UnsafeCell<Terminal<CrosstermBackend<Stdout>>>, } impl App { fn terminal(&self) -> &mut Terminal<CrosstermBackend<Stdout>> { unsafe { &mut *self.terminal.get() } } fn draw(&self) -> Result<(), Box<dyn Error>> { self.terminal().draw(|frame| { // 必须确保render_frame不会访问terminal字段 let self_mut = unsafe { &mut *(self as *const Self as *mut Self) }; self_mut.render_frame(frame); })?; Ok(()) } }
注意:该方案使用unsafe代码,若违反借用规则会导致未定义行为,非必要不推荐使用。
内容的提问来源于stack exchange,提问作者Iris

