(规则问题、无法编译、限制)可变struct中的闭包参数使用struct本身、挑战可变借用多次无法编译

问题描述

希望实现一个解释器,将Rust中的函数绑定到解释器的全域上下文cx中,并让从cx中获取的target_func能够将cx自身作为参数传入。但在实现过程中遇到编译错误,以下是简化的代码示例:

use std::collections::HashMap;

struct Content {
    pub data: i32,
    pub map_env_fun: HashMap<String, EnvFunction>,
}
impl Content {
    fn new() -> Self {
        Content {
            data: 0,
            map_env_fun: HashMap::new(),
        }
    }
}

type BoxFnMut = Box<dyn FnMut(&mut Content) -> bool>;

enum EnvFunction {
    Func(BoxFnMut),
}

fn __fun(cx: &mut Content) -> bool {
    cx.data += 1;
    true
}

fn init_built_in_functions(cx: &mut Content) {
    let fun: BoxFnMut = Box::new(__fun);
    cx.map_env_fun
        .insert("key_1".to_string(), EnvFunction::Func(fun));
    cx.data += 1;
}

fn main() {
    let mut cx = Content::new();
    init_built_in_functions(&mut cx);

    let EnvFunction::Func(target_func) = cx.map_env_fun.get_mut("key_1").unwrap();
    // --------------------------------------------------------------
    target_func(&mut cx);      // Error:编译失败
    // --------------------------------------------------------------

    println!("Content data: {}", cx.data);
}

讨论与解决方案

代码优化提示

用户small-white0-0指出,使用cargo clippy可以简化代码:

let EnvFunction::Func(target_func) = cx.map_env_fun
    .get_mut("key_1")
    .map(clone)  // 替代 map(|f| f.clone())
    .unwrap();

方案1:使用函数指针(FnPtr

用户elsejj提出使用函数指针避免闭包的借用问题:

type FuncPtr = fn(&mut Content) -> bool;
enum EnvFunction {
    Func(FuncPtr),  // 函数指针可复制,避免借用冲突
}

Playground 示例

方案2:使用RefCell实现内部可变性

用户zylthinking分享了使用RefCell的方案,通过运行时借用检查绕过编译期限制:

use std::cell::RefCell;
use std::collections::HashMap;

struct Content {
    pub data_i32: RefCell<i32>,
    pub data_str: RefCell<String>,
    pub map_env_fun: RefCell<HashMap<String, EnvFunction>>,
}

type BoxFnMut = Box<dyn Fn(&Content) -> bool>;
enum EnvFunction {
    Func(BoxFnMut),
}

// 使用宏简化重复的借用逻辑
macro_rules! get_dyn_hashmap_func {
    ($name:ident, $cx:expr, $key:expr) => {
        let map_env_fun = $cx.map_env_fun.borrow();
        let EnvFunction::Func(target_func_1) = map_env_fun.get($key).unwrap();
        let $name = target_func_1;
    };
}

方案3:使用Arc<Rc>共享闭包

用户small-white0-0提出使用引用计数智能指针Arc实现闭包的共享,避免所有权转移问题:

use std::collections::HashMap;
use std::sync::Arc;

type ARrcFn = Arc<dyn Fn(&mut Content) -> bool>;
#[derive(Clone)]
enum EnvFunction {
    Func(ARrcFn),  // 支持克隆,允许多次安全借用
}

fn init_built_in_functions(cx: &mut Content) {
    let fun: ARrcFn = Arc::new(__fun);
    cx.map_env_fun.insert("key_1".to_string(), EnvFunction::Func(fun));
}

fn main() {
    let mut cx = Content::new();
    init_built_in_functions(&mut cx);
    let EnvFunction::Func(target_func) = cx.map_env_fun.get_mut("key_1").map(|f| f.clone()).unwrap();
    target_func(&mut cx);  // 编译通过
}

Playground 示例

其他讨论点

  • 性能与安全性权衡RefCell将借用检查推迟到运行时,可能导致panicArc在单线程中可使用Rc,兼顾安全与性能。
  • 函数复用需求:若需要多次调用map中的函数,需避免通过remove取出后再插入的临时方案(如let EnvFunction::Func(mut target_func) = cx.map_env_fun.remove("key_1").unwrap();),应优先使用共享指针。
  • 内置函数特性:若函数是静态的且不捕获环境,直接使用函数指针是最优解;若需要动态闭包,需通过智能指针或内部可变性解决借用冲突。

总结

通过引入函数指针、RefCellArc/Rc等智能指针,结合Rust的借用规则,可以有效解决可变struct中闭包参数使用自身时的编译错误。具体方案需根据场景权衡安全性、性能和代码复杂度,优先避免unsafe代码,合理利用Rust的类型系统特性。