循环中的可变借用时间过长

3

我有一段需要存储字符串并访问这些字符串引用的代码。我最初编写的代码如下:

struct Pool {
    strings : Vec<String>
}

impl Pool {
    pub fn new() -> Self {
        Self {
            strings: vec![]
        }
    }

    pub fn some_f(&mut self) -> Vec<&str> {
        let mut v = vec![];
        
        for i in 1..10 {
            let string = format!("{}", i);
            let string_ref = self.new_string(string);
            v.push(string_ref);
        }
    
        v
    }
    
    fn new_string(&mut self, string : String) -> &str {
        self.strings.push(string);
        &self.strings.last().unwrap()[..]
    }
}

这不通过借用检查器:
error[E0499]: cannot borrow `*self` as mutable more than once at a time
  --> src/main.rs:19:30
   |
14 |     pub fn some_f(&mut self) -> Vec<&str> {
   |                   - let's call the lifetime of this reference `'1`
...
19 |             let string_ref = self.new_string(string);
   |                              ^^^^ mutable borrow starts here in previous iteration of loop
...
23 |         v
   |         - returning this value requires that `*self` is borrowed for `'1`

据说借用检查器不够聪明,无法意识到可变借用并未超出对new_string的调用范围。我尝试将修改结构的部分与检索引用分离,得到了以下代码:

use std::vec::*;

struct Pool {
    strings : Vec<String>
}

impl Pool {
    pub fn new() -> Self {
        Self {
            strings: vec![]
        }
    }

    pub fn some_f(&mut self) -> Vec<&str> {
        let mut v = vec![];
        
        for i in 1..10 {
            let string = format!("{}", i);
            self.new_string(string);
        }
        for i in 1..10 {
            let string = &self.strings[i - 1];
            v.push(&string[..]);
        }
    
        v
    }
    
    fn new_string(&mut self, string : String) {
        self.strings.push(string);
    }
}

这段代码在语义上等价(希望如此)且可以编译通过。然而,将两个for循环尽可能地合并为一个:

for i in 1..10 {
    let string = format!("{}", i);
    self.new_string(string);
    let string = &self.strings[i - 1];
    v.push(&string[..]);
}

会出现类似的借用错误:

error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
  --> src/main.rs:19:13
   |
14 |     pub fn some_f(&mut self) -> Vec<&str> {
   |                   - let's call the lifetime of this reference `'1`
...
19 |             self.new_string(string);
   |             ^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
20 |             let string = &self.strings[i - 1];
   |                           ------------ immutable borrow occurs here
...
24 |         v
   |         - returning this value requires that `self.strings` is borrowed for `'1`

我有几个问题:

  1. 为什么在这种情况下,借用检查器会如此严格,以至于它会延长可变借用的整个循环期间?分析传递给 new_string&mut 不泄漏到该函数调用之外难道不可能/非常困难吗?

  2. 是否可以通过自定义生命周期来解决这个问题,以便我可以返回既改变又返回引用的原始帮助程序?

  3. 有没有一种不会打乱借用检查器的不同方式,我可以在其中实现我想要的内容,即具有自我修改并返回引用的结构?

我发现了这个问题,但我不理解答案(它是对问题#2的否定回答吗?毫无头绪)而且大多数其他问题都存在明确的生命周期参数问题。我的代码只使用推断的生命周期。


2
这里有一个非常好的解释:https://dev59.com/wVYN5IYBdhLWcg3wT2xa。希望你能在那里找到答案。 - Ibraheem Ahmed
1个回答

1
在这种情况下,借用检查器的做法是正确的,不允许这样做:
    self.new_string(string);
    let string = &self.strings[i - 1];
    v.push(&string[..]);

self.new_string 可能会导致您推送到 v 的所有先前引用无效,因为它可能需要为 strings 分配内存并移动其内容。借用检查器可以捕获这种情况,因为您推送到 v 的引用需要与 v 的生命周期匹配,因此整个方法必须借用 &self.strings(因此也是&self),这会防止您的可变借用。

如果使用两个循环,则在调用 new_string 时不会存在共享借用。

您可以看到,在这个(完全无用的)循环版本中,延长可变借用并不是问题所在,它可以编译:

for i in 1..10 {
    let string = format!("{}", i);
    self.new_string(string);
    let mut v2 = vec![];
    let string = &self.strings[i - 1];
    v2.push(&string[..]);
}

关于更为惯用的方法,Vec类在可变操作中可以使引用无效,因此您不能在安全的rust中做您想要的事情。即使编译器允许您使用c++ vector,您也不希望这样做,除非您预先分配了向量并手动确保您永远不会推送比最初分配的元素更多的元素。显然,rust不希望您手动验证程序的内存安全性;预分配的大小在类型系统中不可见,并且无法由借用检查器检查,因此这种方法不可行。
即使您使用固定大小的容器如[String; 10],也无法解决这个问题。在这种情况下,可能没有分配,但实际上使其安全的是您从未更新过已经取出引用的索引。但是,rust没有部分借用容器的概念,因此无法告诉它“直到索引n为止存在共享借用,因此对于我来说,在索引n + 1处进行可变借用是可以的”。
如果出于性能原因确实需要单个循环,则需要预先分配并使用unsafe块,例如:
struct Pool {
    strings: Vec<String>,
}

const SIZE: usize = 10;

impl Pool {
    pub fn new() -> Self {
        Self {
            strings: Vec::with_capacity(SIZE),
        }
    }

    pub fn some_f(&mut self) -> Vec<&str> {
        let mut v: Vec<&str> = vec![];

        // We've allocated 10 elements, but the loop uses 9, it's OK as long as it's not the other way around!
        for i in 1..SIZE {
            let string = format!("{}", i);
            self.strings.push(string);
            let raw = &self.strings as *const Vec<String>;
            unsafe {
                let last = (*raw).last().unwrap();
                v.push(last);
            }
        }

        v
    }
}

一个可能的替代方案是使用 Rc,但是如果你想要一个单一的循环是为了性能,那么使用堆 + 引用计数的运行时成本可能会带来不好的权衡。无论如何,以下是代码:
use std::rc::Rc;

struct Pool {
    strings: Vec<Rc<String>>,
}

impl Pool {
    pub fn new() -> Self {
        Self { strings: vec![] }
    }

    pub fn some_f(&mut self) -> Vec<Rc<String>> {
        let mut v = vec![];

        for i in 1..10 {
            let string = format!("{}", i);
            let rc = Rc::new(string);
            let result = rc.clone();
            self.strings.push(rc);
            v.push(result);
        }

        v
    }
}

谢谢,这确实有道理。有没有一种通过间接层来修复这个问题的方法?如果我不是取 Vec 内部的引用,而是将字符串存储在堆上并在 Vec 中保留 Box 版本,那么 &str 切片会有什么生命周期呢? - V0ldek
我认为在这里使用一个Box并不能帮助你,数据可能在堆上,但是Box变量的生命周期和借用将与你现在拥有的局部变量相同。 - Diego Veralli
但这会解决根本问题,对吧?如果Vec重新分配内存只移动Box对象而不是实际的String对象,那么对该String的引用在重分配后就不会无效。而且我不需要借用盒子,我只需要底层引用。我不是说我知道如何注释生命周期,但从内存管理的角度来看,我不明白为什么不能通过另一层间接来实现。 - V0ldek
Box 拥有底层数据,你不能有两个指向堆的同一部分的盒子,因此任何涉及盒子的解决方案最终都会复制字符串。在这种情况下最好自己复制字符串。但是你可以使用 Rc 引用来实现你想要的功能。我将在答案中添加详细信息。 - Diego Veralli

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