Strings
In one sentence
Section titled “In one sentence”String owns a growable UTF-8 buffer, &str borrows a view of UTF-8 text, and neither lets you index characters by position.
The Java you know
Section titled “The Java you know”Java has an immutable String and a mutable StringBuilder. Both are UTF-16 inside. charAt(i) returns a UTF-16 code unit, and substring(a, b) works on code-unit positions. For most European text this is the same as “characters”, so most Java code never notices the difference.
Java
static String greeting(String name) { var text = new StringBuilder("Dear "); // mutable builder text.append(name); text.append(','); return text.toString(); // immutable String}Rust
pub fn greeting(name: &str) -> String { let mut text = String::from("Dear "); // owned, growable text.push_str(name); // `name` is a borrowed `&str` text.push(','); text}
pub fn demo() -> (String, String) { let literal: &str = "Ada"; // a literal is a `&'static str` let owned: String = literal.to_string(); // a new, owned copy let part: &str = &owned[0..2]; // a borrowed slice of `owned`: "Ad" (greeting(literal), greeting(part)) // `&String` and `&str` both work}The Rust you will see
Section titled “The Rust you will see”Rust has two main string types:
| Type | What it is | Java analogy |
|---|---|---|
String |
Owned, growable, heap-allocated UTF-8 text. | StringBuilder and String in one type. |
&str |
A borrowed view of UTF-8 text: a pointer and a length. | A read-only CharSequence, or a substring that does not copy. |
A String can be changed in place (push_str, push) if the variable is mut. A &str cannot be changed. Every String gives a &str for free: &owned or owned.as_str(). String literals such as "Ada" are &'static str: views of text that is part of the program.
pub fn greeting(name: &str) -> String { let mut text = String::from("Dear "); // owned, growable text.push_str(name); // `name` is a borrowed `&str` text.push(','); text}
pub fn demo() -> (String, String) { let literal: &str = "Ada"; // a literal is a `&'static str` let owned: String = literal.to_string(); // a new, owned copy let part: &str = &owned[0..2]; // a borrowed slice of `owned`: "Ad" (greeting(literal), greeting(part)) // `&String` and `&str` both work}- Line 1: The function reads
nameand returns new text. So it borrows a&strand returns an ownedString. This is the normal shape. - Line 2:
String::frommakes an owned copy.let mutallows changes. - Line 9: A literal is a
&str. No allocation. - Line 11:
&owned[0..2]borrows a slice of theString. The range is in **bytes**, not characters. Here it is safe, because "Ad" is ASCII. - Line 12:
greetingtakes&str, so both a literal and a slice work. A&Stringwould work too: it converts to&strautomatically.
UTF-8, and no indexing by position
Section titled “UTF-8, and no indexing by position”Rust strings are always valid UTF-8. A character can take one to four bytes. So s[0] does not compile: there is no fast, correct answer to “the character at position 0”:
let sku = String::from("Ä-100");let first = sku[0]; // error E0277: the type `str` cannot be indexed by `{integer}`Instead, you say what you want: bytes or characters.
pub fn describe(sku: &str) -> (usize, usize, Option<char>) { let bytes = sku.len(); // length in bytes (UTF-8), not characters let chars = sku.chars().count(); // number of Unicode scalar values let first = sku.chars().next(); // the first character, if any (bytes, chars, first)}.len()is the length in bytes. For “Ä-100” it is 6, not 5..chars()iterates over Unicode scalar values (char, four bytes each)..chars().count()is the number of characters, but it reads the whole string..bytes()iterates over the raw bytes.
Slicing with a range, &s[a..b], is allowed, but the range is in bytes. If a or b is past the end, or inside a multi-byte character, the program panics. On text from users or files, that is a crash waiting for the first name with an accent. See S24: Indexing (v[i], s[a..b]) on untrusted input.
How to read it
Section titled “How to read it”Five ways to make a String
Section titled “Five ways to make a String”You will see all of these. They all produce a new, owned String:
pub fn all_the_same(name: &str, id: u64) -> [String; 5] { [ name.to_string(), // from `Display`: works for any displayable value name.to_owned(), // "make an owned copy of this borrowed value" String::from(name), // explicit conversion name.into(), // conversion chosen by the target type format!("{name}#{id}"), // build a new string from parts ]}All five are correct. Codebases differ in which they prefer. to_string() and to_owned() are the most common for &str. format! is for building text from parts. What matters in a review is not which one, but how many: each one allocates.
OsString, PathBuf and &Path
Section titled “OsString, PathBuf and &Path”File paths are not always valid UTF-8 (on Linux, a file name can be any bytes). So Rust has separate types:
PathBuf(owned) and&Path(borrowed) for paths. They are to each other asStringis to&str.OsStringand&OsStrfor text from the operating system: environment variables, command-line arguments, file names.
pub fn report_path(dir: &Path, customer: &str) -> PathBuf { dir.join("reports").join(format!("{customer}.csv")) // `PathBuf` is to `&Path` as `String` is to `&str`}When you see a function that takes &str for a path and calls format!("{dir}/{name}"), suggest &Path and join. It handles separators, and it does not fail on non-UTF-8 names.
What good looks like
Section titled “What good looks like”- Parameters are
&str. A function that reads text takes&str. A constructor that keeps the text takesString(orimpl Into<String>). See S05: Owned parameters (String,Vec<T>) where a borrow is sufficient. - Return values are
Stringwhen the function builds new text, and&strwhen it returns part of its input. - No position arithmetic on text. Use
split,split_once,strip_prefix,trim,find,char_indices, andget(..). - Joining and building:
join(",")for lists,push_strorwrite!for building in a loop, oneformat!for a fixed template. - Paths are
PathandPathBuf, notString.
Red flags in AI output
Section titled “Red flags in AI output”fn f(s: String)whenfonly reads. Every caller must allocate or give away its string. See S05: Owned parameters (String,Vec<T>) where a borrow is sufficient.&Stringparameters. Use&str. See S06:&String,&Vec<T>,&Box<T>parameters..to_string()repeated in a chain, for examplename.to_string().trim().to_string(). Each one allocates. Usually one at the end is enough.+in a loop, andformat!("{}", x)used only to convert one value. See S32: String building with+in a loop,format!used only to convert.- Slicing by byte position on input:
&line[..3],&name[0..1]. See S24: Indexing (v[i],s[a..b]) on untrusted input. .chars().nth(i)in a loop: each call starts from the beginning of the string, so the loop is quadratic..len()used as “number of characters”, for example for a maximum length check on a user name.
- S05Severity: LowOwned parameters (
String,Vec<T>) where a borrow is sufficientThe function takes ownership of data that it only reads. - S06Severity: Low
&String,&Vec<T>,&Box<T>parametersA reference to the container where a slice (&str,&[T]) or&Tis more flexible. - S24Severity: HighIndexing (
v[i],s[a..b]) on untrusted inputOut-of-range input makes the program panic. - S32Severity: LowString building with
+in a loop,format!used only to convertExtra allocations and noise when building or converting strings.
Better vs worse
Section titled “Better vs worse”The task: show a preview of the first 10 characters of a product description.
WorseA byte slice: panics on short text, and on text where byte 10 is inside a character.
/// Panics if `text` is shorter than 10 bytes, or if byte 10 is in the/// middle of a multi-byte character.pub fn preview(text: &str) -> &str {&text[..10]}Run in Playground (opens the Rust Playground in a new tab) BetterFinds the byte position of the 11th character. Never panics, and counts characters, not bytes.
/// The first 10 characters, or the whole text if it is shorter.pub fn preview(text: &str) -> &str {match text.char_indices().nth(10) {Some((end, _)) => &text[..end], // `end` is on a character boundaryNone => text,}}Run in Playground (opens the Rust Playground in a new tab)
The “worse” version passes every test with ASCII text of 10 or more characters. It panics on “short”, and on “Grüße für Köln”, where byte 10 is in the middle of a “ü”. The tests in examples/concepts/src/strings.rs show both panics.
And building a CSV line:
Worse
+in a loop, andformat!only to convert a&strthat is already text.pub fn csv_line(fields: &[&str]) -> String {let mut line = String::new();for (i, field) in fields.iter().enumerate() {if i > 0 {line = line + ",";}line = line + &format!("{}", field);}line}Run in Playground (opens the Rust Playground in a new tab) Better
joindoes the same in one call, with one allocation.pub fn csv_line(fields: &[&str]) -> String {fields.join(",")}Run in Playground (opens the Rust Playground in a new tab)
The “worse” version is correct. It is a direct translation of a Java += loop. Each format! allocates a new String, and each + can reallocate the line. The cost is small for a few fields, but the code is longer than it needs to be.
Check yourself
Section titled “Check yourself”Why does
name[0]not compile on aString?Show the answer
UTF-8 characters have different lengths, so “the character at index 0” is not a simple lookup. Rust makes you choose:
name.chars().next()for the first character, orname.as_bytes()[0]for the first byte.What does
len()return for the text Köln?Show the answer
len()is the length in bytes, and “ö” is two bytes in UTF-8. The number of characters ischars().count(), which is 4.
A function has the parameter
customer: Stringand only uses it informat!. What do you suggest?Show the answer
Change it to
customer: &str. Callers with aStringpass&name, and callers with a literal pass it directly. No allocation for the call.When do you see
PathBufinstead ofString?Show the answer
For file paths. Paths can contain bytes that are not valid UTF-8, and
Path::joinhandles separators for each operating system.
Go deeper
Section titled “Go deeper”- The Rust Programming Language, chapter 8.2: Storing UTF-8 encoded text with strings
- Standard library:
str, with all the methods that you see in code review - Standard library:
String - Standard library:
Path