Strings: String vs &str
Prerequisites: Slices
Rust has two main string types, and knowing which to use is one of the first practical hurdles:
String— an owned, growable, heap-allocated string. Use it when you need to build or modify text, or store it in a struct.&str— a borrowed string slice: a view into text owned by someone else (aString, or a literal in the program binary). Use it for function parameters that only read text.
Both are always valid UTF-8.
Syntax
let mut s = String::new(); // empty String
let s1 = String::from("hello"); // from a literal
let s2 = "hello".to_string(); // same thing
let view: &str = &s1; // borrow a String as &str
s.push_str("abc"); // append a &str
s.push('d'); // append one char
let joined = format!("{s1}-{s2}"); // combine without moving
+ also joins strings, but it takes ownership of the left side:
let c = a + &b; moves a into c. format! doesn't move anything
and is usually easier to read.
Why s[0] doesn't compile
UTF-8 uses 1 to 4 bytes per character: "hello" is 5 bytes, but
"héllo" is 6, because é takes 2. Indexing by a number would be
ambiguous — byte or character? — and couldn't be done in constant
time for characters, so Rust doesn't allow it. Instead:
.chars()— iterate over characters..bytes()— iterate over raw bytes..len()— the length in bytes..chars().count()— the number of characters.&s[0..1]— slicing by byte range still works, but panics if a boundary falls inside a multi-byte character.
Example
fn main() {
let mut s = String::from("Hello");
s.push_str(", world");
s.push('!');
println!("{s}");
let first = String::from("Tic");
let second = String::from("Tac");
let joined = first + "-" + &second; // `first` is moved here
println!("{joined}");
let game = format!("{joined}-{}", "Toe"); // nothing is moved
println!("{game}");
let word = "héllo";
println!("bytes: {}, chars: {}", word.len(), word.chars().count());
for c in word.chars() {
print!("[{c}]");
}
println!();
println!("{}", s.to_uppercase());
println!("{}", s.contains("world"));
println!("{}", s.replace("world", "Rust"));
let padded = " trim me ";
println!("[{}]", padded.trim());
for part in "one two three".split_whitespace() {
println!("- {part}");
}
}
Expected output
Hello, world!
Tic-Tac
Tic-Tac-Toe
bytes: 6, chars: 5
[h][é][l][l][o]
HELLO, WORLD!
true
Hello, Rust!
[trim me]
- one
- two
- three
Common mistakes
- Indexing a string with a number:
let s = String::from("hello");
let c = s[0];
error[E0277]: the type `str` cannot be indexed by `{integer}`
The compiler's note suggests .chars().nth() or .bytes().nth().
- Using a
Stringafter+moved it. Inlet c = a + &b;,acan't be used afterwards. Useformat!("{a}{b}")if you need to keep both. - Assuming
.len()counts characters. It counts bytes; the two only match for plain ASCII text. - Taking
&Stringparameters when&strworks.fn greet(name: &str)accepts both"Ada"and&my_string.
Practice
Write a function count_vowels(text: &str) -> usize that counts the
vowels a, e, i, o, u (in either case) using .chars(). For
"Programming in Rust" it should print Vowels: 5.
Show solution
fn count_vowels(text: &str) -> usize {
let mut count = 0;
for c in text.chars() {
if "aeiou".contains(c.to_ascii_lowercase()) {
count += 1;
}
}
count
}
fn main() {
println!("Vowels: {}", count_vowels("Programming in Rust"));
}