【问题标题】:Borrow checker doesn't realize that `clear` drops reference to local variable借用检查器没有意识到 `clear` 删除了对局部变量的引用
【发布时间】:2017-02-18 04:13:21
【问题描述】:

以下代码从标准输入读取以空格分隔的记录,并将逗号分隔的记录写入标准输出。即使使用优化的构建,它也相当慢(大约是使用 awk 的两倍)。

use std::io::BufRead;

fn main() {
    let stdin = std::io::stdin();
    for line in stdin.lock().lines().map(|x| x.unwrap()) {
        let fields: Vec<_> = line.split(' ').collect();
        println!("{}", fields.join(","));
    }
}

一个明显的改进是使用itertools 加入而不分配向量(collect 调用会导致分配)。但是,我尝试了另一种方法:

fn main() {
    let stdin = std::io::stdin();
    let mut cache = Vec::<&str>::new();
    for line in stdin.lock().lines().map(|x| x.unwrap()) {
        cache.extend(line.split(' '));
        println!("{}", cache.join(","));
        cache.clear();
    }
}

此版本尝试一遍又一遍地重用相同的向量。不幸的是,编译器抱怨:

error: `line` does not live long enough
 --> src/main.rs:7:22
  |
7 |         cache.extend(line.split(' '));
  |                      ^^^^
  |
note: reference must be valid for the block suffix following statement 1 at 5:39...
 --> src/main.rs:5:40
  |
5 |     let mut cache = Vec::<&str>::new();
  |                                        ^
note: ...but borrowed value is only valid for the for at 6:4
 --> src/main.rs:6:5
  |
6 |     for line in stdin.lock().lines().map(|x| x.unwrap()) {
  |     ^

error: aborting due to previous error

这当然是有道理的:line 变量仅在for 循环体中有效,而cache 在迭代中保持指向它的指针。但是这个错误在我看来仍然是虚假的:因为每次迭代后缓存都是 cleared,所以不能保留对 line 的引用,对吧?

我如何告诉借阅检查员这件事?

【问题讨论】:

  • “不能保留对line的引用,对吧?” → 对。但是,borrowck 是怎么知道的呢?
  • 我会注意到line 是分配的String:即使Vec 缓存有效,每次迭代仍然需要新的内存分配。
  • @mcarton:没错:这就是为什么我要问我如何告诉借阅检查员这件事:)
  • @MatthieuM。我很想知道一种方法来重用该字符串的内存:) 但作为一个单独的问题可能会更好。
  • @Clément 为避免每次都重新分配line,您应该使用read_line 而不是lines

标签: rust lifetime borrow-checker


【解决方案1】:

这样做的唯一方法是使用transmuteVec&lt;&amp;'a str&gt; 更改为Vec&lt;&amp;'b str&gt;transmute 是不安全的,如果您忘记在此处调用 clear,Rust 不会引发错误。您可能希望将 unsafe 块扩展到对 clear 的调用之后,以明确(不是双关语)代码返回到“安全地带”的位置。

use std::io::BufRead;
use std::mem;

fn main() {
    let stdin = std::io::stdin();
    let mut cache = Vec::<&str>::new();
    for line in stdin.lock().lines().map(|x| x.unwrap()) {
        let cache: &mut Vec<&str> = unsafe { mem::transmute(&mut cache) };
        cache.extend(line.split(' '));
        println!("{}", cache.join(","));
        cache.clear();
    }
}

【讨论】:

  • 我认为这应该有一个安全的抽象,但我想不出任何现有的抽象。例如recycler 可能具有相同的生命周期限制。
【解决方案2】:

在这种情况下,Rust 不知道您要做什么。不幸的是,.clear() 不会影响.extend() 的检查方式。

cache 是“与主函数一样长的字符串向量”,但在 extend() 调用中,您附加了“仅与一次循环迭代一样长的字符串”,所以这是一种类型不匹配。对.clear() 的调用不会改变类型。

通常这种有限时间的使用是通过制作一个长寿命的不透明对象来表达的,该对象可以通过借用一个具有正确生命周期的临时对象来访问其内存,例如RefCell.borrow()提供一个临时Ref对象。实现它会有点复杂,并且需要不安全的方法来回收Vec 的内部内存。

在这种情况下,另一种解决方案可能是完全避免任何分配(.join() 也分配)并通过 Peekable 迭代器包装器流式传输打印:

for line in stdin.lock().lines().map(|x| x.unwrap()) {
    let mut fields = line.split(' ').peekable();
    while let Some(field) = fields.next() {
        print!("{}", field);
        if fields.peek().is_some() {
            print!(",");
        }
    }
    print!("\n");
}

顺便说一句:弗朗西斯对transmute 的回答也很好。您可以使用unsafe 表示您知道自己在做什么并覆盖生命周期检查。

【讨论】:

    【解决方案3】:

    Itertools 有 .format() 用于延迟格式化,它也跳过分配字符串。

    use std::io::BufRead;
    use itertools::Itertools;
    
    fn main() {
        let stdin = std::io::stdin();
        for line in stdin.lock().lines().map(|x| x.unwrap()) {
            println!("{}", line.split(' ').format(","));
        }
    }
    

    题外话,在此处的另一个答案中,从最小意义上的解决方案来看,这样的事情是“安全抽象”:

    fn repurpose<'a, T: ?Sized>(mut v: Vec<&T>) -> Vec<&'a T> {
        v.clear();
        unsafe {
            transmute(v)
        }
    }
    

    【讨论】:

      【解决方案4】:

      另一种方法是完全避免存储引用,而是存储索引。这个技巧在其他数据结构上下文中也很有用,所以这可能是一个尝试的好机会。

      use std::io::BufRead;
      
      fn main() {
          let stdin = std::io::stdin();
          let mut cache = Vec::new();
          for line in stdin.lock().lines().map(|x| x.unwrap()) {
              cache.push(0);
              cache.extend(line.match_indices(' ').map(|x| x.0 + 1));
              // cache now contains the indices where new words start
      
              // do something with this information
              for i in 0..(cache.len() - 1) {
                  print!("{},", &line[cache[i]..(cache[i + 1] - 1)]);
              }
              println!("{}", &line[*cache.last().unwrap()..]);
              cache.clear();
          }
      }
      

      虽然您自己在问题中发表了评论,但我觉得有必要指出,有更优雅的方法可以使用迭代器来做到这一点,这可能会完全避免分配向量。

      上述方法的灵感来自similar question here,如果您需要做一些比打印更复杂的事情,它会变得更加有用。

      【讨论】:

        【解决方案5】:

        详细说明 Francis 对使用 transmute() 的回答,我认为可以通过这个简单的函数安全地抽象出来:

        pub fn zombie_vec<'a, 'b, T: ?Sized>(mut data: Vec<&'a T>) -> Vec<&'b T> {
            data.clear();
            unsafe {
                std::mem::transmute(data)
            }
        }
        

        使用这个,原始代码将是:

        fn main() {
            let stdin = std::io::stdin();
            let mut cache0 = Vec::<&str>::new();
            for line in stdin.lock().lines().map(|x| x.unwrap()) {
                let mut cache = cache0; // into the loop
                cache.extend(line.split(' '));
                println!("{}", cache.join(","));
                cache0 = zombie_vec(cache); // out of the loop
            }
        }
        

        您需要将外部向量移动到每个循环迭代中,并在完成之前将其恢复,同时安全地擦除本地生命周期。

        【讨论】:

          【解决方案6】:

          安全的解决方案是使用.drain(..) 而不是.clear(),其中.. 是“全范围”。它返回一个迭代器,因此可以在循环中处理耗尽的元素。它也可用于其他集合(StringHashMap 等)

          fn main() {
              let mut cache = Vec::<&str>::new();
              for line in ["first line allocates for", "second"].iter() {
                  println!("Size and capacity: {}/{}", cache.len(), cache.capacity());
                  cache.extend(line.split(' '));
                  println!("    {}", cache.join(","));
                  cache.drain(..);
              }
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-02-11
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-04-12
            相关资源
            最近更新 更多