Strings are everywhere in programming, and Rust has its own way of handling them—with a focus on safety and performance. In this guide, you’ll get a clear overview of how strings work in Rust, including the difference between String
and &str
, plus three real-world examples of string manipulation.
By the end, you’ll be writing, modifying, and thinking about strings the Rust way.
What You’ll Learn
- The two main string types:
String
and&str
- How Rust handles UTF-8 encoding
- The role of ownership and borrowing with strings
- Three practical string manipulation examples
Let’s jump in.
Strings in Rust: The Basics
Rust has two main types for working with strings:
1. &str
: The String Slice
- What it is: A reference to a string—immutable and usually from a string literal.
- Where it lives: Stored on the stack as a pointer and length.
- Use it when: You just need to read or pass around string data without changing it.
Example:
let greeting: &str = "Hello, world!";
println!("Length of '{}': {}", greeting, greeting.len());
2. String
: The Owned String
- What it is: A growable, heap-allocated string that owns its data.
-
How to create it: Use
String::from()
or call.to_string()
on a&str
. - Use it when: You need to modify the contents or build a string dynamically.
Example:
let mut message = String::from("Hello");
message.push_str(", world!");
println!("Modified string: {}", message);
Quick Comparison
Feature | &str |
String |
---|---|---|
Ownership | Borrowed | Owned |
Mutability | Immutable | Mutable |
Memory | Stack (reference) | Heap (data) |
Use Case | Read-only access | Dynamic manipulation |
A Note on UTF-8
Rust strings are UTF-8 encoded, which means they support characters from all languages and even emojis. But here’s the catch:
-
.len()
gives the byte length, not the character count. - To count characters, use
.chars().count()
.
Example:
let s = "olá";
println!("Bytes: {}, Characters: {}", s.len(), s.chars().count());
// Output: Bytes: 5, Characters: 3
Real-World String Manipulation Examples
Time to get hands-on. Here are three practical ways to manipulate strings in Rust.
Example 1: Reversing a String
Goal: Reverse the characters in a string.
pub fn reverse_string(input: &str) -> String {
input.chars().rev().collect()
}
fn main() {
let s = "Hello, Rust!";
let reversed = reverse_string(s);
println!("Original: '{}', Reversed: '{}'", s, reversed);
}
What’s happening:
- We borrow the input as a
&str
. - Use
.chars()
to get an iterator over characters (not bytes). - Reverse the iterator and collect it into a new
String
.
✅ Handles Unicode characters properly.
Example 2: Appending Text to a String
Goal: Add a suffix to a String
using a mutable reference.
pub fn add_suffix(s: &mut String, suffix: &str) {
s.push_str(suffix);
}
fn main() {
let mut name = String::from("Alice");
println!("Before: {}", name);
add_suffix(&mut name, " in Wonderland");
println!("After: {}", name);
}
Why it works:
-
&mut String
gives us exclusive, mutable access. -
push_str()
adds the&str
to the end of the string. - No need to return anything—the original
String
is updated.
Example 3: Replacing Parts of a String
Goal: Replace all instances of one word with another.
pub fn replace_text(input: &str, from: &str, to: &str) -> String {
input.replace(from, to)
}
fn main() {
let sentence = "I like to code in Rust, Rust is great!";
let new_sentence = replace_text(sentence, "Rust", "Python");
println!("Original: '{}'", sentence);
println!("Replaced: '{}'", new_sentence);
}
Behind the scenes:
-
replace()
works on a&str
and returns a newString
. - Doesn’t modify the original—just returns the updated version.
Wrap-Up: Full Program Example
Here’s everything combined in a complete Rust program:
pub fn reverse_string(input: &str) -> String {
input.chars().rev().collect()
}
pub fn add_suffix(s: &mut String, suffix: &str) {
s.push_str(suffix);
}
pub fn replace_text(input: &str, from: &str, to: &str) -> String {
input.replace(from, to)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reverse_string() {
assert_eq!(reverse_string("Hello"), "olleH");
}
#[test]
fn test_add_suffix() {
let mut s = String::from("Hello");
add_suffix(&mut s, "!");
assert_eq!(s, "Hello!");
}
#[test]
fn test_replace_text() {
assert_eq!(replace_text("cat cat", "cat", "dog"), "dog dog");
}
}
fn main() {
// Reverse
let s1 = "Rust";
println!("Reversed: '{}'", reverse_string(s1));
// Append
let mut s2 = String::from("Rust");
add_suffix(&mut s2, " is fun");
println!("Appended: '{}'", s2);
// Replace
let s3 = "Rust Rust";
println!("Replaced: '{}'", replace_text(s3, "Rust", "Code"));
}
Run it with:
cargo run
Output:
Reversed: 'tsuR'
Appended: 'Rust is fun'
Replaced: 'Code Code'
Final Thoughts
- Use
&str
when reading or passing strings. - Use
String
when you need ownership or want to modify the data. - Always think in terms of Rust’s borrowing and ownership model.
- Remember
.len()
gives byte length—use.chars()
when working with Unicode.
You’ve got the tools now. Want a challenge? Try writing a function to capitalize each word or trim whitespace. Rust gives you the power—use it well.