【问题标题】:Rust lifetime confusionRust 生命周期混乱
【发布时间】:2021-05-31 04:38:40
【问题描述】:

我目前正在自学 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 是通用的(因为我传递给它的两个闭包具有不同的匿名类型),所以我的下一步是做一些类似于类型擦除的事情:

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&lt;'a&gt;(&amp;'a mut self, /*...*/) 并相应地更新cell_access 的类型:Box&lt;dyn Fn(usize) -&gt; &amp;'a Cell&gt;,但这失败了:

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

此时,我已经想不出问题出在哪里以及如何解决它。

【问题讨论】:

    标签: rust lifetime


    【解决方案1】:

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

    据我了解,这里的问题是,为了能够以这种形式编写代码,cell_access 函数的签名需要引用它的有效生命周期,这是不可能的,因为该生命周期没有名称——它是一个 self 的局部重借,隐含在闭包的创建中。您对&amp;'a mut self 的尝试不起作用,因为它没有捕捉到闭包的self 是函数self 的重借,因此不需要生活在完全相同的生命周期,因为可变借用的生命周期是不变的而不是协变的;不能任意将它们视为更短(因为这会破坏可变引用的排他性)。

    最后一点让我知道如何解决这个问题:将all_in_line 代码移动到一个函数中,该函数通过不可变 引用获取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 设为闭包,不如将其设为单独的函数(可以是通用的),并避免使用 Box&lt;dyn Fn&gt;完全没有。

    在这个位置,我们可以写出我们实际需要的生命周期——函数的生命周期泛型 &lt;'a&gt; 意味着生命周期 'a 持续只要 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 函数内部,这样它就不会存在于外部。是否更易读的代码由您决定。


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

    #[derive(Copy, Clone, Eq, PartialEq)]
    struct Player;
    
    struct Position {
        row: usize,
        col: usize,
    }
    
    #[derive(Eq, PartialEq)]
    enum Cell {
        Filled(Player),
    }
    
    const SIZE: usize = 10;
    
    struct Board {
        // state: GameState,
        cells: [[Cell; SIZE]; SIZE],
    }
    
    impl Board {
        fn mark(&mut self, pos: &Position, player: Player) {
            self.cells[pos.row][pos.col] = Cell::Filled(player);
            if all_in_line(player, |i: usize| &self.cells[pos.row][i])
                || all_in_line(player, |i: usize| &self.cells[i][pos.col])
            {
                // self.state = GameState::Winner(player);
            }
        }
    }
    
    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))
    }
    

    【讨论】:

    • 注意,为了避免污染全局命名空间,all_in_line函数可以定义在markplayground)内部。
    • 是的,这绝对是修复它的合理方法。我仍然很想了解我的代码到底有什么问题(以及为什么你的显式函数不会遇到与我的代码相同的生命周期问题......)
    • @AndreasMorhammer 我添加了更多想法。考虑到这一点,我还不够好,无法全面了解“这行不通,因为……如果我们试图使语言有所不同,那么它会破坏……”但我希望它有所帮助.
    • 非常感谢,这很有帮助!
    猜你喜欢
    • 2019-10-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-03
    • 2017-05-03
    相关资源
    最近更新 更多