借贷检查器没有意识到 `clear` 函数会丢弃对本地变量的引用

17
以下代码从标准输入中读取以空格分隔的记录,并将逗号分隔的记录写入标准输出。即使进行了优化构建,它仍然相当缓慢(大约比使用 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 的引用,对吗?如何告诉借用检查器这一点?

“不能保留对 line 的引用,对吧?” → 对。但是借用检查如何知道呢? - mcarton
1
我想指出line是一个已分配的String:即使Vec缓存起作用,每次迭代仍需要进行新的内存分配。 - Matthieu M.
@mcarton:确切地说,这就是为什么我问如何告诉借用检查器的原因 :) - Clément
@MatthieuM。我很想知道如何重复使用该字符串的内存 :) 但最好作为一个单独的问题。 - Clément
3
为了避免每次重新分配line,您应该使用read_line而不是lines - mcarton
显示剩余2条评论
6个回答

10
这样做的唯一方法是使用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
我认为这应该有一个安全的抽象,但我想不到任何现有的抽象。例如,recycler可能具有相同的生命周期限制。 - bluss

9
在这种情况下,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");
}

顺便说一下:Francis的答案中使用的transmute也很好。你可以使用unsafe来表明你知道自己在做什么,并覆盖生命周期检查。


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

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

虽然你在问题中提到了这个方法,但我觉得有更优雅的方法可以使用迭代器来完成,这可能避免了完全分配向量。

上述方法是受到类似问题的启发,如果你需要做比打印更复杂的事情,那么它会变得更加有用。


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

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

-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(..);
    }
}

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