Strings & Text
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. Rust has one main owned string
type, String, and one main borrowed type, &str; both always hold valid UTF-8. Several more types exist for text
that is not guaranteed to be UTF-8 — operating-system strings, file paths, C strings — and Rust keeps them apart
so that encoding problems surface at the boundaries instead of deep inside a program.
String and &str
String |
&str |
|
|---|---|---|
Ownership |
owns a heap buffer |
borrows bytes owned elsewhere (a |
Size |
3 words: pointer, capacity, length |
2 words: pointer, length |
Mutable |
yes, can grow ( |
no |
Created by |
|
string literals, |
Use as parameter |
only when the function needs ownership |
default: accepts |
fn shout(text: &str) -> String { // borrow in, own out
let mut loud = text.to_uppercase();
loud.push('!');
loud
}
fn main() {
let literal: &'static str = "dune"; // stored in the binary
let owned: String = String::from("emma");
println!("{} {}", shout(literal), shout(&owned)); // &String coerces to &str
let mut title = String::with_capacity(32);
title.push_str("Middle");
title += "march"; // String + &str
let full = title.clone() + " by " + "George Eliot";
println!("{title} / {full} / {}", title.len());
}
+ takes ownership of its left operand (fn add(self, s: &str) -> String), which is why title.clone() + ... is
needed above to keep title. For anything more than two pieces, format! is clearer.
Bytes, chars and grapheme clusters
A String is a Vec<u8> with a UTF-8 guarantee. Text can be viewed at three levels, and they differ for anything but
ASCII:
-
bytes (
u8): the UTF-8 encoding;len()counts bytes; -
chars (
char): Unicode scalar values, 1—4 bytes each in UTF-8; -
grapheme clusters: what a reader perceives as one character — possibly several
chars, such as a letter plus a combining accent, or an emoji with a skin-tone modifier. The standard library does not segment graphemes; use theunicode-segmentationcrate.
fn main() {
let title = "Señor 📚";
println!("bytes: {}", title.len()); // 11
println!("chars: {}", title.chars().count()); // 7
for (i, c) in title.char_indices() {
print!("{i}:{c} "); // 0:S 1:e 2:ñ 4:o 5:r 6: 7:📚
}
println!();
println!("{:?}", "é".bytes().collect::<Vec<u8>>()); // [195, 169] -- precomposed
println!("{:?}", "e\u{301}".chars().collect::<Vec<char>>()); // ['e', '\u{301}'] -- decomposed
}
Why strings cannot be indexed
s[0] does not compile (error E0277, "the type str cannot be
indexed by {integer}`"). A byte index would return a fragment of a character for non-ASCII text, a char index would
need an O(n) scan, and neither is what users usually mean. Ask for the level you want instead: `s.as_bytes()[0],
s.chars().nth(0), or s.chars().next().
Range indexing is allowed, in bytes, but panics if a bound falls inside a character:
fn main() {
let title = "Señor";
println!("{}", &title[0..2]); // "Se"
println!("{:?}", title.get(0..3)); // None -- byte 3 is inside 'ñ'
println!("{}", title.is_char_boundary(3)); // false
let cut = title.char_indices().nth(3).map_or(title.len(), |(i, _)| i);
println!("{}", &title[..cut]); // "Señ" -- first three chars, safely
// &title[0..3] would panic: byte index 3 is not a char boundary
}
Common string APIs
Most methods are defined on str, so they work on String and &str alike. Methods that need to change the string
in place (push_str, insert, truncate, retain, drain, make_ascii_lowercase) are on String.
| Task | Methods |
|---|---|
Searching |
|
Splitting |
|
Trimming |
|
Transforming |
|
Inspecting |
|
Building |
|
fn main() {
let line = " 978-0-441-01359-3 ; Dune ; Frank Herbert ";
let fields: Vec<&str> = line.split(';').map(str::trim).collect();
println!("{fields:?}"); // ["978-0-441-01359-3", "Dune", "Frank Herbert"]
let isbn = fields[0].replace('-', "");
println!("{isbn} {}", isbn.starts_with("978"));
println!("{:?}", fields[2].split_once(' ')); // Some(("Frank", "Herbert"))
println!("{:?}", fields[1].find('u')); // Some(1)
let tag = "[sci-fi]";
println!("{:?}", tag.strip_circumfix('[', ']')); // Some("sci-fi") -- 1.98
println!("{:?}", "v2.1".strip_prefix('v')); // Some("2.1")
let mut slug = String::from("The Left Hand of Darkness");
slug.make_ascii_lowercase();
slug.retain(|c| c.is_ascii_alphanumeric() || c == ' ');
let slug = slug.split_whitespace().collect::<Vec<_>>().join("-");
println!("{slug}"); // the-left-hand-of-darkness
}
Source: std: str primitive
Formatting: format! and the formatting mini-language
format!, print!, println!, eprintln!, write! and panic! share one syntax, checked at compile time. Values
can be passed as arguments or captured directly from the surrounding scope by name (since 1.58):
fn main() {
let title = "Dune";
let year = 1965;
let price = 9.5;
let rating = 4.25_f64;
println!("{title} ({year})"); // inline capture
println!("{0} -- {0}, {1}", title, year); // positional
println!("{t}", t = title.to_uppercase()); // named argument (expressions need this form)
println!("[{:>10}] [{:<6}] [{:^8}]", title, year, "*"); // right, left, centre in a width
println!("[{:*^12}]", title); // custom fill character
println!("{price:.2} {rating:8.1} {:+}", -3); // precision, width + precision, sign
println!("{:#x} {:#b} {:o} {:e}", 255, 5, 8, 1500.0); // 0xff 0b101 10 1.5e3
println!("{:05}", 42); // 00042 -- zero padding
let width = 6;
println!("[{year:>width$}]"); // width from a variable
println!("{:?} {:#?}", Some(title), (1, "a"));// Debug and pretty Debug
println!("{{literal braces}}");
}
Source: std::fmt
| Spec | Meaning |
|---|---|
|
|
|
hex, upper hex, octal, binary, scientific ( |
|
minimum width with default, left, centre, right alignment |
|
fill character before the alignment |
|
precision for floats (or max width for strings); |
|
always show the sign; zero-pad to width |
|
named, positional, width from a variable |
Your own types become formattable by implementing Display or deriving Debug; see
Standard Traits & Operator Overloading.
std::fmt::from_fn (1.93) builds a one-off displayable value from a closure without defining a type.
ToString, FromStr and parse
Every type that implements Display gets to_string() through a blanket impl of ToString. The reverse direction is
FromStr, which str::parse calls:
use std::fmt;
use std::str::FromStr;
#[derive(Debug, PartialEq)]
struct ShelfCode {
section: String,
number: u16,
}
impl FromStr for ShelfCode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (section, number) = s.split_once('-').ok_or("expected SECTION-NUMBER")?;
let number = number.parse().map_err(|e| format!("bad number: {e}"))?;
Ok(ShelfCode { section: section.to_uppercase(), number })
}
}
impl fmt::Display for ShelfCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}-{:03}", self.section, self.number)
}
}
fn main() {
let code: ShelfCode = "fic-7".parse().unwrap();
println!("{code} {}", code.to_string().len()); // FIC-007 7
println!("{:?}", "fic".parse::<ShelfCode>()); // Err("expected SECTION-NUMBER")
}
Cow<str>: borrow when you can, own when you must
std::borrow::Cow<'a, str> ("clone on write") holds either a borrowed &'a str or an owned String. Functions that
usually return their input unchanged but sometimes need to modify it can return a Cow and avoid allocating in the
common case:
use std::borrow::Cow;
fn normalise_title(title: &str) -> Cow<'_, str> {
if title.contains(" ") || title != title.trim() {
Cow::Owned(title.split_whitespace().collect::<Vec<_>>().join(" "))
} else {
Cow::Borrowed(title) // no allocation
}
}
fn main() {
for t in ["Dune", " Dune Messiah "] {
let n = normalise_title(t);
let kind = if matches!(n, Cow::Borrowed(_)) { "borrowed" } else { "owned" };
println!("{n:?} ({kind})");
}
let lossy = String::from_utf8_lossy(&[0x44, 0x75, 0xFF, 0x6E, 0x65]); // Cow<str>
println!("{lossy}"); // Du�ne
}
Source: std::borrow::Cow
Converting bytes to text
-
String::from_utf8(vec)andstr::from_utf8(&bytes)validate and return aResult; -
String::from_utf8_lossy(&bytes)replaces invalid sequences with U+FFFD and returnsCow<str>; -
String::from_utf8_lossy_owned(vec)(1.99) does the same but consumes aVec<u8>, reusing its allocation when the bytes are already valid; -
from_utf8_uncheckedskips validation and isunsafe.
fn main() {
let ok = String::from_utf8(vec![0x44, 0x75, 0x6E, 0x65]);
let bad = String::from_utf8(vec![0x44, 0xFF]);
println!("{ok:?} {}", bad.is_err());
let repaired = String::from_utf8_lossy_owned(vec![0x44, 0xFF, 0x65]); // 1.99
println!("{repaired}");
}
Strings that are not UTF-8
| Owned | Borrowed | Purpose |
|---|---|---|
|
|
strings from the operating system (environment variables, command-line arguments, file names): arbitrary bytes on Unix, potentially ill-formed UTF-16 on Windows |
|
|
file-system paths; wrappers around |
|
|
NUL-terminated strings for C APIs; no interior NUL bytes |
|
|
arbitrary bytes; byte-string literals |
use std::ffi::{CStr, CString, OsStr};
use std::path::Path;
fn main() {
let path = Path::new("/srv/shelf/covers/dune.jpg");
println!("{:?} {:?}", path.file_stem(), path.extension().and_then(OsStr::to_str));
println!("{}", path.display()); // lossy Display for paths
let cover = path.with_extension("webp");
println!("{}", cover.display());
let c_title: &CStr = c"Dune"; // C string literal (1.77): &'static CStr
let owned = CString::new("Emma").expect("no interior NUL");
println!("{:?} {:?} {}", c_title, owned, c_title.count_bytes()); // "Dune" "Emma" 4
println!("{}", CString::new("bad\0title").is_err()); // true
let raw = r#"{"title": "Dune", "path": "C:\shelf"}"#; // raw string: no escapes
let method: &[u8; 3] = b"GET";
println!("{raw} {method:?}");
}
Source: std::ffi
Convert at the boundaries: os_str.to_str() returns Option<&str> (fails on non-UTF-8), to_string_lossy() always
succeeds with replacement characters, and Path::display() is for printing only. The C types are covered in more depth
on FFI & C Interop, and paths on
I/O, Files, Processes & Environment.
Common errors
| Problem | Fix |
|---|---|
E0277: the type |
use |
E0308 expected |
|
E0277 "the size for values of type |
use |
panic "byte index N is not a char boundary" |
slice at indices from |
E0369 cannot add |
the left operand must be a |
References
-
The Rust Programming Language: Storing UTF-8 Encoded Text with Strings
-
std::ffi (OsString, OsStr, CString, CStr)