首页 文章

借用的值不够长(BufReader lines()到String的迭代器)

提问于
浏览
2

使用此示例代码:

use std::fs::{File};
use std::io::{BufRead, BufReader};
use std::path::Path;

type BoxIter<T> = Box<Iterator<Item=T>>;

fn tokens_from_str<'a>(text: &'a str) 
-> Box<Iterator<Item=String> + 'a> {
    Box::new(text.lines().flat_map(|s|
        s.split_whitespace().map(|s| s.to_string())
    ))
}

// Returns an iterator of an iterator. The use case is a very large file where
// each line is very long. The outer iterator goes over the file's lines.
// The inner iterator returns the words of each line.
pub fn tokens_from_path<P>(path_arg: P) 
-> BoxIter<BoxIter<String>>
where P: AsRef<Path> {
    let reader = reader_from_path(path_arg);
    let iter = reader.lines()
        .filter_map(|result| result.ok())
        .map(|s| tokens_from_str(&s));
    Box::new(iter)
}

fn reader_from_path<P>(path_arg: P) -> BufReader<File>
where P: AsRef<Path> {
    let path = path_arg.as_ref();
    let file = File::open(path).unwrap();
    BufReader::new(file)
}

我收到此编译器错误消息:

rustc 1.18.0 (03fc9d622 2017-06-06)
error: `s` does not live long enough
  --> <anon>:23:35
   |
23 |         .map(|s| tokens_from_str(&s));
   |                                   ^- borrowed value only lives until here
   |                                   |
   |                                   does not live long enough
   |
   = note: borrowed value must be valid for the static lifetime...

我的问题是:

  • 如何修复(如果可能的话,不更改功能签名?)

  • 有关更好的函数参数和返回值的任何建议吗?

3 回答

  • 2

    一个问题是 .split_whitespace() 接受引用,并且不拥有其内容 . 因此,当您尝试使用拥有的对象构造 SplitWhitespace 对象时(当您调用 .map(|s| tokens_from_str(&s)) 时会发生这种情况),字符串 s 将被删除,而 SplitWhitespace 仍在尝试引用它 . 我通过创建一个获取 String 所有权的结构并根据需要产生 SplitWhitespace 来快速解决这个问题 .

    use std::fs::File;
    use std::io::{BufRead, BufReader};
    use std::path::Path;
    use std::iter::IntoIterator;
    use std::str::SplitWhitespace;
    
    pub struct SplitWhitespaceOwned(String);
    
    impl<'a> IntoIterator for &'a SplitWhitespaceOwned {
        type Item = &'a str;
        type IntoIter = SplitWhitespace<'a>;
        fn into_iter(self) -> Self::IntoIter {
            self.0.split_whitespace()
        }
    }
    
    // Returns an iterator of an iterator. The use case is a very large file where
    // each line is very long. The outer iterator goes over the file's lines.
    // The inner iterator returns the words of each line.
    pub fn tokens_from_path<P>(path_arg: P) -> Box<Iterator<Item = SplitWhitespaceOwned>>
        where P: AsRef<Path>
    {
        let reader = reader_from_path(path_arg);
        let iter = reader
            .lines()
            .filter_map(|result| result.ok())
            .map(|s| SplitWhitespaceOwned(s));
        Box::new(iter)
    }
    
    fn reader_from_path<P>(path_arg: P) -> BufReader<File>
        where P: AsRef<Path>
    {
        let path = path_arg.as_ref();
        let file = File::open(path).unwrap();
        BufReader::new(file)
    }
    
    fn main() {
        let t = tokens_from_path("test.txt");
    
        for line in t {
            for word in &line {
                println!("{}", word);
            }
        }
    }
    
  • 0

    Disclaimer: Frame Challenge

    处理大文件时,最简单的解决方案是使用Memory Mapped Files .

    也就是说,您告诉操作系统您希望整个文件可以在内存中访问,并且由它来处理文件内部和内存的分页部分 .

    一旦关闭,那么您的整个文件可以作为 &[u8]&str (在您方便时)访问,并且您可以轻松访问它的切片 .

    它可能并不总是最快的解决方案;它当然是最简单的 .

  • 2

    这里的问题是你使用 to_string() 将每个项目变成一个拥有的值,这是懒惰的 . 由于它是惰性的,因此使用to_string之前的值( &str )仍然存在于返回的迭代器状态中,因此无效(因为一旦 map 闭包返回,源字符串就会被删除) .

    天真的解决方案

    这里最简单的解决方案是删除迭代器的那部分的惰性求值,并在分配行时立即分配所有令牌 . 这将不会那么快,并且将涉及额外的分配,但是对当前函数的改动很小,并保持相同的签名:

    // Returns an iterator of an iterator. The use case is a very large file where
    // each line is very long. The outer iterator goes over the file's lines.
    // The inner iterator returns the words of each line.
    pub fn tokens_from_path<P>(path_arg: P) -> BoxIter<BoxIter<String>>
    where
        P: AsRef<Path>
    {
        let reader = reader_from_path(path_arg);
        let iter = reader.lines()
            .filter_map(|result| result.ok())
            .map(|s| {
                let collected = tokens_from_str(&s).collect::<Vec<_>>();
    
                Box::new(collected.into_iter()) as Box<Iterator<Item=String>>
            });
    
        Box::new(iter)
    }
    

    此解决方案适用于任何小型工作负载,并且它将仅为该线路同时分配大约两倍的内存 . 有性能损失,但除非你有10mb线,否则可能无关紧要 .

    如果您确实选择了此解决方案,我建议您更改 tokens_from_path 的函数签名以直接返回 BoxIter<String>

    pub fn tokens_from_path<P>(path_arg: P) -> BoxIter<String>
    where
        P: AsRef<Path>
    {
        let reader = reader_from_path(path_arg);
        let iter = reader.lines()
            .filter_map(|result| result.ok())
            .flat_map(|s| {
                let collected = tokens_from_str(&s).collect::<Vec<_>>();
    
                Box::new(collected.into_iter()) as Box<Iterator<Item=String>>
            });
    
        Box::new(iter)
    }
    

    备选:解耦tokens_from_path和tokens_from_str

    原始代码不起作用,因为您试图将借用返回到您不返回的String .

    我们可以通过返回String来修复它 - 只是隐藏在不透明的API后面 . 这非常类似于breeden's solution,但在执行方面略有不同 .

    use std::fs::{File};
    use std::io::{BufRead, BufReader};
    use std::path::Path;
    
    type BoxIter<T> = Box<Iterator<Item=T>>;
    
    /// Structure representing in our code a line, but with an opaque API surface.
    pub struct TokenIntermediate(String);
    
    impl<'a> IntoIterator for &'a TokenIntermediate {
        type Item = String;
        type IntoIter = Box<Iterator<Item=String> + 'a>;
    
        fn into_iter(self) -> Self::IntoIter {
            // delegate to tokens_from_str
            tokens_from_str(&self.0)
        }
    }
    
    fn tokens_from_str<'a>(text: &'a str) -> Box<Iterator<Item=String> + 'a> {
        Box::new(text.lines().flat_map(|s|
            s.split_whitespace().map(|s| s.to_string())
        ))
    }
    
    // Returns an iterator of an iterator. The use case is a very large file where
    // each line is very long. The outer iterator goes over the file's lines.
    // The inner iterator returns the words of each line.
    pub fn token_parts_from_path<P>(path_arg: P) -> BoxIter<TokenIntermediate>
    where
        P: AsRef<Path>
    {
        let reader = reader_from_path(path_arg);
        let iter = reader.lines()
            .filter_map(|result| result.ok())
            .map(|s| TokenIntermediate(s));
    
        Box::new(iter)
    }
    
    fn reader_from_path<P>(path_arg: P) -> BufReader<File>
    where P: AsRef<Path> {
        let path = path_arg.as_ref();
        let file = File::open(path).unwrap();
        BufReader::new(file)
    }
    

    正如您所注意到的, tokens_from_str 没有区别,而 tokens_from_path 只返回这个不透明的 TokenIntermediate 结构 . 这将与原始解决方案一样可用,它只是将中间 String 值的所有权推送到调用者,因此他们可以迭代它们中的令牌 .

相关问题