Search code examples
asynchronousrustrust-tokio

async fn calling split generates error "borrowed value does not live long enough"


I've created a contrived example to understand an error I'm seeing with an async function using tokio:

pub async fn unique_words_async() -> io::Result<u32> {
    let file = File::open("sample.txt").await?;
    let reader = BufReader::new(file);
    let mut lines = reader.lines();

    let mut h = HashMap::new();
    h.insert("word", true);
    while let Some(line) = lines.next_line().await? {
        let mut split = line.trim().split(' ');
        while let Some(word) = split.next() {
            h.insert(word, true);
        }
    }
    Ok(h.len() as u32)
}

here's the exact error:

16 |         let mut split = line.trim().split(' ');
   |                         ^^^^^^^^^^^ borrowed value does not live long enough
...
20 |     }
   |     - `line` dropped here while still borrowed
21 |     Ok(h.len() as u32)
   |        ------- borrow later used here

see running live in playground

The non-async code to accomplish the same task works fine (see playground):

pub fn unique_words() -> u32 {
    let input = "this is one line\nhere is another line";
    let mut lines = input.lines();
    let mut h = std::collections::HashMap::new();
    while let Some(line) = lines.next() {
        let mut split = line.split(' ');
        while let Some(word) = split.next() {
            h.insert(word, true);
        }
    }
    h.len() as u32
}

Solution

  • Your non-async translation is incorrect. If you will use File with BufReader, you will have the same error:

    pub fn unique_words() -> std::io::Result<u32> {
        use std::io::BufRead;
    
        let file = std::fs::File::open("sample.txt")?;
        let reader = std::io::BufReader::new(file);
        let mut lines = reader.lines();
    
        let mut h = std::collections::HashMap::new();
        while let Some(line) = lines.next().transpose()? {
            let mut split = line.split(' ');
            while let Some(word) = split.next() {
                h.insert(word, true);
            }
        }
        Ok(h.len() as u32)
    }
    
    error[E0597]: `line` does not live long enough
      --> src/main.rs:10:25
       |
    10 |         let mut split = line.split(' ');
       |                         ^^^^^^^^^^^^^^^ borrowed value does not live long enough
    ...
    14 |     }
       |     - `line` dropped here while still borrowed
    15 |     Ok(h.len() as u32)
       |        ------- borrow later used here
    

    Playground.

    This is because Lines::next() returns Option<io::Result<String>>, and the String is dropped at the end of each loop turn. However, str::trim() and str::split() takes &str and produce &str with the same lifetime (as they only slice the string, not change it). Thus you push into h a &str with the lifetime of one loop turn, that is, a dangling reference.

    The reason it worked with str::lines() is that std::str::Lines::next() returns Option<&str> with the reference tied to the original &str, which is 'static.

    The simplest way to fix that is to convert the &str to an owned String:

    h.insert(word.to_owned(), true);
    

    Playground.