5.3Introduction to String
Lesson 1.6 made you a promise: "why text comes in two flavors (this one, and the string literals you've been printing) is a chapter 5 story." This lesson and the next are that story. And it starts with an introduction to someone you already know: every time you've read input, you've written String::new(). You've been manufacturing Strings for four chapters. Today you learn what you've been making.
An owned, growable text type
A String is Rust's owned, growable text type. Both adjectives are load-bearing. Growable: a String's contents can change length at runtime, which is why read_line can pour an arbitrarily long line into one, and why String can't be a fixed-size fundamental type like the chapter 4 cast (lesson 4.1 noted text is built out of fundamental pieces; a String manages a resizable collection of bytes for you). Owned: the String is responsible for the text it holds; the text lives as long as the String does and goes away when it goes away. The full weight of "owned" is chapter 8's headline act; for now, read it as "this one's mine," in contrast with something arriving next lesson.
There are three common ways to make one:
fn main() {
let empty = String::new();
let from_fn = String::from("carrot");
let converted = "potato".to_string();
println!("[{empty}]");
println!("{from_fn} and {converted}");
}[]
carrot and potato
String::new() makes an empty one, ready to receive input or grow. String::from("...") and "...".to_string() both make a String from literal text; they do the same job, and which one a programmer writes is mostly habit. (Printing an empty String prints nothing, hence the brackets to make the nothing visible.)
Growing a String
Mutation needs mut, same as every other type. The growing tools are push_str, which appends text, and push, which appends one character:
fn main() {
let mut order = String::from("salad");
order.push_str(", soup");
order.push('!');
println!("{order}");
}salad, soup!
Mind the quote marks on push('!'): single quotes, because push takes a char, lesson 4.9's one-character type. Write push("!") and you'll get the mismatched-types error you'd predict, with the same charming quote-mark correction 4.9 showed.
Rust also lets you concatenate with +, and you'll see it in other people's code, but it comes with a surprise: + consumes the String on its left side, which stops compiling in ways we can't explain until chapter 8 puts ownership on the table. This course owes you that explanation and will pay it there; until then, push_str and format! (lesson 5.5, two lessons away) cover every concatenation you need, with no surprises.
What len() counts
Strings know their length, but the unit deserves a warning label:
fn main() {
let plain = String::from("cafe");
let fancy = String::from("café");
println!("{}", plain.len());
println!("{}", fancy.len());
}4
5
len() counts bytes, not characters. Lesson 4.9 explained that Unicode gives every character a catalog number and that the popular ones fit small; in a String, text is stored in the UTF-8 encoding, where plain ASCII characters take one byte each and rarer characters take more. é takes two, so "café" is four characters in five bytes. For the ASCII-only text of most examples, bytes and characters agree, which is exactly what makes this bug a delayed-action one: code that "worked for months" meets its first accented name. Counting characters properly is a job for iterators (chapter 19); until then, treat len() as a size in memory, not a letter count. Its honest sibling is_empty() asks the question you usually meant anyway.
(len() returns a usize, of course: it's a memory measurement, and lesson 4.3 told you they'd keep arriving with collections of things. String is the first collection this course has admitted to.)
Text is not numbers
One more boundary, cheap to state now that chapter 4 trained the instinct: "45" is text that depicts a number, not a number. It won't add, compare against 45, or convert implicitly; lesson 4.10's no-implicit-conversions policy applies with full force at this border too. Crossing it deliberately is parse, whose full teardown is lesson 5.6.
Quiz time
Question #1
What does this print?
fn main() {
let mut s = String::new();
s.push_str("naïve");
println!("{} {}", s.len(), s.is_empty());
}Show solution
6 false
Five characters, but ï costs two bytes in UTF-8, so len() reports 6. is_empty() is false: the string has contents, however you count them.
Question #2
Predict the compiler's reaction:
fn main() {
let mut greeting = String::from("hello");
greeting.push(", world");
println!("{greeting}");
}Show solution
error[E0308]: mismatched types: push takes a single char, and ", world" is a string literal (expected char, found &str). The fix is either push_str(", world") or, if only one character was wanted, single quotes. The double-quote/single-quote type boundary from lesson 4.9 cuts both ways.
Question #3
Write a program that asks for the user's first name, then their last name, and builds (not just prints) a single String containing Firstname Lastname before printing it on one line, like Ada Lovelace. Use push_str and push; keep the input recipe and trim from lessons 1.6 and 5.2.
Show solution
use std::io;
fn main() {
println!("First name?");
let mut first = String::new();
io::stdin()
.read_line(&mut first)
.expect("failed to read input");
println!("Last name?");
let mut last = String::new();
io::stdin()
.read_line(&mut last)
.expect("failed to read input");
let mut full = String::from(first.trim());
full.push(' ');
full.push_str(last.trim());
println!("{full}");
}
String::from(first.trim()) starts the new String from the cleaned first name, push adds the single space (a char, single quotes), push_str appends the cleaned last name. Building the value in a variable, rather than printing pieces, is the new skill: full can now be handed to a function, measured, or grown further.
The promised flavor count was two, and this was only the first. The second flavor is hiding in plain sight, in the quotes of every literal you've written since your first program, and inside trim's mystery return type. Next lesson it takes off the mask.