借用检查器没有意识到`clear`会丢弃对局部变量的引用

时间:2016-10-09 17:27:29

标签: rust lifetime borrow-checker

以下代码从stdin读取空格分隔的记录,并将逗号分隔的记录写入stdout。即使使用优化的构建,它也相当慢(大约是使用的两倍,比如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在迭代中保持指针。但是这个错误对我来说仍然是虚假的:因为每次迭代后缓存都是clear,所以不能保留对line的引用,对吗?

我如何告知借阅检查员?

6 个答案:

答案 0 :(得分:8)

执行此操作的唯一方法是使用transmuteVec<&'a str>更改为Vec<&'b str>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();
    }
}

答案 1 :(得分:8)

在这种情况下,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表示您知道自己在做什么,并覆盖终身检查。

答案 2 :(得分:5)

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)
    }
}

答案 3 :(得分:2)

另一种方法是不完全存储引用,而存储索引。此技巧在其他数据结构环境中也可能有用,因此这可能是一个很好的机会来尝试一下。

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启发的,如果您需要执行比打印更复杂的操作,它将变得更加有用。

答案 4 :(得分:0)

我认为,弗朗西斯(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
    }
}

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

答案 5 :(得分:-1)

安全的解决方案是使用.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(..);
    }
}