An i32 walks up to a String and asks for its number. The String replies:
"Sorry, you're not my type."
Many other languages have a mushy concept of a "string" that can be anything from a null-terminated byte array to a UTF-16 buffer to something else entirely (I'm looking at you, Perl). Rust splits "string" across distinct types:
char is one Unicode scalar value (always 4 bytes).&str is a borrowed view into UTF-8 text. Cheap to pass around.String is an owned, growable UTF-8 buffer. You own the memory.I'll use two words a lot in this course:
std::unique_ptr: dropping the owner frees
its allocation. Python and Java don't assign that responsibility to one
variable.const T& in C++, or handing out a read-only
pointer in C. Borrows are written with an & (or &mut if you also want to
mutate). The borrow has to end before the owner is dropped, and the compiler
enforces that for you, ruling out use-after-free and dangling pointers.This ownership model is why Rust has two string types in the first place: it tracks who owns each piece of data. The brief version is that every value has one owner, the value is dropped (deleted) when that owner goes out of scope, and you can borrow a value without owning it. The useful mental model is "one owner, many borrows."
This split lets you pass text around without copying it or losing track of who
owns it. Taking &str lets a function read text without copying it or
allocating a new buffer. A function that produces new text returns String
and returns ownership to the caller, who can then decide what to do with it.
You'll see this pattern again and again:
fn underscore_spaces(text: &str) -> String {
text.replace(' ', "_")
}
let s = String::from("meeting notes");
// &String coerces to &str. The original s is unchanged.
let filename = underscore_spaces(&s);
&str ("string slice", pronounced stir) is a borrowed view into text that
lives somewhere else. Taking name: &str means "I'll just need to read this
string; I'm not taking ownership of it."String is owned and heap-allocated. Returning -> String means the caller
gets a fresh, owned value back.There's one common gotcha: If you call .len() on a string, it returns the
number of bytes, not the number of characters in that string. Rust uses UTF-8
for strings, which means a single visible character can take more than one byte.
A visible character can also contain multiple char values, such as a letter
followed by a combining accent.
.chars() lets you walk through the char values in a string, one at a time.
The returned value is an iterator over those characters. Iterator provides
methods such as .next(), .count(), and .any(...) for consuming or
inspecting its values. We'll get to iterators in more detail later.
String with format!A convenient way to assemble a new String is the format! macro. It works
like println!, except instead of printing, it returns the formatted text:
let item = "notebooks";
let quantity = 12;
let label: String = format!("{item}: {quantity} in stock");
In the format string, {item} and {quantity} are captured identifiers. Rust
pulls the variables from the surrounding scope. You can also pass arguments
explicitly: format!("{}: {} in stock", item, quantity). The exclamation mark
(!) means format! is a macro rather than a regular function call.
The simplest way to consume an iterator is a for loop:
for c in "hello".chars() {
println!("{c}");
}
You can read it as "for each c produced on the right, run the body once." In
this case, for each character in the string "hello", do something with it. The
loop variable is a fresh binding scoped to each iteration. Ranges, arrays, and
collections can also go on the right-hand side because each can produce an
iterator.
Useful Resources
Time to put &str and String together. Implement format_welcome_message so
it returns the string "Welcome, {name}!".
The signature already tells you what to do:
fn format_welcome_message(name: &str) -> String
You borrow the &str to read the name and return a new String that the caller
owns.
/// Build and return the welcome message for `name`.
fn format_welcome_message(name: &str) -> String {
format!("Welcome, {name}!")
}
// In these exercises, tests live right next to the code they exercise.
// Don't worry about the syntax yet.
#[test]
fn test_format_welcome_message() {
assert_eq!(format_welcome_message("Alice"), "Welcome, Alice!");
assert_eq!(format_welcome_message("Bob"), "Welcome, Bob!");
}
In many languages, asking for the "length" of a string gives you back the number
of characters. In Rust, str::len returns the number of bytes in the
underlying UTF-8 buffer. For "hello" the byte count and char count both happen
to be 5, but "café" is 5 bytes and 4 chars. That's because the é is two bytes
in UTF-8.
Use chars() to count Unicode scalar values rather than bytes.
Useful from the Standard Library
str::charsiterates over thechars of a string.Iterator::countconsumes an iterator and returns how many items it produced.
/// Counts how many characters are in `text`.
///
/// Watch out: `text.len()` returns the number of bytes, not characters. For
/// "hello" those happen to be the same, but for "café" they aren't. See:
/// <https://doc.rust-lang.org/std/primitive.str.html#method.chars>
fn count_chars(text: &str) -> usize {
text.chars().count()
}
#[test]
fn test_count_chars() {
assert_eq!(count_chars("hello"), 5);
assert_eq!(count_chars("rust"), 4);
assert_eq!(count_chars("café🦀"), 5);
// A combining accent is a separate Unicode scalar value.
assert_eq!(count_chars("e\u{301}"), 2);
assert_eq!(count_chars(""), 0);
}
Here you borrow text to read it, then return an uppercase String that the
caller owns. You'll often see &str in and String out when a function reads
existing text to produce new text.
Useful from the Standard Library
str::to_uppercasereturns a newStringwith the text converted to uppercase.
/// Takes a borrowed `&str` and returns an owned, uppercased `String`.
///
/// Notice the signature: borrow on the way in, own on the way out. That's the
/// same pattern as the text replacement example in the chapter intro. See:
/// <https://doc.rust-lang.org/std/primitive.str.html#method.to_uppercase>
fn shout(text: &str) -> String {
text.to_uppercase()
}
#[test]
fn test_shout() {
assert_eq!(shout("hello"), "HELLO");
assert_eq!(shout("Rust"), "RUST");
assert_eq!(shout(""), "");
}
Strings aren't directly indexable in Rust, because UTF-8 characters have varying
widths, but you can walk through their chars. A for c in text.chars() loop
works. The exercise asks whether the text contains an uppercase character,
including non-ASCII ones such as É or Ω. You can use any(), which stops
as soon as it finds a match.
It takes a closure: an anonymous function such as |n| n > 10. The name
between the pipes is the parameter; the expression after them is the result.
Here, any passes each item to the closure and checks the returned bool. A
for loop is fine too.
Useful from the Standard Library
Iterator::anyreturnstrueif any item in the iterator matches a predicate. It stops at the first match, so you don't have to inspect the rest of the string.char::is_uppercasechecks whether a character is uppercase according to Unicode, including letters such asÉandΩ.
/// Returns true if `text` contains at least one Unicode uppercase character.
///
/// `for c in text.chars()` lets you inspect each character; the iterator
/// methods (`any`, `find`, ...) usually express this kind of "is there at least
/// one ..." check more directly. See:
/// <https://doc.rust-lang.org/std/primitive.char.html#method.is_uppercase>
fn has_uppercase(text: &str) -> bool {
text.chars().any(char::is_uppercase)
}
#[test]
fn test_has_uppercase() {
assert!(has_uppercase("Hello"));
assert!(has_uppercase("rustY"));
assert!(!has_uppercase("hello"));
assert!(has_uppercase("École"));
assert!(!has_uppercase("école"));
assert!(has_uppercase("éZ"));
assert!(!has_uppercase(""));
}
#[test]
fn test_has_uppercase_outside_ascii() {
assert!(has_uppercase("éΩ"));
assert!(!has_uppercase("éω"));
}
You counted UTF-8 characters, took a &str and produced a new String, and
walked a string character by character.
What We Learned
&stris a borrowed view into UTF-8 text;Stringis an owned, growable buffer;charis one Unicode scalar value. Functions that read text can take a&str, while functions that produce new text return aString.str::lenreturns the byte length. Uses.chars().count()when you want the number ofcharvalues.str::chars()returns an iterator. You can use it in a loop or with iterator methods:for c in s.chars(),s.chars().any(...),s.chars().count(), and so on.- Case conversion (
to_uppercase,to_lowercase) returns a newString. It doesn't change the original string.char::is_uppercaserecognizes Unicode uppercase characters, including letters outside ASCII.char::is_ascii_uppercaseonly checks forAthroughZ, so it would miss uppercase letters such asÉandΩ.
Extra practice to explore at your own pace. These chapters do not count toward course progress.