存储引用该对象中的对象的盒装闭包

Storing a boxed closure which references an object in that object

我正在尝试为我正在编写的游戏实现一个控制台系统,并找到了一个相当简单的系统:我定义了一个 Console 对象,它将命令存储为盒装闭包(特别是 Box<FnMut + 'a> 对于一些 'a)。这适用于引擎的任何组件,只要先创建 Console

不幸的是,这会阻止我添加修改 Console 本身的命令,这意味着我无法创建仅打印文本或定义其他变量或命令的命令。我写了一个复制错误的小例子:

use std::cell::Cell;

struct Console<'a> {
    cmds: Vec<Box<FnMut() + 'a>>,
}

impl<'a> Console<'a> {
    pub fn println<S>(&self, msg: S)
        where S: AsRef<str>
    {
        println!("{}", msg.as_ref());
    }

    pub fn add_cmd(&mut self, cmd: Box<FnMut() + 'a>) {
        self.cmds.push(cmd);
    }
}

struct Example {
    val: Cell<i32>,
}

fn main() {
    let ex = Example {
        val: Cell::new(0),
    };

    let mut con = Console {
        cmds: Vec::new(),
    };

    // this works
    con.add_cmd(Box::new(|| ex.val.set(5)));

    (con.cmds[0])();

    // this doesn't
    let cmd = Box::new(|| con.println("Hello, world!"));
    con.add_cmd(cmd);

    (con.cmds[1])();
}

错误:

error: `con` does not live long enough
  --> console.rs:34:31
   |
34 |         let cmd = Box::new(|| con.println("Hello, world!"));
   |                            -- ^^^ does not live long enough
   |                            |
   |                            capture occurs here
35 |         con.add_cmd(cmd);
36 |     }
   |     - borrowed value dropped before borrower
   |
   = note: values in a scope are dropped in the opposite order they are created

error: aborting due to previous error

是否有解决此问题的方法,或者我应该研究更好的系统?这是在 rustc 1.18.0-nightly (53f4bc311 2017-04-07).

这是编译器不允许的相当棘手的资源借用难题之一。基本上,我们有一个 Console 拥有多个闭包,这些闭包反过来捕获对同一控制台的不可变引用。这意味着两个约束:

  • 由于 Console 拥有闭包,它们将与控制台本身一样存在,并且内部向量将在 之后立即删除它们 Console被丢弃了。
  • 同时,每个闭包的生命周期不得超过 Console,否则我们最终会得到对控制台的悬空引用。

控制台和相应的闭包立即超出范围这一事实似乎无害。但是,drop method 这里遵循严格的顺序:首先是控制台,然后是闭包。

当然更不用说,如果您希望闭包在没有 interior mutability 的情况下自由地对控制台应用修改,您将不得不可变地借用它,这不能在多个闭包中完成。

解决问题的一种方法是将两者分开:让控制台不拥有闭包,而是将它们放在单独的注册表中,并且让闭包仅在调用闭包时借用控制台。

这可以通过将控制台作为参数传递给闭包并将闭包向量移动到另一个对象来完成 (Playground):

use std::cell::Cell;

struct CommandRegistry<'a> {
    cmds: Vec<Box<Fn(&mut Console) + 'a>>,
}

impl<'a> CommandRegistry<'a> {
    pub fn add_cmd(&mut self, cmd: Box<Fn(&mut Console) + 'a>) {
        self.cmds.push(cmd);
    }
}

struct Console {
}

impl Console {
    pub fn println<S>(&mut self, msg: S)
        where S: AsRef<str>
    {
        println!("{}", msg.as_ref());
    }
}

struct Example {
    val: Cell<i32>,
}

fn main() {
    let ex = Example {
        val: Cell::new(0),
    };

    let mut reg = CommandRegistry{ cmds: Vec::new() };

    let mut con = Console {};

    // this works
    reg.add_cmd(Box::new(|_: &mut Console| ex.val.set(5)));
    (reg.cmds[0])(&mut con);

    // and so does this now!
    let cmd = Box::new(|c: &mut Console| c.println("Hello, world!"));
    reg.add_cmd(cmd);

    (reg.cmds[1])(&mut con);
}

我还冒昧地让闭包接受可变引用。这里不会出现冲突,因为我们不再借用在获取借用闭包时已经借用的控制台。这样,闭包也可以比控制台更有效。