Rust: Rust Slices: Efficient Data Views and String Slices
Last updated: 2026-08-26
A slice is a special type of reference in Rust—it does not point to the entire data set, but rather to a contiguous segment of it.
Slicing allows you to safely "extract" a portion of the data for processing without having to copy it.
1. What You'll Learn
- The Concept and Creation of String Slices
&str - Use the
[start..end]range syntax to create slices - String slices must fall on UTF-8 character boundaries
- Array Slicing
&[T]and Vector Slicing&[Vec] - Slices as function arguments—the most flexible type of string input
- Internal representation of a slice: pointer + length
2. Conceptual Diagrams
flowchart LR
subgraph "Heap memory"
HEAP["'H','e','l','l','o','','R','u','s','t','!'"]
end
subgraph "String s"
S_PTR["ptr"] --> HEAP
S_LEN["len: 12"]
S_CAP["cap: 12"]
end
subgraph "&str slice"
SL_PTR["ptr"] -->|"Pointing Forward5Byte"| HEAP
SL_LEN["len: 5"]
end
3. The Story of a Data Analyst
(1) Problem: Extracting information from the logs is too slow
Maria is a data analyst at an e-commerce company. She needs to extract IP addresses from a massive volume of server logs:
- Each log line has tens of thousands of characters
- If you use
substringto copy, each log entry will require a few thousand bytes of additional memory. - Processes 1,000,000 logs per day, with several GB of additional memory allocated
- The server crashed once due to an OOM (out of memory) error
"I need a way to extract data that allows me to 'just look, not copy'—just like running my finger along a line of text in a newspaper without having to write it down."
(2) Solutions for Rust Slices
fn main() {
let log_line = "192.168.1.1 - - [01/Jul/2026:12:00:00] \"GET /index.html\"";
// Slices -- do not copy, they just "point to" a portion of the original string
let ip = &log_line[0..13]; // "192.168.1.1"
let date = &log_line[20..38]; // "01/Jul/2026:12:00:00"
let path = &log_line[50..62]; // "/index.html"
println!("IP: {}", ip);
println!("Date: {}", date);
println!("Path: {}", path);
// Key Points: No strings were copied! All slices point to different areas of log_line
println!("The original logs are still available: {}", log_line);
}
A slice is like a "window" that points to a different view of the original data. There is no copying, no allocation—zero overhead. This is crucial when working with large-scale data.
4. The Principle of Slicing
(1) Memory Model
let s = String::from("Hello, Rust!");
let slice = &s[0..5]; // "Hello"
graph TB
subgraph "String s"
S_ptr[ptr ──→ H e l l o , R u s t !]
S_len[len: 12]
S_cap[cap: 12]
end
subgraph "&str slice"
SL_ptr[ptr ──→ H e l l o]
SL_len[len: 5]
end
S_ptr -.-> heap[Load the data]
SL_ptr -.-> heap
| Feature | Entire String |
&s[0..5] slice |
|---|---|---|
| Memory | ptr + len + cap (3 bytes) | ptr + len (2 bytes) |
| Ownership | Owned | Borrowed (cited) |
| Whether to copy data | — | Do not copy |
| Access Range | Entire string | "Hello" (5 bytes) |
(2) Scope Syntax
| Syntax | Meaning | Example |
|---|---|---|
[0..5] |
0 to 5 (excluding 5) | "Hello" |
[..5] |
From the beginning to 5 | "Hello" |
[5..] |
From 5 to the end | ", Rust!" |
[..] |
Entire string | "Hello, Rust!" |
(3) Quick Reference for Slice Types
| Slice Type | Syntax | Size | Description |
|---|---|---|---|
| String slice | &str |
16 bytes | ptr + len (fat pointer) |
| Array Slicing | &[T] |
16 bytes | ptr + len (fat pointer) |
| Array reference | &[T; N] |
8 bytes | ptr only (length known at compile time) |
| Variable slice | &mut [T] |
16 bytes | ptr + len (modifiable elements) |
5. Slicing Examples
▶ Example 1: String Slicing (Difficulty ⭐)
Output:
hello: '<hello>'
rust: '<rust>'
world: '<world>'
full: '<full>'
Literal Slicing: '<first_word>'
// ============================================
// Basic Usage of String Slicing
// ============================================
fn main() {
let s = String::from("Hello, Rust World!");
// Various Slicing Methods
let hello = &s[..5]; // "Hello"
let rust = &s[7..11]; // "Rust"
let world = &s[12..]; // "World!"
let full = &s[..]; // All
println!("hello: '{}'", hello);
println!("rust: '{}'", rust);
println!("world: '{}'", world);
println!("full: '{}'", full);
// String literals are, by their very nature, &str
let literal: &str = "Create a slice directly";
let first_word = &literal[..2];
println!("Literal Slicing: '{}'", first_word);
}
Output:
Rust Part: <rust>
Byte Index <i>: Character '<c>'
First 4 character slice: <safe_slice>
A string literal (such as
"hello") is itself of type&str—a slice pointing to a binary file. It does not require the&prefix because it is already a reference.
▶ Example 2: String Slicing Boundary Pitfalls (Difficulty ⭐⭐)
Output:
Rust Part: <rust>
Byte Index <i>: Character '<c>'
First 4 character slice: <safe_slice>
// ============================================
// UTF-8 Boundary: Slices must be at character boundaries.
// ============================================
fn main() {
let s = "RustProgramming"; // Number of bytes: R(1) u(1) s(1) t(1) Bian(3) Cheng(3) = 10 bytes
// Security Slices
let rust = &s[..4]; // "Rust" (first 4 bytes are exactly ASCII)
println!("Rust Part: {}", rust);
// The following are examples of errors (uncomment to see panic!)
// let bad = &s[0..5]; // ❌ 5 is between 4 and 6, landing in the middle of "Bian"'s first byte
// A Safe Approach: use chars() and char_indices() to iterate
for (i, c) in s.char_indices() {
println!("Byte Index {}: Character '{}'", i, c);
}
// Use char_indices to find safe slice boundaries
if let Some((pos, _)) = s.char_indices().nth(4) {
let safe_slice = &s[..pos];
println!("First 4 character slice: {}", safe_slice);
}
}
Output:
Array Slicing: <slice_arr>
Vector Slicing(first 3): <slice_vec>
Modified original vector: [1, 2, 3, 4, 5]
&[i32] Occupancy: <std::mem::size_of::<&[i32]>()> Byte
&[i32;5] Occupancy: <std::mem::size_of::<&[i32; 5]>()> Byte
Slices are indexed by bytes, not by characters. If a slice boundary falls in the middle of a multibyte character, the program will panic and crash. Use
.char_indices()to obtain a safe character-boundary index.
▶ Example 3: Array Slicing vs. Vector Slicing (Difficulty ⭐⭐)
Output:
Array Slicing: <slice_arr>
Vector Slicing(first 3): <slice_vec>
Modified original vector: [1, 2, 3, 4, 5]
&[i32] Occupancy: <std::mem::size_of::<&[i32]>()> Byte
&[i32;5] Occupancy: <std::mem::size_of::<&[i32; 5]>()> Byte
// ============================================
// Slicing Arrays and Vectors
// ============================================
fn main() {
// Array Slicing
let arr = [1, 2, 3, 4, 5];
let slice_arr = &arr[1..4]; // [2, 3, 4]
println!("Array Slicing: {:?}", slice_arr);
// Vector Slicing
let vec = vec![10, 20, 30, 40, 50];
let slice_vec = &vec[..3]; // [10, 20, 30]
println!("Vector Slicing(first 3): {:?}", slice_vec);
// Edit Slice Content (requires &mut)
let mut numbers = vec![1, 2, 3, 4, 5];
let mut_slice = &mut numbers[1..4]; // [2, 3, 4]
mut_slice[0] = 99; // Modifying a slice affects the original vector.
println!("Modified original vector: {:?}", numbers);
// Slice Type Size
println!("&[i32] Occupancy: {} Byte", std::mem::size_of::<&[i32]>());
println!("&[i32;5] Occupancy: {} Byte", std::mem::size_of::<&[i32; 5]>());
}
Output:
The first word in the literal string: '<result1>'
String The First Word: '<result2>'
Array Slicing and: <sum>
A slice type (
&[i32]) occupies 16 bytes (8 bytes for the pointer + 8 bytes for the length), while a regular reference (&[i32;5]) occupies only 8 bytes (just the pointer). This is what is known as a "fat pointer"—a slice is a pointer that includes length information.
▶ Example 4: Slices as Function Arguments (Difficulty ⭐⭐)
Output:
The first word in the literal string: '<result1>'
String The First Word: '<result2>'
Array Slicing and: <sum>
// ============================================
// &str As a function argument -- the most flexible approach
// ============================================
// ✅ Best Practices: Receive &str -- regardless of whether the input is &str or &String
fn first_word(s: &str) -> &str {
for (i, &b) in s.as_bytes().iter().enumerate() {
if b == b' ' {
return &s[..i];
}
}
&s[..] // No spaces, return the entire string
}
fn main() {
// Incoming &str Literal
let result1 = first_word("hello world");
println!("The first word in the literal string: '{}'", result1);
// Incoming &String (automatically converts to &str)
let s = String::from("Rust is awesome");
let result2 = first_word(&s); // &String Automatically convert to &str
println!("String The First Word: '{}'", result2);
// Passing an array slice
let arr = [1, 2, 3, 4, 5];
let sum: i32 = sum_slice(&arr[..]); // Use [..] to convert array to slice
println!("Array Slicing and: {}", sum);
}
fn sum_slice(slice: &[i32]) -> i32 {
let mut total = 0;
for x in slice {
total += *x;
}
total
}
Output:
The first word in the literal string: 'hello'
String The First Word: 'Rust'
Array Slicing and: 15
Best Practice: When a function parameter accepts a string, use
&strinstead of&String.&Stringis automatically converted to&str(via a Deref cast), so the&strparameter is more versatile—it accepts both literals and String references.
▶ Example 5: Comprehensive Exercise—Log Parser (Difficulty ⭐⭐⭐)
Output:
=== Log Analysis (Zero-Copy) ===
IP: <ip> Methods: <method> Path: <path>
=== Longest Path ===
Longest Path: '<find_longest(&paths)>'
=== Statistics on Numerical Slices ===
Grade Slices: [85, 92, 78, 95, 88, 70, 96]
Average: <avg>, Lowest: <min>, Highest: <max>
// ============================================
// Practical Guide to Slicing: Zero-Copy Log Parsing
// ============================================
fn parse_log_line(line: &str) -> (&str, &str, &str) {
let ip_end = line.find(' ').unwrap_or(line.len());
let ip = &line[..ip_end];
let rest = &line[ip_end..].trim_start();
let method_end = rest.find(' ').unwrap_or(rest.len());
let method = &rest[..method_end];
let path_part = &rest[method_end..].trim_start();
let path_end = path_part.find(' ').unwrap_or(path_part.len());
let path = &path_part[..path_end];
(ip, method, path)
}
fn find_longest<'a>(strings: &[&'a str]) -> &'a str {
strings.iter().max_by_key(|s| s.len()).unwrap_or(&"")
}
fn summarize(data: &[i32]) -> (f64, i32, i32) {
if data.is_empty() {
return (0.0, 0, 0);
}
let sum: i32 = data.iter().sum();
let avg = sum as f64 / data.len() as f64;
let min = *data.iter().min().unwrap();
let max = *data.iter().max().unwrap();
(avg, min, max)
}
fn main() {
let logs = [
"192.168.1.1 GET /index.html HTTP/1.1",
"10.0.0.5 POST /api/login HTTP/1.1",
"172.16.0.1 DELETE /api/user/42 HTTP/1.1",
];
println!("=== Log Analysis (Zero-Copy) ===");
for log in &logs {
let (ip, method, path) = parse_log_line(log);
println!("IP: {:<15} Methods: {:<6} Path: {}", ip, method, path);
}
println!("\n=== Longest Path ===");
let paths: Vec<&str> = logs.iter().map(|l| {
let (_, _, p) = parse_log_line(l);
p
}).collect();
println!("Longest Path: '{}'", find_longest(&paths));
println!("\n=== Statistics on Numerical Slices ===");
let scores = [85, 92, 78, 95, 88, 70, 96];
let (avg, min, max) = summarize(&scores);
println!("Grade Slices: {:?}", scores);
println!("Average: {:.1}, Lowest: {}, Highest: {}", avg, min, max);
}
Output:
=== Log Analysis (Zero-Copy) ===
IP: 192.168.1.1 Methods: GET Path: /index.html
IP: 10.0.0.5 Methods: POST Path: /api/login
IP: 172.16.0.1 Methods: DELETE Path: /api/user/42
=== Longest Path ===
Longest Path: '/api/user/42'
=== Statistics on Numerical Slices ===
Grade Slices: [85, 92, 78, 95, 88, 70, 96]
Average: 86.3, Lowest: 70, Highest: 96
During log parsing, the three
&strvalues returned byparse_log_lineall point to different regions of the original string—zero-copy, zero-allocation.find_longestaccepts a slice of&[&str], making it extremely versatile. Statistical functions for numeric slices also only borrow data.
❓ FAQ
&str stores two pieces of information in memory: "where it points" and "how long it is," whereas &String stores only "where it points." A slice lets you know the boundaries of the data, preventing out-of-bounds access.&s[0..4] On an ASCII string, this would be exactly “hello,” but on the string “RustBianCheng,” it is “Rust” (4 bytes). &s[0..5] It falls on the middle byte of the character “Bian,” and the compiler cannot determine if this is a valid character, so it panics.vec![1,2,3] with &vec[..] create new data?📖 Summary
- A slice is a "view" of the data; it does not own the data or create a copy of it.
- String slicing
&strrefers to a segment of a String or a literal - Range syntax:
[start..end],[..end],[start..],[..] - A slice is a fat pointer (pointer + length) that occupies 16 bytes
- String slices must fall on UTF-8 character boundaries
- Use
&strfor function parameters that accept strings (more versatile than&String) - Array slicing
&[T]works the same way as vector slicing
📝 Exercises
- Difficulty ⭐: Create a
String, use[..]to slice the entire string, and verify that the type of the slice is&str. - Difficulty ⭐⭐: Write a function
fn last_word(s: &str) -> &strthat returns the last word (separated by spaces). Call it on both a string literal andString. - Difficulty ⭐⭐⭐: Create a string containing Chinese characters and emojis
"Rust🦀BianCheng", use.char_indices()to find the safe slice boundary, and extract only the "Rust🦀" portion (Hint: 🦀 takes up 4 bytes).