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

时间:2017-04-08 22:42:17

标签: rust closures ownership

我正在尝试为我正在编写的游戏实现一个控制台系统并找到了一个相当简单的系统:我定义了一个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)

1 个答案:

答案 0 :(得分:4)

这是编译器无法允许的相当棘手的资源借用难题之一。基本上,我们有一个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);
}

我也冒昧地让闭包接受一个可变的引用。这里没有冲突,因为我们不再借用已经借用借用关闭时已经借用的控制台。这样,闭包也可以比控制台更长。