Why Rust strings need care
Building a message out of pieces is one of the most common jobs in a program. In Rust it works a little differently than in most languages, because there are two string types. A string literal such as "awesome" is a &str, a borrowed, read-only view of text that can't grow. A String owns its text in a buffer that can. Concatenation always means growing a String, so the left-hand side has to be one.
Concatenating with +
The + operator takes a String on the left and a &str on the right, and returns a String:
let adjective = "awesome";
let mut message = String::from("AlgoCademy is ");
message = message + adjective;
println!("{}", message);
AlgoCademy is awesome
The expression takes message by value. It appends adjective to the same buffer, and the result is assigned back to message. The variable is declared mut because it is assigned a second time.
Concatenating with +=
Since "append to a variable" is so common, += does the append and the assignment in one step:
let mut message = String::from("Hey, ");
message += "Andy";
message += "! Nice ";
message += "dog";
message += "!";
println!("{}", message);
Hey, Andy! Nice dog!
You can append as many pieces as you like. The right-hand side must be a &str: a literal, or a &String borrowed from another string.
Concatenating with push_str
The method push_str appends a string slice to a String in place. It does the same job as += and is the form the lesson's exercise uses:
let mut text = String::from("ab");
text.push_str("cd");
text.push('e');
println!("{}", text);
abcde
A single character is appended with push, whose argument is a char in single quotes. push_str takes a whole string in double quotes.
Because these forms write into the existing buffer, they are the natural choice inside a loop that builds one long string. The buffer grows when it needs to, and the old text is never copied into a brand new string for every piece.
Mistakes the compiler catches
- Two literals.
let c = "Hello, " + "world";fails withcannot add `&str` to `&str`, and the compiler notes that concatenation requires an ownedStringon the left. Start fromString::from(...)or.to_string(). - A
Stringon the right.a + bwith twoStringvalues reportsexpected `&str`, found `String`. Borrow the second one:a + &b. - Using the left side afterwards.
let b = a + "x";movesainto the operation. Printingaafter that fails withborrow of moved value: `a`. If you need both, writea.clone() + "x", or build the result withformat!("{}x", a). - Appending to a literal. With
let mut message = "Coding is ";the variable holds a&str, andmessage.push_str("fun")fails withno method named `push_str` found for reference `&str`. Making the variablemutdoesn't help; the type has to beString, which is why the starter code calls.to_string().
Appending numbers
Only text can be appended. message += 42 is rejected with expected `&str`, found integer, because Rust never converts a number to text silently. Convert it first with to_string() and borrow the result:
let n = 42;
let mut s = String::from("n = ");
s += &n.to_string();
println!("{}", s);
n = 42
The explicit conversion is typical of Rust: the cost of building a new string is visible in the code, and the type of every piece is clear to the reader. When the pieces are mostly numbers, format!("n = {}", n) does the conversion for you.
Joining many pieces with format!
When a message mixes several values, format! is often clearer than a chain of +. It takes the same arguments as println! but returns a String instead of printing, and it doesn't consume its arguments:
let name = String::from("Andy");
let greeting = format!("Hi, {}!", name);
println!("{} {}", greeting, name);
Hi, Andy! Andy
The lesson's exercise
The starter code declares message as a mutable String made from a literal and adjective as a &str. The exercise has three steps.
- Append the adjective. Call
message.push_str(&adjective). Passing&adjectiveborrows the variable, and Rust converts the resulting&&strto the&strthatpush_strexpects. - Append the ending. Call
push_stragain with", isn't it?". The apostrophe inside a double-quoted literal needs no escape. - Print the result with
println!("{}", message).
let mut message = "Coding is ".to_string();
let adjective = "worthwhile";
message.push_str(&adjective);
message.push_str(", isn't it?");
println!("{}", message);
Coding is worthwhile, isn't it?
Key takeaways
- Concatenation grows a
String; a&strliteral can't grow. +needs aStringon the left and a&stron the right, and it moves the left operand.+=andpush_strappend in place and need amutvariable.format!builds a new string from several values without consuming them.
