为什么在 if let 的 else 块中 borrow 仍然被持有?

17

为什么以下代码中的调用self.f2()会触发借用检查器?难道else块不是在不同的作用域吗?这真是个难题!

use std::str::Chars;

struct A;

impl A {
    fn f2(&mut self) {}

    fn f1(&mut self) -> Option<Chars> {
        None
    }

    fn f3(&mut self) {
        if let Some(x) = self.f1() {

        } else {
            self.f2()
        }
    }
}

fn main() {
    let mut a = A;
}

Playground

error[E0499]: cannot borrow `*self` as mutable more than once at a time
  --> src/main.rs:16:13
   |
13 |         if let Some(x) = self.f1() {
   |                          ---- first mutable borrow occurs here
...
16 |             self.f2()
   |             ^^^^ second mutable borrow occurs here
17 |         }
   |         - first borrow ends here

借用自身的范围不是始于self.f1()的调用,终于该调用。一旦从f1()的调用返回后,f1()不再使用self,因此借用检查器不应该对第二个借用存在任何问题。请注意以下代码也会失败...

// ...
if let Some(x) = self.f1() {
    self.f2()
}
// ...

游乐场

我认为这里第二次借用应该没问题,因为f1f3在同一时间内未与f2使用self

5个回答

12

我准备了一个例子来展示这里的作用域规则:

struct Foo {
    a: i32,
}

impl Drop for Foo {
    fn drop(&mut self) {
        println!("Foo: {}", self.a);
    }
}

fn generate_temporary(a: i32) -> Option<Foo> {
    if a != 0 { Some(Foo { a: a }) } else { None }
}

fn main() {
    {
        println!("-- 0");
        if let Some(foo) = generate_temporary(0) {
            println!("Some Foo {}", foo.a);
        } else {
            println!("None");
        }
        println!("-- 1");
    }
    {
        println!("-- 0");
        if let Some(foo) = generate_temporary(1) {
            println!("Some Foo {}", foo.a);
        } else {
            println!("None");
        }
        println!("-- 1");
    }
    {
        println!("-- 0");
        if let Some(Foo { a: 1 }) = generate_temporary(1) {
            println!("Some Foo {}", 1);
        } else {
            println!("None");
        }
        println!("-- 1");
    }
    {
        println!("-- 0");
        if let Some(Foo { a: 2 }) = generate_temporary(1) {
            println!("Some Foo {}", 1);
        } else {
            println!("None");
        }
        println!("-- 1");
    }
}

这将打印:

-- 0
None
-- 1
-- 0
Some Foo 1
Foo: 1
-- 1
-- 0
Some Foo 1
Foo: 1
-- 1
-- 0
None
Foo: 1
-- 1

简而言之,似乎在if语句中的表达式会存在于if块和else块中。

一方面,这并不令人惊讶,因为确实需要比if块更长时间存活,但另一方面,它确实阻止了有用的模式。

如果您更喜欢视觉解释:

if let pattern = foo() {
    if-block
} else {
    else-block
}

转换为:

{
    let x = foo();
    match x {
    pattern => { if-block }
    _ => { else-block }
    }
}

虽然您更希望它能转化为以下形式:

bool bypass = true;
{
    let x = foo();
    match x {
    pattern => { if-block }
    _ => { bypass = false; }
    }
}
if not bypass {
    else-block
}

你不是第一个被这个绊倒的人,所以这可能会在某个时候得到解决,尽管会改变一些代码的含义(特别是守卫)。


6

这很烦人,但您可以通过引入内部作用域并稍微更改控制流来解决这个问题:

fn f3(&mut self) {
    {
        if let Some(x) = self.f1() {
            // ...
            return;
        }
    }
    self.f2()
}

正如评论中指出的那样,这个代码可以不用额外的大括号就能运行。这是因为if或if...let表达式有隐含的作用域,借用只在此作用域中有效:

fn f3(&mut self) {
    if let Some(x) = self.f1() {
        // ...
        return;
    }

    self.f2()
}

这是 Sandeep Datta 和 mbrubeck 之间 IRC 聊天的日志:
mbrubeck:std:tr::Chars 包含对创建它的字符串的借用引用。完整的类型名称是 Chars<'a>。因此,没有去掉省略时 f1(&mut self) -> Option 是 f1(&'a mut self) -> Option>,这意味着只要 f1 的返回值在作用域内,self 就保持被借用状态。
Sandeep Datta:我可以用 'b 表示 self,'a 表示 Chars 来避免这个问题吗?
mbrubeck:如果你实际上从 self 返回了某些东西的迭代器,那么不行。但是如果你可以创建一个函数,它的类型是 &self -> Chars(而不是 &mut self -> Chars),那么就可以解决这个问题。

1
即使您删除if let表达式周围的大括号,此代码仍将正常工作。 - Sandeep Datta
答案被接受了,因为您除了解释之外还提供了一种解决方法。 - Sandeep Datta
然而,我认为后续问题尚未得到充分回答。"只要f1的返回值在作用域内,self就会保持借用状态"听起来像是借用检查器现在工作方式的产物。这里有几个不同的生命周期混淆在一起,self.f1的借用与self.f3的借用的生命周期并不相同,但它们都由相同的生命周期'a表示。 - Sandeep Datta

4

3
一份可变引用是一个非常强大的保证:只有一个指针指向特定的内存位置。因为你已经有了一个&mut借用,所以你不能再有第二个。在多线程上下文中,这会引入数据竞争,在单线程上下文中则会引起迭代器失效和其他类似问题。
现在,借用是基于词法作用域的,因此第一个借用持续到函数结束,没有例外。最终,我们希望放宽这个限制,但这需要一些工作。

我猜仅仅有两个可变引用指向同一数据并不会自动引入数据竞争 :) 至少需要两个线程才能引发数据竞争。 - Vladimir Matveev
4
可能不是数据竞争,但仍存在问题,例如迭代器失效。 - Steve Klabnik
是的,当然。我只是针对“数据竞争”这个术语提出了异议。 - Vladimir Matveev

3

以下是如何消除虚假的错误。我对Rust还很陌生,所以下面的解释可能存在严重错误。

use std::str::Chars;

struct A<'a> {
    chars: Chars<'a>,
}

'a在这里是一个生命周期参数(就像C++中的模板参数一样)。在Rust中,类型可以通过生命周期进行参数化。

Chars类型也采用了生命周期参数。这意味着Chars类型可能有一个需要生命周期参数的成员元素。生命周期参数只对引用有意义(因为此处的生命周期实际上是“借用的生命周期”)。

我们知道Chars需要保留对其创建字符串的引用,'a可能会用来表示源字符串的生命周期。

在这里,我们简单地将'a作为生命周期参数提供给Chars,告诉Rust编译器Chars的生命周期与结构体A的生命周期相同。在我看来,“类型A的生命周期'a”应该被理解为“包含在结构体A中的引用的生命周期'a'”。

我认为结构体的实现可以独立于结构体本身进行参数化,因此我们需要使用impl关键字重复参数。在这里,我们将名称'a'绑定到结构体A的生命周期。

impl<'a> A<'a> {

名称'b是在函数f2的上下文中引入的。这里它用于与引用&mut self的生命周期绑定。

fn f2<'b>(&'b mut self) {}

在函数 f1 的上下文中引入了名称为 'b。这个 'b 与上面的 f2 引入的 'b 没有直接关系。
在这里,它用于绑定到引用 &mut self 的生命周期。毫无疑问,这个引用也与上一个函数中的 &mut self 没有任何关系,这是一个新的独立借用 self
如果我们没有在这里使用显式生命周期注释,Rust 将使用其生命周期省略规则来得出以下函数签名...
//fn f1<'a>(&'a mut self) -> Option<Chars<'a>>

你可以看到,这将引用&mut self参数的生命周期绑定到从该函数返回的Chars对象的生命周期上(此Chars对象不需要与self.chars相同),但这是荒谬的,因为返回的Chars将比&mut self引用更长寿。因此,我们需要按以下方式分离两个生命周期...

fn f1<'b>(&'b mut self) -> Option<Chars<'a>> {
    self.chars.next();

请记住,&mut self 是对 self 的借用,而任何被 &mut self 引用的内容也都是借用。因此我们不能在这里返回 Some(self.chars)self.chars 不属于我们(错误:无法从借用内容中移动)。

我们需要创建一个 self.chars 的克隆,以便它可以被提供出去。

Some(self.chars.clone())

请注意,返回的Chars与结构体A具有相同的生命周期。
现在,f3保持不变且没有编译错误!
fn f3<'b>(&'b mut self)  {
    if let Some(x) = self.f1() { //This is ok now

    } else {
        self.f2() //This is also ok now
    }
}

主要功能仅为完整性考虑...
fn main() {
    let mut a = A { chars:"abc".chars() };

    a.f3();

    for c in a.chars {
        print!("{}", c);
    }
}

我已更新代码,使生命周期关系更清晰。


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