Rust 生命周期混淆

时间:2021-05-31 04:38:40

标签: rust rust-lifetimes

我目前正在自学 Rust,并通过实施井字游戏进行练习。

我有一个 Board 结构(CellGameState 是简单的枚举,SIZE3 usize):

struct Board {
    state : GameState,
    cells : [[Cell; SIZE]; SIZE],
}

我正在尝试实现一个 mark 方法:

impl Board {
    fn mark(&mut self, pos: &Position, player: Player) {
        // various checks omitted
        // mark cell for player
        self.cells[pos.row][pos.col] = Cell::Filled(player);
        // check whether player has won either via a full row or column (diagonals omitted):
        if (0..SIZE).map(|i| &self.cells[pos.row][i]).all(|c| *c == Cell::Filled(player)) ||
           (0..SIZE).map(|i| &self.cells[i][pos.col]).all(|c| *c == Cell::Filled(player)) {
            self.state = GameState::Winner(player);
    }
}

到目前为止一切都很好……但是代码重复很丑。

所以我的下一步是引入一个闭包并用它替换 if 中的重复:

let all_in_line = |cell_access| (0..SIZE).map(cell_access).all(|c : &Cell| *c == Cell::Filled(player));

if all_in_line(|i : usize| &self.cells[pos.row][i]) ||
   all_in_line(|i : usize| &self.cells[i][pos.col]) {

虽然这不起作用,因为它需要 all_in_line 是通用的(因为我传递给它的两个闭包具有不同的匿名类型),所以我的下一步是做一些类似于 type 的事情擦除:

fn mark(&mut self, pos: &Position, player: Player) {
    let all_in_line = |cell_access : Box<dyn Fn(usize) -> &Cell>| (0..SIZE).map(cell_access).all(|c : &Cell| *c == Cell::Filled(player));
    if all_in_line(Box::new(|i : usize| &self.cells[pos.row][i])) ||
       all_in_line(Box::new(|i : usize| &self.cells[i][pos.col])) {
        self.state = GameState::Winner(player);
    }

但是现在 rust 编译器抱怨 cell_access 的类型说明符中返回的引用缺少生命周期参数:

error[E0106]: missing lifetime specifier
let all_in_line = |cell_access : Box<dyn Fn(usize) -> &Cell>| (0..SIZE).map(cell_access).all(|c : &Cell| *c == Cell::Filled(player));
                                                      ^ expected named lifetime parameter

我尝试将 mark 修改为 fn mark<'a>(&'a mut self, /*...*/) 并相应地更新 cell_access 的类型:Box<dyn Fn(usize) -> &'a Cell> 但这失败了:

error[E0597]: `self` does not live long enough
fn mark<'a>(&'a mut self, pos: &Position, player: Player) {
        -- lifetime `'a` defined here
...
    if all_in_line(Box::new(|i : usize| &self.cells[pos.row][i]))
                           ----------- -^^^^------------------
                           |           ||
                           |           |borrowed value does not live long enough
                           |           returning this value requires that `self` is borrowed for `'a`
                           value captured here
...
}
- `self` dropped here while still borrowed

此时,我已经想不通出了什么问题以及如何解决。

1 个答案:

答案 0 :(得分:2)

<块引用>

但是现在 rust 编译器抱怨 cell_access 的类型说明符中缺少返回引用的生命周期参数:

据我所知,这里的问题是,为了能够以这种形式编写代码,cell_access 函数的签名需要引用其有效的生命周期 /em>,这是不可能的,因为那个生命周期没有名字——它是闭包创建中隐含的 self本地重新借用。您对 &'a mut self 的尝试不起作用,因为它没有捕捉到闭包的 self 是函数 self 的重新借用的事实,因此不需要完全存活相同的生命周期,因为可变借用的生命周期是不变的而不是协变的;不能随意将它们视为较短(因为这会破坏可变引用的排他性)。

最后一点让我知道如何解决这个问题:将 all_in_line 代码移动到一个函数中,该函数通过 immutable 引用接受 self。这编译:

impl Board {
    fn mark<'a>(&'a mut self, pos: &Position, player: Player) {
        if self.all_in_line_either_direction(pos, player)
        {
            // self.state = GameState::Winner(player);
        }
    }
    
    fn all_in_line_either_direction<'a>(&'a self, pos: &Position, player: Player) -> bool {
        let all_in_line = |cell_access: Box<dyn Fn(usize) -> &'a Cell>| {
            (0..SIZE)
                .map(cell_access)
                .all(|c: &Cell| *c == Cell::Filled(player))
        };
        all_in_line(Box::new(|i: usize| &self.cells[pos.row][i]))
            || all_in_line(Box::new(|i: usize| &self.cells[i][pos.col]))
    }
}

然而,虽然上述方法确实有效,但我认为这里最好的选择是,与其尝试使 all_in_line 成为闭包,不如使其成为一个单独的函数(可以是通用的),并避免使用 { {1}}。

在这个位置,我们可以写出我们实际需要的生命周期——函数的生命周期泛型 Box<dyn Fn> 意味着生命周期 <'a> 持续 只要 'a 需要它< /em>(生命周期参数不能在函数调用的中间结束),而采用盒装函数特征的闭包没有自己的泛型作用域。

all_in_line

然后您必须将 fn all_in_line<'a, F>(player: Player, cell_access: F) -> bool where F: Fn(usize) -> &'a Cell { (0..SIZE).map(cell_access).all(|c| *c == Cell::Filled(player)) } 传递给每个调用,但没有其他复杂情况。

如果您愿意,您甚至可以在 player 函数内部编写 all_in_line 的定义,使其不存在于外部。这是否是更具可读性的代码取决于您的决定。


我添加了一些片段来检查代码是否已编译;希望这一切都与您实际所做的相符:

mark