7

I am trying to get a &str and &str to concatenate in a for loop withe intention of using the new combined string after a number of parts have been added to it. A general layout of the for loop can be seen below but I am having a lot of trouble combining strings due to numerous errors.

for line in reader.lines() {
    let split_line = line.unwrap().split(",");
    let mut edited_line = "";

    for word in split_line {
        if !word.contains("substring") {
            let test_string = [edited_line, word].join(",");
            edited_line = &test_string;
        }
    }
    let _ = writeln!(outfile, "{}", edited_line).expect("Unable to write to file"); 
}

First error:

error[E0716]: temporary value dropped while borrowed

Comes when running the above.

Second error:

error[E0308]: mismatched types expected &str, found struct std::string::String

happens when you remove the & from test_string when it is assigned to edited_line

Note: format! and concat! macros both also give error 2.
It seems to be if I get error 2 and convert the std::string:String and convert it to &str I get the error stating the variables don't live long enough.

How am I supposed to go about building a string of many parts?

8176135
  • 3,755
  • 3
  • 19
  • 42
John7867
  • 93
  • 1
  • 1
  • 5
  • What do you mean by `edited_line = &test_string;`? Do you mean `edited_like += test_string`? – tadman Jul 07 '20 at 00:53

2 Answers2

12

Note that Rust has two string types, String and &str (actually, there are more, but that's irrelevant here).

  • String is an owned string and can grow and shrink dynamically.
  • &str is a borrowed string and is immutable.

Calling [edited_line, word].join(",") creates a new String, which is allocated on the heap. edited_line = &test_string then borrows the String and implicitly converts it to a &str.

The problem is that its memory is freed as soon as the owner (test_string) goes out of scope, but the borrow lives longer than test_string. This is fundamentally impossible in Rust, since it would otherwise be a use-after-free bug.

The correct and most efficient way to do this is to create an empty String outside of the loop and only append to it in the loop:

let mut edited_line = String::new();

for word in split_line {
    if !word.contains("substring") {
        edited_line.push(',');
        edited_line.push_str(word);
    }
}

Note that the resulting string will start with a comma, which might not be desired. To avoid it, you can write

let mut edited_line = String::new();

for word in split_line {
    if !word.contains("substring") {
        if !edited_line.is_empty() {
            edited_line.push(',');
        }
        edited_line.push_str(word);
    }
}

This could be done more elegantly with the itertools crate, which provides a join method for iterators:

use itertools::Itertools;

let edited_line: String = line
    .unwrap()
    .split(",")
    .filter(|word| !word.contains("substring"))
    .join(",");
Aloso
  • 5,123
  • 4
  • 24
  • 41
2

let mut edited_line = ""; makes edited_line a &str with a static lifetime.

To actually make edited_line a string, either append .to_owned(), or use String::new():

let mut edited_line = String::new();
// Or
let mut edited_line = "".to_owned();

See What are the differences between Rust's `String` and `str`? if you are unfamiliar with the differences.

Most importantly for your case, you can't extend a &str, but you can extend a String.


Once you switched edited_line to a String, using the method of setting edited_line to [edited_line, word].join(","); works:

for line in reader.lines() {
    let split_line = line.unwrap().split(",");
    let mut edited_line = String::new();

    for word in split_line {
        if !word.contains("substring") {
            let test_string = [edited_line.as_str(), word].join(","); // Added .as_str() to edited_line
            edited_line = test_string; // Removed the & here
        }
    }
    let _ = writeln!(outfile, "{}", edited_line).expect("Unable to write to file"); 
}

Playground

However, this is both not very efficient, nor ergonomic. Also it has the (probably unintended) result of prepending each line with a ,.


Here is an alternative that uses only one String instance:

for line in reader.lines() {
    let split_line = line.unwrap().split(",");
    let mut edited_line = String::new();

    for word in split_line {
        if !word.contains("substring") {
            edited_line.push(',');
            edited_line.push_str(word);
        }
    }
    let _ = writeln!(outfile, "{}", edited_line).expect("Unable to write to file"); 
}

This still prepends the , character before each line however. You can probably fix that by checking if edited_line is not empty before pushing the ,.

Playground


The third option is to change the for loop into an iterator:

for line in reader.lines() {
    let edited_line = line.split(",")
        .filter(|word| !word.contains("substring"))
        .collect::<Vec<&str>>() // Collecting allows us to use the join function.
        .join(",");
    let _ = writeln!(outfile, "{}", edited_line).expect("Unable to write to file"); 
}

Playground

This way we can use the join function as intended, neatly eliminating the initial , at the start of each line.


PS: If you have trouble knowing what types each variable is, I suggest using an IDE like Intellij-rust, which shows type hints for each variable as you write them.

8176135
  • 3,755
  • 3
  • 19
  • 42