为何通过Trait对象的可变操作失败,函数指针却可行?
代码示例
pub struct FnWrapper1 { pub f: fn(&mut BoolAndFns1), } pub struct BoolAndFns1 { pub b: bool, pub fs: Vec<FnWrapper1>, } impl BoolAndFns1 { pub fn meth(&mut self) { if let Some(wrapper) = self.fs.last_mut() { (wrapper.f)(self); } } } pub struct FnWrapper2 { pub f: Box<dyn Fn(&mut BoolAndFns2)>, } pub struct BoolAndFns2 { pub b: bool, pub fs: Vec<FnWrapper2>, } impl BoolAndFns2 { pub fn meth(&mut self) { if let Some(wrapper) = self.fs.last_mut() { (wrapper.f)(self); } } } fn main() { let mut bf1 = BoolAndFns1 { b: false, fs: vec![FnWrapper1 { f: |bf| { bf.b = true; }, }], }; bf1.meth(); let mut bf2 = BoolAndFns2 { b: false, fs: vec![FnWrapper2 { f: Box::new(|bf2| { bf2.b = true; }), }], }; bf2.meth(); }
编译错误信息
error[E0499]: cannot borrow
*selfas mutable more than once at a time
--> src/main.rs:30:16
|
29 | if let Some(wrapper) = self.fs.last_mut() {
| ------- first mutable borrow occurs here
30 | (wrapper.f)(self);
| ----------- ^^^^ second mutable borrow occurs here
| |
| first borrow later used by call
问题
- 为何将可变引用传递给函数指针时,二次可变借用是合法的,但传递给boxed trait对象时却不合法?
- 是否可以让第二个版本正常工作?
1. 可变借用差异的原因
函数指针(fn(...))属于具体类型,编译器能精准分析它的行为:它只会接收传入的&mut BoolAndFns1,不会对wrapper(即self.fs的最后一个元素)进行任何操作。因此编译器可以确定:self.fs.last_mut()产生的可变借用,和传递给函数指针的self可变借用之间不存在冲突——函数指针不会同时访问self.fs的元素和整个self,所以允许这种借用。
而boxed trait对象Box<dyn Fn(&mut BoolAndFns2)>是动态类型,编译器无法在编译期确定这个trait对象内部的代码逻辑。它可能会尝试访问self.fs(比如修改wrapper本身),这就会和self的可变借用产生冲突。为了保证内存安全,Rust的借用检查器会采取保守策略,直接禁止这种可能引发风险的双重可变借用。
简言之:函数指针的行为静态可知,编译器能证明安全性;trait对象的行为动态不可预测,编译器无法排除冲突风险,因此报错。
2. 让第二个版本正常工作的方法
有几种可行的解决方案:
方法一:提前获取函数,释放对self.fs的借用
把boxed函数从wrapper中取出,让self.fs的可变借用提前结束,之后再调用函数并传递self:
impl BoolAndFns2 { pub fn meth(&mut self) { // 取出boxed函数,释放对self.fs的可变借用 let f = self.fs.last_mut().map(|wrapper| wrapper.f.take()); if let Some(mut f) = f { f(self); // 如果需要保留函数,再放回容器 self.fs.last_mut().unwrap().f = f; } } }
通过take()转移boxed函数的所有权,让self.fs的借用提前失效,从根源上避免双重借用冲突。
方法二:使用RefCell实现内部可变性
如果不需要严格的编译期借用检查,可以用RefCell绕开静态检查,在运行时保证安全:
use std::cell::RefCell; pub struct FnWrapper2 { pub f: Box<dyn Fn(&mut BoolAndFns2)>, } pub struct BoolAndFns2 { pub b: bool, pub fs: RefCell<Vec<FnWrapper2>>, } impl BoolAndFns2 { pub fn meth(&mut self) { // 先获取函数的不可变引用(仅调用无需修改wrapper) let f = self.fs.borrow().last().map(|wrapper| wrapper.f.clone()); if let Some(f) = f { f(self); } } }
将fs改为RefCell<Vec<FnWrapper2>>,通过运行时借用检查允许先读取函数,再传递self的可变引用。注意若运行时出现冲突会触发panic,需确保逻辑安全。
方法三:重新设计结构,避免同时借用
比如将函数与数据分开存储,或者调整调用逻辑,确保调用函数时不持有对fs的借用,从根源上消除冲突场景。
内容的提问来源于stack exchange,提问作者nicoty

