【问题标题】:Why is a borrow still held in the else block of an if let?为什么借用仍然在 if let 的 else 块中?
【发布时间】:2015-05-14 17:28:54
【问题描述】:

为什么下面代码中的调用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()
}
// ...

Playground

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

【问题讨论】:

    标签: rust


    【解决方案1】:

    我在这里整理了一个示例来展示范围规则:

    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
    }
    

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

    【讨论】:

      【解决方案2】:

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

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

      正如 cmets 中所指出的,这无需额外的大括号即可工作。这是因为 ifif...let 表达式具有隐式作用域,并且借用在此作用域内持续:

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

      这是 Sandeep Datta 和 mbrubeck 之间的 IRC 聊天记录:

      mbrubeck: std:tr::Chars 包含对创建它的字符串的借用引用。完整的类型名称是Chars&lt;'a&gt;。所以f1(&amp;mut self) -&gt; Option&lt;Chars&gt; 没有省略是f1(&amp;'a mut self) -&gt; Option&lt;Chars&lt;'a&gt;&gt; 这意味着self 保持借用只要 f1 的返回值在范围内。

      Sandeep Datta:我可以使用 'b 代表 self 和 'a 代表 Chars 来避免这个问题吗?

      mbrubeck: 如果您实际上是从self 返回一个迭代器,则不会。虽然如果你可以从&amp;self -&gt; Chars(而不是&amp;mut self -&gt; Chars)创建一个函数,就可以解决这个问题。

      【讨论】:

      • 即使您删除了 if let 表达式周围的大括号,这仍然有效。
      • 答案被接受,因为除了解释之外,您还提供了解决方法。
      • 但是我认为后续问题尚未得到完全回答。 “只要 f1 的返回值在范围内,self 就会一直被借用。”听起来像是借用检查器现在工作方式的神器。这里有几个不同的生命周期相互混合,self.f1 借用的生命周期与 self.f3 借用的生命周期不同,但它们由相同的生命周期 'a 表示。
      【解决方案3】:

      从 Rust 2018 开始,在 Rust 1.31 中可用,original code will work as-is。这是因为 Rust 2018 启用了non-lexical lifetimes

      【讨论】:

        【解决方案4】:

        可变引用是一个非常强有力的保证:只有一个指向特定内存位置的指针。既然你已经借了一个&amp;mut,你就不能再有第二个了。这将在多线程上下文中引入数据竞争,并在单线程上下文中引入迭代器失效和其他类似问题。

        现在,借用是基于词法作用域的,所以第一个借用一直持续到函数句点结束。最终,我们希望放宽这个限制,但这需要一些工作。

        【讨论】:

        • 我想,仅仅对相同数据的两个可变引用的可用性不会单独引入数据竞争:) 至少,你需要两个线程来进行数据竞赛。
        • 可能不是数据竞争,但还是有问题,比如迭代器失效
        • 是的,当然。我只是专门反对“数据​​竞赛”这个词。
        【解决方案5】:

        以下是消除虚假错误的方法。我是 Rust 新手,所以下面的解释可能存在严重错误。

        use std::str::Chars;
        
        struct A<'a> {
            chars: Chars<'a>,
        }
        

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

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

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

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

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

        impl<'a> A<'a> {
        

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

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

        名称'b是在函数f1的上下文中引入的。这个'b与上面f2引入的'b没有直接关系。

        这里用来绑定引用&amp;mut self的生命周期。这个引用不用说也和前面函数中的&amp;mut self没有任何关系,这是对self的新独立借用。

        如果我们没有在这里使用显式生命周期注解,Rust 会使用其生命周期省略规则来得出以下函数签名...

        //fn f1<'a>(&'a mut self) -> Option<Chars<'a>>
        

        如您所见,这将引用&amp;mut self 参数的生命周期绑定到从该函数返回的Chars 对象的生命周期(此Chars 对象不必与self.chars 相同)这是荒谬,因为返回的Chars 将比&amp;mut self 引用寿命长。因此,我们需要将两个生命周期分开如下...

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

        记住&amp;mut selfself 的借用,&amp;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);
            }
        }
        

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

        【讨论】:

          猜你喜欢
          • 2019-11-30
          • 1970-01-01
          • 1970-01-01
          • 2021-08-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多