Skip to content
Lesson 4 of 15

Step 1 of 5 · Reading · ~3 min

Learn

Strings

Strings: String and &str

Rust has two string types, and knowing which one you are holding is the first step toward understanding its memory rules. By the end of this lesson you can read a line into a String, borrow a trimmed &str view of it, transform it, and measure it correctly in both bytes and characters.

Owned String, borrowed &str

  • String is owned text: a growable buffer on the heap that belongs to one variable and is freed when that variable goes out of scope.
  • &str (read "string slice") is a borrowed view: a pointer plus a length into text that lives somewhere else. You can read through it; it owns nothing.
let lit: &str = "hello";              // literals are &str, stored in the binary
let owned: String = String::from("hi"); // owned buffer
let view: &str = &owned;              // borrow a view of the String
let copy: String = lit.to_string();   // copy borrowed text into a new owner

Picture String as a house and &str as an address card. Many cards can point at one house; only the owner of the house is responsible for it.

Growing a String needs mut

Literals and &str views are read-only. A String you own can grow if you declared it mut:

let mut tag = String::from("v1");
tag.push_str(".2");   // append a &str  -> "v1.2"
tag.push('b');        // append a char  -> "v1.2b"

trim returns a view, to_lowercase returns a new String

The line that lines() gives you is a String. Calling .trim() on it does not copy anything: it returns a &str that points into the same buffer, skipping leading and trailing whitespace (spaces, tabs, \r, \n). Case-changing methods cannot work in place, so they allocate and return a brand new String:

let raw = String::from("  Zurich \t");
let city: &str = raw.trim();          // view: "Zurich"
let low: String = city.to_lowercase(); // new house: "zurich"
println!("[{}] [{}]", raw, low);

Output:

[  Zurich 	] [zurich]

raw is untouched. Every transform hands you its result as a return value; if you print the original variable, you print the original text.

len counts bytes, chars().count() counts characters

Rust strings are UTF-8. An ASCII letter takes 1 byte, ñ or ü takes 2, a Japanese character takes 3. .len() returns bytes. To count characters, walk them with .chars() and count: .chars().count(). This is also why s[0] does not compile: byte 0 might be half of a character.

text.len().chars().count()
"cat"33
"piñata"76
"Zürich"76
"日本"62

Case changes can also change length. German ß has no single uppercase letter, so "groß".to_uppercase() is "GROSS": 4 characters became 5. Measure the text you mean to measure, not a transformed copy.

let word = "groß";
let n_before = word.chars().count();              // 4
let n_after = word.to_uppercase().chars().count(); // 5
println!("{} {}", n_before, n_after);

Output: 4 5.

Common mistakes

  • Forgetting trim(). Input " hi " prints as HI and its length comes out 6 instead of 2. Input from Windows ends in \r, which lines() keeps, so even a clean-looking word counts one too many.
  • Using .len() as the character count. For "piñata" it prints 7, not 6: bytes, not characters. Only pure-ASCII text hides this bug.
  • Counting the uppercased copy. "groß" uppercased has 5 characters; the original has 4. Count the trimmed original.
  • Printing the original variable after a transform. to_uppercase() returns a new String; the one you called it on is unchanged.

Your exercise

Read one line, and print its trimmed text in uppercase, then its character count, then its byte count, all measured on the trimmed original. The starter reads the line into line and currently echoes it unchanged. The likeliest slip is using .len() where you need characters; the hidden tests use accented letters and ß.

Up nextFormattingStrings

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…