3

我正在尝试编写一个函数,该函数仅在元素大于向量中已经存在的最后一个元素时将元素推到排序向量的末尾,否则返回错误,并带有对最大元素的引用。据我所知,这似乎没有违反任何借用规则,但借用检查器不喜欢它。我不明白为什么。

struct MyArray<K, V>(Vec<(K, V)>);

impl<K: Ord, V> MyArray<K, V> {
    pub fn insert_largest(&mut self, k: K, v: V) -> Result<(), &K> {
        {
            match self.0.iter().next_back() {
                None => (),
                Some(&(ref lk, _)) => {
                    if lk > &k {
                        return Err(lk);
                    }
                }
            };
        }
        self.0.push((k, v));
        Ok(())
    }
}

error[E0502]: cannot borrow `self.0` as mutable because it is also borrowed as immutable
  --> src/main.rs:15:9
   |
6  |             match self.0.iter().next_back() {
   |                   ------ immutable borrow occurs here
...
15 |         self.0.push((k, v));
   |         ^^^^^^ mutable borrow occurs here
16 |         Ok(())
17 |     }
   |     - immutable borrow ends here

为什么这不起作用?


回应Paolo Falabella 的回答

我们可以将任何带有 return 语句的函数转换为没有 return 语句的函数,如下所示:

fn my_func() -> &MyType {
    'inner: {
        // Do some stuff
        return &x;
    }
    // And some more stuff
}

进入

fn my_func() -> &MyType {
    let res;
    'outer: {
        'inner: {
            // Do some stuff
            res = &x;
            break 'outer;
        }
        // And some more stuff
    }
    res
}

由此,很明显,借用超出了 的范围'inner

为了借用检查而使用以下重写有什么问题吗?

fn my_func() -> &MyType {
    'outer: {
        'inner: {
            // Do some stuff
            break 'outer;
        }
        // And some more stuff
    }
    panic!()
}

考虑到 return 语句可以防止之后发生任何可能违反借用规则的事情。

4

2 回答 2

7

如果我们明确命名生命周期,则签名insert_largest变为fn insert_largest<'a>(&'a mut self, k: K, v: V) -> Result<(), &'a K>。因此,当您创建返回类型时&K,它的生命周期将与&mut self.

而且,实际上,您正在lk从内部获取和返回self。编译器看到对的引用lk超出了匹配的范围(因为它被分配给函数的返回值,所以它必须比函数本身更长寿)并且当匹配结束时它不能让借用结束。

我认为您是在说编译器应该更聪明,并意识到self.0.push只有lk在没有返回的情况下才能达到。但事实并非如此。而且我什至不确定教它进行这种分析有多难,因为它比我今天理解借用检查器原因的方式要复杂一些。

今天,编译器看到一个参考,基本上试图回答一个问题(“这能活多久?”)。当它看到您的返回值为时,它会根据 fn 的签名(使用我们上面给出的明确名称)为返回值lk分配lk它期望的生命周期,并将其称为一天。'a

所以,简而言之:

  • 是否应该提前返回结束对自身的可变借用?不,正如所说,借用应该扩展到函数之外并遵循它的返回值
  • 在从早期返回到函数结束的代码中,借用检查器是否有点过于严格?是的,我想是这样。提前返回之后和函数结束之前的部分只有在函数没有提前返回的情况下才可以访问,所以我认为你有一个观点,即在特定的代码区域中,借用检查的借用可能不那么严格
  • 我认为更改编译器以启用该模式是否可行/可取?我没有任何线索。借用检查器是 Rust 编译器中最复杂的部分之一,我没有资格对此给出答案。这似乎与关于非词法借用范围的讨论有关(甚至可能是其中的一个子集),因此我鼓励您研究它,如果您对此主题感兴趣,可能会做出贡献。

如果可能的话,我暂时建议只返回一个克隆而不是参考。我假设返回 anErr不是典型情况,因此性能不应该是一个特别的问题,但我不确定K:Clone绑定如何与您使用的类型一起工作。

impl <K, V> MyArray<K, V> where K:Clone + Ord { // 1. now K is also Clone
    pub fn insert_largest(&mut self, k: K, v: V) -> 
                                    Result<(), K> { // 2. returning K (not &K)
        match self.0.iter().next_back() {
            None => (),
            Some(&(ref lk, _)) => {
                if lk > &k {
                    return Err(lk.clone()); // 3. returning a clone
                }
            }
        };
        self.0.push((k, v));
        Ok(())
    }
}
于 2016-01-18T11:11:20.250 回答
1

为什么提前归还没有完成未偿还的借款?

因为借用检查器的当前实现过于保守。

启用非词法生命周期后,您的代码将按原样工作,但仅适用于实验性的“Polonius”实现。Polonius 能够有条件地跟踪借用。

我还稍微简化了您的代码:

#![feature(nll)]

struct MyArray<K, V>(Vec<(K, V)>);

impl<K: Ord, V> MyArray<K, V> {
    pub fn insert_largest(&mut self, k: K, v: V) -> Result<(), &K> {
        if let Some((lk, _)) = self.0.iter().next_back() {
            if lk > &k {
                return Err(lk);
            }
        }

        self.0.push((k, v));
        Ok(())
    }
}
于 2018-07-15T17:28:07.660 回答