为什么借用检查器即使第一个可变借用已经超出作用域,也不允许第二个可变借用?

4

背景

我知道借用检查器不允许多个可变借用。例如,下面的代码是无效的:

fn main() {
    let mut x = 42;
    let a = &mut x;
    let b = &mut x;
    println!("{} {}", a, b);
}

然而,如果第一个借用因超出作用域而被丢弃,则第二个借用是有效的:
fn main() {
    let mut x = 1;
    {
        let a = &mut x;
        println!("{}", a);
    }
    let b = &mut x;
    println!("{}", b);
}

由于非词汇生命周期 (NLL),第一个借用甚至不必超出作用域- 借用检查器只要求它不再被使用。因此,下面的Rust 2018代码是有效的:
fn main() {
    let mut x = 1;

    let a = &mut x;
    println!("{}", a);

    let b = &mut x;
    println!("{}", b);
}

问题

但我不明白为什么下面的代码是无效的:

use std::str::Chars;

fn main() {
    let s = "ab".to_owned();
    let mut char_iter = s.chars();

    let mut i = next(&mut char_iter);
    dbg!(i.next());

    let mut j = next(&mut char_iter);
    dbg!(j.next());
}

fn next<'a>(char_iter: &'a mut Chars<'a>) -> impl Iterator<Item = char> + 'a {
    char_iter.take_while(|&ch| ch != ' ')
}

编译错误信息:

error[E0499]: cannot borrow `char_iter` as mutable more than once at a time
  --> src/main.rs:10:22
   |
7  |     let mut i = next(&mut char_iter);
   |                      -------------- first mutable borrow occurs here
...
10 |     let mut j = next(&mut char_iter);
   |                      ^^^^^^^^^^^^^^ second mutable borrow occurs here
11 |     dbg!(j.next());
12 | }
   | - first borrow might be used here, when `i` is dropped and runs the destructor for type `impl std::iter::Iterator`

从错误信息来看,我认为NLL可能还不支持这种情况。因此,我提前删除了i

use std::str::Chars;

fn main() {
    let s = "ab".to_owned();
    let mut char_iter = s.chars();

    {
        let mut i = next(&mut char_iter);
        dbg!(i.next());
    }

    let mut j = next(&mut char_iter);
    dbg!(j.next());
}

fn next<'a>(char_iter: &'a mut Chars<'a>) -> impl Iterator<Item = char> + 'a {
    char_iter.take_while(|&ch| ch != ' ')
}

( Rust Playground)

但是我收到了一个更加令人困惑的错误信息:

error[E0499]: cannot borrow `char_iter` as mutable more than once at a time
  --> src/main.rs:12:22
   |
8  |         let mut i = next(&mut char_iter);
   |                          -------------- first mutable borrow occurs here
...
12 |     let mut j = next(&mut char_iter);
   |                      ^^^^^^^^^^^^^^
   |                      |
   |                      second mutable borrow occurs here
   |                      first borrow later used here

为什么即使i已经被删除并超出范围,这里还会显示first borrow later used here

另一种方法

如果我将next函数的签名更改为以下内容,则代码将被编译:

fn next(char_iter: impl Iterator<Item = char>) -> impl Iterator<Item = char> {
    char_iter.take_while(|&ch| ch != ' ')
}

但是,我仍然想了解为什么原始的next函数不起作用。

2个回答

4
让我们在这里揭开类型推断的神秘面纱。实际上,impl Iterator 是一个具体类型:Chars,它被TakeWhile包装起来。因此,您可以像这样重新编写您的方法(顺便说一句,一个有趣的任务是确定&char的生命周期):
fn next<'a>(
    char_iter: &'a mut Chars<'a>,
) -> TakeWhile<&'a mut Chars<'a>, impl FnMut(&char) -> bool> {
    char_iter.take_while(|&ch| ch != ' ')
}

现在你可能已经看到了输出类型与输入类型的寿命是相等的,反之亦然。实际上,这种寿命是由最初使用的&str导出的。因此,你可以得出结论:结果类型的寿命与所使用的字符串一样长(即到达main的末尾),即使显式地调用drop(i)也没有帮助,因为编译器知道Chars被借用到了最后。要让nll正常工作,你必须(不幸地)帮助编译器。
fn next<'a, 'b: 'a>(
    char_iter: &'a mut Chars<'b>,
) -> TakeWhile<&'a mut Chars<'b>, impl FnMut(&char) -> bool> {
    char_iter.take_while(|&ch| ch != ' ')
}

很高兴知道impl特质可以用于类型参数。我之前认为它只能用于参数或返回类型,如官方文档所述。 - Daniel
我也不知道,我本以为那个例子只是一个粗略的等价物,但令我惊讶的是它成功编译了。 - Kitsu

2
问题在于你通过声明 'a 生命周期相同来告诉借用检查器在以下块中 i 的生命周期与 char_iter 相同。请注意,这可能会导致错误的借用。
fn next<'a>(char_iter: &'a mut Chars<'a>) -> impl Iterator<Item = char> + 'a {
    char_iter.take_while(|&ch| ch != ' ')
}

这意味着编译器认为只要 char_iter 仍在作用域内,&mut char_iter 就仍然在使用中。也就是说,在 main() 结束之前。

我认为真正的问题是“为什么缩小i的范围不起作用?” - mcarton

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