Rust: A Comprehensive Guide to Rust Strings

Last updated: 2026-08-26

Rust has not one, but two string types—because system programming requires precise control over memory.

String handling in Rust is a source of confusion for many beginners: Why are there two types—String and &str? How do they differ from char* in C? This lesson will help you fully understand it.


1. What You'll Learn



2. The Story of a Librarian

(1) Problem: The JSON data is too long to fit

Maria is a librarian who is developing a small book search system to manage 5,000 books.

"It would be so great if there were strings that automatically managed memory..." She later found the answer in Rust.

(2) Rust's String Solution

RUST
fn main() {
    // &str: String Literals, Present in the binary at compile time
    let title_book1: &str = "One Hundred Years of Solitude";

    // String: Allocated on the heap, Variable length, Automatic Memory Management
    let mut title_book2 = String::from("How to Make a Complete Map of an Infinite Universe");
    title_book2.push_str(" (Second Edition)");  // Additional Content

    println!("Book 1: {} (Length: {} Bytes)", title_book1, title_book1.len());
    println!("Book 2: {} (Length: {} Bytes)", title_book2, title_book2.len());
    // String Automatically released when leaving the scope, No manual free required
}

Rust's String is like Java's strings—it automatically manages memory; &str is like C's const char*—read-only references. Together, they strike a balance between performance and flexibility.



3. The Difference Between &str and String

(1) Memory Model

100%
graph LR
    subgraph "&str (String Literals)"
        S1_ptr[ptr ──→ ...rust...]
        S1_len[len: 4]
    end
    subgraph "String (Heap Allocation)"
        S2_ptr[ptr ──→ Rust is...]
        S2_len[len: 10]
        S2_cap[cap: 16]
    end
    subgraph "Storage Location"
        STACK[Stack: &str<br>Read-Only Borrow]
        HEAP[Heap: String<br>Ownership]
    end
    S1_ptr -.->|Determined at compile time| STACK
    S2_ptr -.->|Runtime Allocation| HEAP
Characteristics &str String
Owner Read-only access Full ownership
Mutability Immutable Can be appended/modified
Storage Location Read-only data segment (at compile time) Heap (allocated at runtime)
Creation Method Literal "hello" String::from() or .to_string()
Use Cases Read-only string parameters Need to modify or take ownership of the string

(2) Quick Reference for Common String Methods

Method Return Type Description Example
push_str(&str) () Additional character string slice s.push_str("abc")
push(char) () Add a single character s.push('!')
len() usize Return Byte Length s.len()
is_empty() bool Is it empty? s.is_empty()
trim() &str Trim leading and trailing spaces s.trim()
contains(&str) bool Whether the substring is included s.contains("rust")
replace(&str, &str) String Replace all matches s.replace("a", "b")
Split by delimiter split(char) Split s.split(',')
Convert to lowercase
Upper case to_uppercase() String Switch to uppercase
chars() Chars Iterate by character s.chars()
bytes() Bytes Iterate by bytes s.bytes()
Clear string
capacity() usize Return to Buffer Capacity s.capacity()

(3) Creation Method

RUST
// &str Create
let s1: &str = "hello, world";       // String Literals
let s2: &str = r"raw string\n";      // Original string(Do not escape)

// String Create
let s3 = String::from("hello");
let s4 = "world".to_string();
let s5 = format!("{}-{}", s3, s4);   // Format and Create
let s6: String = "hi".into();         // Through Type Conversion

(4) Quick Reference for String Conversion Methods

Source Type Target Type Conversion Method Zero-Overhead
Allocate heap memory
Allocate heap memory
Allocate heap memory
String &str &s or s.as_str() Zero-cost (borrow)
String &str &s[..] Zero-cost (slice)
&str &[u8] "hello".as_bytes() Zero-cost (borrowing)
String Vec<u8> s.into_bytes() Zero Overhead (Transfer of Ownership)
Vec<u8> String String::from_utf8(v) Zero overhead (transfer of ownership; UTF-8 verification required)


4. Common String Operations

▶ Example 1: Creating and Modifying Strings (Difficulty ⭐)

Output:

TEXT 📖 Display only
After the addition: Hello
After concatenation: Hello, World!
format Results: tic-tac-toe
RUST
// ============================================
// String Creation, Append and Concatenation
// ============================================

fn main() {
    // Create
    let mut s = String::from("Hello");

    // Add
    s.push_str(", ");       // Append a string slice
    s.push('R');            // Add a single character
    s.push_str("ust!");
    println!("After the addition: {}", s);

    // Concatenation (+ operator consumes the left-hand side String)
    let s1 = String::from("Hello, ");
    let s2 = String::from("World!");
    let s3 = s1 + &s2;      // s1 Moved, It can't be used anymore.
    println!("After concatenation: {}", s3);

    // format! macro (Does not consume any variables)
    let a = String::from("tic");
    let b = String::from("tac");
    let c = String::from("toe");
    let result = format!("{}-{}-{}", a, b, c);
    println!("format Results: {}", result);
}

Output:

TEXT 📖 Display only
Byte Length: 9
First 4 Byte Slice: <slice1>
<c> 

<b> 



The + operator consumes (moves) the String on its left, so s1 can no longer be used after concatenation. format! does not consume any variables; ownership of all variables is retained.


▶ Example 2: String Slicing and Indexing (Difficulty ⭐⭐)

Output:

TEXT 📖 Display only
Byte Length: 9
First 4 Byte Slice: <slice1>
<c> 

<b> 
RUST
// ============================================
// String Slicing -- Note UTF-8 Boundary!
// ============================================

fn main() {
    let s = String::from("Rust🦀Code");

    // Get the length of a string (Number of bytes, Not the number of characters)
    println!("Byte Length: {}", s.len());         // 11 (4 Byte ASCII + 4 Byte UTF-8 emoji + 4 Byte ASCII)

    // String Slicing (Index by Byte, Must be at a character boundary)
    let slice1 = &s[0..4];                     // "Rust" (first 4 bytes fall exactly on a character boundary)
    println!("First 4 Byte Slice: {}", slice1);

    // &s[0..5] will panic! (Because bytes 4-5 are in the middle of the emoji)

    // Iterate by character count
    for c in s.chars() {
        print!("{} ", c);
    }
    println!();

    // Iterate by byte
    for b in s.bytes() {
        print!("{:02x} ", b);
    }
    println!();
}

Output:

TEXT 📖 Display only
Byte Length: 11
First 4 Byte Slice: Rust
R u s t 🦀 C o d e
52 75 73 74 f0 9f a6 80 43 6f 64 65

Rust strings do not support direct index-based access (such as s[0]) because UTF-8 characters are variable-length. You must iterate through the characters using .chars() or use byte slices. Slices must align with character boundaries; otherwise, a runtime crash will occur.


▶ Example 3: Common String Methods (Difficulty ⭐⭐)

Output:

TEXT 📖 Display only
trim: 'Hello, Rust World!'
contains 'Rust': true
replace: '  Hello, Go World!  '
split: <parts>
lower: hello, upper: WORLD
RUST
// ============================================
// Common String Queries, Replace, Cutting Methods
// ============================================

fn main() {
    let s = "  Hello, Rust World!  ";

    // Remove leading and trailing spaces
    println!("trim: '{}'", s.trim());

    // Includes a check
    println!("contains 'Rust': {}", s.contains("Rust"));

    // Replace
    let replaced = s.replace("Rust", "Go");
    println!("replace: '{}'", replaced);

    // Split
    let parts: Vec<&str> = "apple,banana,cherry".split(',').collect();
    println!("split: {:?}", parts);

    // Case Conversion
    let lower = "HELLO".to_lowercase();
    let upper = "world".to_uppercase();
    println!("lower: {}, upper: {}", lower, upper);
}

Output:

TEXT 📖 Display only
String: Rust🦀Code
Byte Length: 9
Number of characters: 9

--- Character-by-character traversal ---
Character <i>: '<c>' (Unicode: U+<c as u32>)

--- Traverse byte by byte ---
<b> 

trim() and split() return &str without creating a new string. replace() returns a brand-new String. Consider the performance overhead when making modifications.


▶ Example 4: Iterating Through UTF-8 Strings and Boundary Checks (Difficulty ⭐⭐⭐)

Output:

TEXT 📖 Display only
String: Rust🦀Code
Byte Length: 9
Number of characters: 9

--- Character-by-character traversal ---
Character <i>: '<c>' (Unicode: U+<c as u32>)

--- Traverse byte by byte ---
<b> 



--- Safe Slices ---
ASCII Part &s[0..4]: '<ascii_part>'
Emoji Part &s[4..8]: '<emoji_part>'
Code Part &s[8..12]: '<code_part>'
Concatenated Reconstruction: <combined>
RUST
// ============================================
// UTF-8 Bytes in a string, Character and Grapheme Clusters
// ============================================

fn main() {
    let s = "Rust🦀Code";

    println!("String: {}", s);
    println!("Byte Length: {}", s.len());
    println!("Number of characters: {}", s.chars().count());

    println!("\n--- Character-by-character traversal ---");
    for (i, c) in s.chars().enumerate() {
        println!("Character {}: '{}' (Unicode: U+{:04X})", i, c, c as u32);
    }

    println!("\n--- Traverse byte by byte ---");
    for (i, b) in s.bytes().enumerate() {
        print!("{:02x} ", b);
        if (i + 1) % 8 == 0 { println!(); }
    }
    println!();

    println!("\n--- Safe Slices ---");
    let ascii_part = &s[0..4];
    println!("ASCII Part &s[0..4]: '{}'", ascii_part);

    let emoji_start = 4;
    let emoji_end = emoji_start + 4;
    let emoji_part = &s[emoji_start..emoji_end];
    println!("Emoji Part &s[{}..{}]: '{}'", emoji_start, emoji_end, emoji_part);

    let code_start = emoji_end;
    let code_end = code_start + 4;
    let code_part = &s[code_start..code_end];
    println!("Code Part &s[{}..{}]: '{}'", code_start, code_end, code_part);

    let combined = format!("{}{}{}", ascii_part, emoji_part, code_part);
    println!("Concatenated Reconstruction: {}", combined);
}

Output:

TEXT 📖 Display only
String: Rust🦀Code
Byte Length: 12
Number of characters: 8

--- Character-by-character traversal ---
Character 0: 'R' (Unicode: U+0052)
Character 1: 'u' (Unicode: U+0075)
Character 2: 's' (Unicode: U+0073)
Character 3: 't' (Unicode: U+0074)
Character 4: '🦀' (Unicode: U+1F980)
Character 5: 'C' (Unicode: U+0043)
Character 6: 'o' (Unicode: U+006F)
Character 7: 'd' (Unicode: U+0064)
Character 8: 'e' (Unicode: U+0065)

--- Traverse byte by byte ---
52 75 73 74 f0 9f a6 80 43 6f 64 65

--- Safe Slices ---
ASCII Part &s[0..4]: 'Rust'
Emoji Part &s[4..8]: '🦀'
Code Part &s[8..12]: 'Code'
Concatenated Reconstruction: Rust🦀Code

In UTF-8, ASCII characters take up 1 byte and emojis take up 4 bytes. Slicing must occur at character boundaries; otherwise, a panic will occur at runtime. Use .chars().enumerate() to safely process characters one by one.


▶ Example 5: Practical String Parsing and Formatting (Difficulty: ⭐⭐)

Output:

TEXT 📖 Display only
Raw Data: <raw_data>

--- Parsing Key-Value Pairs ---
<key> => <value>

--- Construct a New String ---
Abstract: Alice, 30 years old, From Beijing

Personnel Report
RUST
// ============================================
// String Parsing, Combined Use of Formatting and Linking
// ============================================

fn main() {
    let raw_data = "name=Alice;age=30;city=Beijing";

    println!("Raw Data: {}", raw_data);
    println!("\n--- Parsing Key-Value Pairs ---");

    let pairs: Vec<&str> = raw_data.split(';').collect();
    for pair in pairs {
        let parts: Vec<&str> = pair.split('=').collect();
        if parts.len() == 2 {
            let key = parts[0].trim();
            let value = parts[1].trim();
            println!("{} => {}", key, value);
        }
    }

    println!("\n--- Construct a New String ---");
    let name = "Alice";
    let age = 30;
    let city = "Beijing";
    let summary = format!("{}, {} years old, From {}", name, age, city);
    println!("Abstract: {}", summary);

    let mut report = String::from("Personnel Report\n");
    report.push_str(&format!("Name: {}\n", name));
    report.push_str(&format!("Age: {}\n", age));
    report.push_str(&format!("City: {}\n", city));
    report.push_str("--- End ---");
    println!("\n{}", report);

    let csv_line = ["Alice", "30", "Beijing"].join(",");
    println!("\nCSV Format: {}", csv_line);
}

Output:

TEXT 📖 Display only
Raw Data: name=Alice;age=30;city=Beijing

--- Parsing Key-Value Pairs ---
name => Alice
age => 30
city => Beijing

--- Construct a New String ---
Abstract: Alice, 30 years old, From Beijing

Personnel Report
Name: Alice
Age: 30
City: Beijing
--- End ---

CSV Format: Alice,30,Beijing

split() + collect() are the basic patterns for parsing structured text; format! is suitable for one-time concatenation; push_str() is suitable for gradually constructing long texts; join() is suitable for connecting arrays with delimiters.


❓ FAQ

Q Why does Rust have two types of strings?
A To balance ownership and performance. &str is a read-only borrow (zero overhead), while String is an owned mutable string (with heap allocation overhead). Using &str for function parameters offers the most flexibility, since String can be automatically converted to &str.
Q How do you convert between String and &str?
A To convert &str to String, use .to_string() or String::from(); to convert String to &str, use &s or s.as_str(). All conversions are zero-cost (they simply represent memory from a different perspective).
Q Why can't s[0] access the first character?
A Because UTF-8 is a variable-length encoding. s[0] accesses the first byte, which is not necessarily a complete character. Rust is designed to be safer: it would rather throw a compile-time error than allow unexpected behavior at runtime.
Q Should I use + or format! for string concatenation?
A Use + for concatenating a small number of strings, and format! for multiple segments. + consumes the String on the left and reuses its buffer (for high performance), while format! does not consume the variable but creates a brand-new String. Use + for two or three segments, and format! for more than three.
Q Can strings be modified? Which can be modified, &str or String?
A Only String can be modified (after declaring mut, you can use methods such as .push_str(), .push(), .insert(), etc.). &str is a read-only reference; the data it points to cannot be modified. If you need to modify a string within a function, the parameter type should be &mut String.

📖 Summary


📝 Exercises

  1. Difficulty ⭐: Create a String, then call the .push_str(), .push(), +, and format! operators in sequence, and compare the results of the four approaches.
  2. Difficulty ⭐⭐: Write a function fn first_word(s: &str) -> &str that returns the first word in a string (separated by spaces). Call it using &str and String, respectively.
  3. Difficulty ⭐⭐⭐: Explore UTF-8 boundary issues—create a string containing the emoji 🦀 "Rust🦀", try slicing it with &s[0..5] and &s[0..6], and observe which one succeeds and which one crashes.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏