Web Analytics

Strings in Rust

Beginner ~25 min read

Strings in Rust are more complex than in many other languages due to Rust's focus on safety and UTF-8 encoding. In this lesson, you'll learn about the two main string types (String and &str), how to create and manipulate strings, and common pitfalls to avoid.

String vs &str

Rust has two main string types:

Feature String &str
Storage Heap-allocated Stack or binary (immutable)
Ownership Owned Borrowed (reference)
Mutability Can be mutable Always immutable
Size Growable Fixed size
Use case When you need to own/modify String literals, slices
Key Insight: &str is a string slice - a view into string data stored elsewhere. String is an owned, growable string type.
Output
Click Run to execute your code

Creating Strings

There are several ways to create strings in Rust:

// String literals (&str) - hardcoded in binary
let greeting = "Hello, World!";

// String::from() - creates owned String
let s1 = String::from("Hello");

// .to_string() method - converts &str to String
let s2 = "World".to_string();

// String::new() - creates empty String
let mut s3 = String::new();
s3.push_str("Hello");
Tip: Use &str for string literals and function parameters (more flexible). Use String when you need to own or modify the string.

UTF-8 Encoding

Rust strings are always valid UTF-8. This means:

  • Each character can be 1-4 bytes
  • You cannot index strings directly (no s[0])
  • .len() returns byte count, not character count
  • Emoji and international characters are fully supported
let hello = "Hello";
println!("Length: {} bytes", hello.len()); // 5 bytes

let emoji = "Hello 👋";
println!("Length: {} bytes", emoji.len()); // 10 bytes (👋 is 4 bytes!)
println!("Chars: {}", emoji.chars().count()); // 7 characters
Warning: String indexing like s[0] is not allowed in Rust because characters can be multiple bytes. Use .chars() or .bytes() to iterate.

String Methods

Rust provides many useful methods for working with strings:

Method Description Example
push(char) Add a character s.push('!')
push_str(&str) Add a string slice s.push_str(" World")
len() Get byte length s.len()
is_empty() Check if empty s.is_empty()
contains(&str) Check substring s.contains("Rust")
replace(&str, &str) Replace substring s.replace("old", "new")
Output
Click Run to execute your code

String Concatenation

There are several ways to combine strings:

Using the + Operator

let s1 = String::from("Hello");
let s2 = String::from("World");
let s3 = s1 + " " + &s2; // s1 is moved here!
// println!("{}", s1); // Error: s1 was moved
Ownership Note: The + operator takes ownership of the left operand. Use & for the right operand to borrow it.

Using the format! Macro

let s1 = String::from("Hello");
let s2 = String::from("World");
let s3 = format!("{} {}", s1, s2); // s1 and s2 still valid!
println!("{}", s1); // OK: s1 not moved
Best Practice: Use format! for concatenation when you need to keep ownership of all strings. It's more flexible and doesn't move values.

String Slicing

You can create string slices using range syntax:

let s = String::from("Hello, World!");
let hello = &s[0..5];   // "Hello"
let world = &s[7..12];  // "World"
println!("{} {}", hello, world);
Danger: Slicing must occur at valid UTF-8 character boundaries! Slicing in the middle of a multi-byte character will panic.
let emoji = "👋";
// let bad = &emoji[0..1]; // PANIC! 👋 is 4 bytes
let good = &emoji[0..4];   // OK: full character

Iterating Over Strings

Rust provides two main ways to iterate over strings:

By Characters

for c in "Hello".chars() {
    println!("{}", c);
}
// Output: H e l l o

By Bytes

for b in "Hello".bytes() {
    println!("{}", b);
}
// Output: 72 101 108 108 111 (ASCII values)
When to use which:
  • Use .chars() when working with Unicode characters
  • Use .bytes() when working with raw byte data

Common Mistakes

1. Trying to index strings

Wrong:

let s = String::from("Hello");
let c = s[0];  // Error: cannot index into a string

Correct:

let s = String::from("Hello");
let c = s.chars().nth(0).unwrap();  // Get first character

2. Confusing byte length with character count

Problem:

let s = "Hello 👋";
println!("{}", s.len());  // 10 bytes, not 7 characters!

Solution:

let s = "Hello 👋";
println!("Bytes: {}", s.len());
println!("Chars: {}", s.chars().count());

3. Moving strings unintentionally

Wrong:

let s1 = String::from("Hello");
let s2 = s1 + " World";
println!("{}", s1);  // Error: s1 was moved

Correct:

let s1 = String::from("Hello");
let s2 = format!("{} World", s1);
println!("{}", s1);  // OK: s1 still valid

Exercise: String Manipulation

Task: Fix the code to make it compile and run correctly.

Requirements:

  • Create mutable and immutable strings
  • Concatenate strings without moving ownership
  • Extract substrings safely
  • Count characters correctly (not bytes)
  • Convert strings to uppercase
Output
Click Run to execute your code
Show Solution
fn main() {
    // Create a mutable String
    let mut language = String::from("Rust");
    
    // Add to the string
    language.push_str(" is awesome!");
    println!("{}", language);
    
    // Concatenate without moving
    let first = String::from("Hello");
    let second = String::from("World");
    let combined = format!("{} {}", first, second);
    println!("{}", combined);
    println!("First: {}, Second: {}", first, second);
    
    // Extract first word
    let sentence = "Rust programming language";
    let first_word = &sentence[0..4]; // "Rust"
    println!("First word: {}", first_word);
    
    // Count characters
    let emoji_text = "Hello 👋 World 🌍";
    let char_count = emoji_text.chars().count();
    println!("Character count: {}", char_count);
    
    // Convert to uppercase
    let message = "rust is great";
    println!("Uppercase: {}", message.to_uppercase());
}

Summary

  • Two string types: String (owned, heap) and &str (borrowed, slice)
  • UTF-8 encoding: All Rust strings are valid UTF-8
  • No indexing: Cannot use s[i] due to variable-width characters
  • String methods: push, push_str, len, contains, etc.
  • Concatenation: Use + (moves left operand) or format! (doesn't move)
  • Slicing: Must occur at UTF-8 character boundaries
  • Iteration: Use .chars() for characters, .bytes() for bytes
  • .len() returns bytes, .chars().count() returns characters

What's Next?

Now that you understand strings, you're ready to learn about type conversion. In the next lesson, you'll learn how to convert between different types using the as keyword, From/Into traits, and safe conversion methods.