Rust - String
Rust - Strings
Section titled “Rust - Strings”Text is a complex data type, and Rust handles it with a two-part approach that provides safety, speed, and control over memory. The two main string types are:
&str(string slice): An immutable, fixed-size view into string data. It’s a reference to a sequence of bytes that are always valid UTF-8.String: A mutable, growable, owned string. It’s a heap-allocated buffer that can be modified.
Understanding the difference between these two is fundamental to writing idiomatic Rust code.
String Literals (&str)
Section titled “String Literals (&str)”String literals, like "Hello, world!", are of type &str. The text of a string literal is hardcoded directly into your program’s binary file. They are immutable and have a 'static lifetime, meaning they are guaranteed to be valid for the entire duration of the program.
fn main() { // The type `&str` is a slice pointing to that string data. let greeting: &str = "Hello, Rust developers!"; println!("{}", greeting);
// We can also let the compiler infer the type. let location = "Planet Earth"; println!("We are live from {}.", location);}String: The Owned String Type
Section titled “String: The Owned String Type”The String type is part of Rust’s standard library. It’s designed for situations where you need to own string data, modify it, or when you don’t know the string’s content at compile time (e.g., user input).
You can create a String in a few common ways:
fn main() { // 1. Create a new, empty String let mut new_string = String::new(); new_string.push_str("This is a new"); new_string.push(' '); // Push a single character println!("new_string: {}", new_string);
// 2. Create from a string literal using String::from() let from_literal = String::from("string from a literal."); println!("from_literal: {}", from_literal);
// 3. Convert a &str to a String using .to_string() let str_slice = "Another way to do it."; let to_string_version = str_slice.to_string(); println!("to_string_version: {}", to_string_version);}Common String Methods
Section titled “Common String Methods”| Method | Signature | Description |
|---|---|---|
new() | pub const fn new() -> String | Creates a new empty String. |
push_str() | pub fn push_str(&mut self, string: &str) | Appends a string slice (&str) to the end of this String. |
push() | pub fn push(&mut self, ch: char) | Appends a single char to the end of this String. |
replace() | pub fn replace<'a, P>(&'a self, from: P, to: &str) -> String | Replaces all matches of a pattern with another string, returning a new String. |
len() | pub fn len(&self) -> usize | Returns the length of the String in bytes (not characters). |
trim() | pub fn trim(&self) -> &str | Returns a string slice with leading and trailing whitespace removed. |
split_whitespace() | pub fn split_whitespace(&self) -> SplitWhitespace | Returns an iterator over substrings, separated by whitespace. |
chars() | pub fn chars(&self) -> Chars | Returns an iterator over the chars of a string. |
Manipulating Strings
Section titled “Manipulating Strings”Concatenation
Section titled “Concatenation”You can combine strings using the + operator or the format! macro. The format! macro is often preferred because it’s more readable and doesn’t take ownership of its arguments.
fn main() { let s1 = String::from("Hello, "); let s2 = String::from("world!");
// Using `+` takes ownership of s1, so s1 cannot be used after this. // The `+` operator uses the `add` method, with signature: fn add(self, s: &str) -> String // That's why s2 must be a reference (`&s2`). let s3 = s1 + &s2; // println!("{}", s1); // This would cause a compile error: value borrowed here after move
println!("Concatenated with +: {}", s3);
// Using `format!` macro is often cleaner and does not take ownership. let s4 = String::from("tic"); let s5 = String::from("tac"); let s6 = String::from("toe"); let formatted = format!("{}-{}-{}", s4, s5, s6);
println!("Formatted: {}", formatted); println!("s4 is still available: {}", s4); // s4 is still valid!}A Note on Indexing
Section titled “A Note on Indexing”Unlike some languages, Rust does not allow you to access characters in a String by integer index (e.g., my_string[0]). This is because Strings are UTF-8 encoded, and characters can be of variable byte length. For example, 'a' is 1 byte, but 'é' is 2 bytes, and '🦀' is 4 bytes. An index could point to a location that isn’t the start of a valid character, which would be unsafe.
Instead, you should iterate or create slices.
fn main() { let hello = "Здравствуйте"; // A Russian string
// Don't do this: it will panic if the index is not on a character boundary. // let slice = &hello[0..1]; // This would be invalid!
// Slicing must be on character boundaries. // The first character 'З' is 2 bytes long. let slice = &hello[0..2]; println!("Slice of first char: {}", slice);
// The best way to process characters is to iterate. println!("Iterating over characters:"); for c in hello.chars() { print!("{} ", c); } println!();}Best Practices
Section titled “Best Practices”When writing functions, it’s often more flexible to accept &str as a parameter instead of String. This allows your function to work with both String types and &str slices without requiring an unnecessary allocation. This is possible because Rust can automatically convert a &String into a &str (a concept called deref coercion).
// This function is flexible because it accepts a string slice.fn log_message(message: &str) { println!("LOG: {}", message);}
fn main() { let owned_string = String::from("An owned message."); let string_literal = "A literal message.";
// We can pass both types to the function! log_message(&owned_string); log_message(string_literal);}