3

I'm writing a Rust program to collect the first words of each input line, which is somewhat similar to the Unix utility cut.

use std::io;


fn main() {
    let mut words = Vec::new();

    let mut input = String::new();

    loop {
        match io::stdin().read_line(&mut input) {
            std::result::Result::Ok(_) => {
                let words_line: Vec<&str> = input.split_whitespace().collect();
                match words_line.get(0) {
                    Some(&word) => {
                        words.push(word.clone());
                    },
                    _ => continue,
                }
            }
            std::result::Result::Err(_) => break
        }
    }

    println!("{:?}", words);
}

This gives me

$ cargo run
   Compiling foo v0.1.0 (/home/ubuntu/projects/foo)
error[E0502]: cannot borrow `input` as mutable because it is also borrowed as immutable
  --> src/main.rs:10:37
   |
10 |         match io::stdin().read_line(&mut input) {
   |                                     ^^^^^^^^^^ mutable borrow occurs here
11 |             std::result::Result::Ok(_) => {
12 |                 let words_line: Vec<&str> = input.split_whitespace().collect();
   |                                             ----- immutable borrow occurs here
...
24 |     println!("{:?}", words);
   |                      ----- immutable borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
error: could not compile `foo`

To learn more, run the command again with --verbose.

I have read Cannot borrow as mutable because it is also borrowed as immutable but am still confused: the mutable borrow happens on line 10 whereas the immutable one happens on line 12, so how could "a variable already borrowed as immutable was borrowed as mutable" happen? At least the error should be "a variable already borrowed as mutable (on line 10) was borrowed as immutable (on line 12)".

nalzok
  • 14,965
  • 21
  • 72
  • 139

2 Answers2

1

I think you want to make words a Vec<String> (as of now, Rust tries to infer a Vec<&str> which gives lifetime problems, as the elements would refer to input which is changed upon the next loop-iteration.

use std::io;


fn main() {
    let mut words : Vec<String> = Vec::new();

    let mut input = String::new();

    loop {
        match io::stdin().read_line(&mut input) {
            std::result::Result::Ok(_) => {
                let words_line: Vec<&str> = input.split_whitespace().collect();
                match words_line.get(0) {
                    Some(&word) => {
                        words.push(word.to_string());
                    },
                    _ => continue,
                }
            }
            std::result::Result::Err(_) => break
        }
    }

    println!("{:?}", words);
}

As a side note, collecting into words_line seems unnecessary: You can simply call next (instead of collect) to find out if there is a first element.

phimuemue
  • 34,669
  • 9
  • 84
  • 115
  • Thanks, that's interesting, but why didn't `words.push(word.clone());` solve the "lifetime problems"? Since I'm creating new objects and pushing them into `words`, there shouldn't be a chance of any invalid reference :/ – nalzok May 25 '21 at 11:13
  • 1
    @nalzok Doesn't `word.clone` still point into `input`, which is altered in the next iteration (so that pointing into it is referring to stale data)? – phimuemue May 25 '21 at 11:33
1

The call io::stdin().read_line(&mut input) returns an io::Result<usize>. Since the return value does not contain any lifetime linked to the reference you pass in, it does not create a borrow that lasts longer than that call expression, so it shouldn't conflict with the immutable borrow that occurs later. In fact the code compiles if you remove the loop.

The reason for this is that word.clone() doesn't actually do anything – it creates a copy of an &str, which is still an &str. Since this is stored in words, the input string is borrowed across loop iterations. Your code compiles if you replace word.clone() with word.to_owned().

It's easy to clean up the code to avoid this problem. Here's an example implementation that also fixes the bug that input accumulates the data from all lines, which I don't think you intend:

while io::stdin().read_line(&mut input).is_ok() {
    if let Some(word) = input.split_whitespace().next() {
        words.push(word.to_owned());
    }
    input.clear();
}

Another alternative:

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

fn main() {
    let mut words = vec![];
    for result in io::stdin().lock().lines() {
        match result {
            Ok(line) => words.extend(line.split_whitespace().next().map(ToOwned::to_owned)),
            Err(_) => break,
        }
    }
    println!("{:?}", words);
}
Sven Marnach
  • 574,206
  • 118
  • 941
  • 841