在Rust中返回可变引用

19

我正在学习Rust,如果这是一个琐碎的问题,请原谅。我已经谷歌了一个小时也没有结果。

我有一个数组的枚举值。我希望在该数组中找到与特定模式匹配的随机位置,并返回对其的可变引用,以便修改该位置的元素。

enum Tile {
    Empty,
    ...  // Other enum values
}

fn random_empty_tile(arr: &mut [Tile]) -> &mut Tile {
    loop {
        let i = rand::thread_rng().gen_range(0, arr.len());
        let tile = &mut arr[i];
        if let Tile::Empty = tile {
            return tile;
        }
    }
}

借用检查器在这里有两个具体的问题。第一个是 arr.len() 的调用。这是不允许的,因为它需要对 arr 取一个不可变引用,而我们已经通过参数拥有了一个可变引用到 arr。因此,不能取得其他引用,所以该调用是不允许的。

第二个问题是 return tile。这会失败,因为借用检查器无法证明此引用的生命周期与 arr 本身的生命周期相同,因此返回它是不安全的。

我认为上述错误描述是正确的;我认为我理解了出了什么问题。不幸的是,我不知道如何解决这些问题中的任何一个。如果有人能够提供一种惯用的解决方案来实现这种行为,那将非常感激。

最终,我希望做到以下几点:

let mut arr = [whatever];
let empty_element = random_empty_tile(&mut arr);
*empty_element = Tile::SomeOtherValue;

因此,通过变异数组以替换空值。


1
这里是一个可工作的版本:https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=aeb42c30f113d560235b8e6d6897f9b4。然而,如果我们加上 let tile = &mut arr[i]; 作为中间变量,为什么借用检查器会在这里失败似乎有点奇怪。这是编译器的缺陷吗? - Psidom
@Psidom 是的,这是借用检查器已知的缺点。 - Chayim Friedman
1个回答

12

问题的解答

fn random_empty_tile(arr: &mut [Tile]) -> &mut Tile {
    let len = arr.len();
    let mut the_chosen_i = 0;
    loop {
        let i = rand::thread_rng().gen_range(0, len);
        let tile = &mut arr[i];
        if let Tile::Empty = tile {
            the_chosen_i = i;
            break;
        }
    }
    &mut arr[the_chosen_i]
}

将起作用。在循环中,您可以使用可变的借用,但是不要从borrowcheckers的角度滥用它。您实际上正在重复地进行可变重新借用数组。像往常一样,如果您知道如何使用编译器,它会非常有帮助。

为了找出问题的根本原因,让我们仅看一下循环的前两个迭代:

fn random_empty_tile_2<'arr>(arr: &'arr mut [Tile]) -> &'arr mut Tile {
   let len = arr.len();

   // First loop iteration
   {
       let i = thread_rng().gen_range(0, len);
       let tile = &mut arr[i]; // Lifetime: 'arr
       if let Tile::Empty = tile {
           return tile;
       }
   } 

   // Second loop iteration
   {
       let i = thread_rng().gen_range(0, len);
       let tile = &mut arr[i]; // Lifetime: 'arr
        if let Tile::Empty = tile {
           return tile;
       }
   }

   unreachable!();

编译器告诉我们:借用名称为tilearr借用必须与数组本身叫做'arr的生命周期相同,因为这是被返回的。在下一次循环迭代中,我们再次为'arr借用arr。这违反了借用检查器的规则。

一些评论

这么多可变性并不是对自己有利的。当您试图同时持有一个指向arr中的值的可变引用并使用arr时,在主函数中可能会导致借用检查器抱怨(当然,如果您考虑过它的话!)。

此外,您选择随机空白瓷砖的算法非常危险,如果在大型数组中只有一个空白瓷砖,那么您的实现将需要很长时间。请先过滤所有指向空瓷砖的索引,然后从该集合中选择一个随机索引,最后返回该索引指向的条目。我不会为此提供代码,您自己可以解决它 :)


1
每次迭代后,本地变量 tile 不是超出范围了吗?为什么借用检查器会将其带到下一次迭代中? - Psidom
你不能同时拥有同一生命周期的同一物体的多个可变借用。这不应该是“你不能同时拥有同一物体的多个可变借用在同一时间内”。基本上,如果借用到达其自己的作用域的末尾,我们可以再次借用它。关于返回语句,一旦函数返回,我们将不再有下一次迭代。为什么它会继续在下一次迭代中进行检查呢? - Psidom
你说得没错,tile_1, tile_2, ... 的存在是互斥的——至少在我看来是这样。但编译器无法想到这一点(可能还没有?)。我们可以在这里使用 RefCell,以查看它是否在运行时实际成立,然后如果我们确定并已经证明了它,就可以使用 unsafe。如果你想听听这个:你说得对 :) - L. Riemer
嗯,有趣。你是说编译器将返回的任何内容的作用域扩展到函数的末尾,而不考虑此处“loop”定义的作用域吗? - Psidom
1
在这种特定情况下,编译器必须相互检查所有生命周期。它看到它们冲突并且无法证明它们不是互斥的,正如您指出的那样。它不能推断每个借用只持续循环的唯一迭代-毕竟,有什么阻止您将其分配给外部&'arr mut Tile?问题是你知道你不会这样做,但这是一个复杂的问题。不过别完全引用我的话-我并不是 Rust 编译器方面的专家。 - L. Riemer
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接