Slices: Difference between revisions
Created page with "Yesterday we checked out Loops in-depth and other niceties around them, if you missed it, I’d recommend checking it out below. Loops Loops in programming (fundamental and very useful), including Rust, are like a repeating task you tell the computer to… medium.com In this post, let’s discuss Slices, it’s day 19 here we go! A slice in Rust is like a window into a portion of a sequence, such as an array, vector, or string. It’s a reference to a contiguous chunk..." |
(No difference)
|
Latest revision as of 19:05, 23 November 2025
Yesterday we checked out Loops in-depth and other niceties around them, if you missed it, I’d recommend checking it out below.
Loops Loops in programming (fundamental and very useful), including Rust, are like a repeating task you tell the computer to… medium.com
In this post, let’s discuss Slices, it’s day 19 here we go! A slice in Rust is like a window into a portion of a sequence, such as an array, vector, or string. It’s a reference to a contiguous chunk of data, letting you work with just that part without taking ownership or copying anything.
Think of it as borrowing a few pages from a book instead of buying the whole book or photocopying it. This makes slices memory-efficient and fast, which is critical in performance-sensitive applications like game development or data processing. Slices come in two main flavors: array/vector slices (&[T]) and string slices (&str). Both are references, meaning they don’t own the data they point to, and they’re defined by a starting point and a length.
For example, if you have an array of numbers, a slice might let you focus on just the first three elements without touching the rest.
Below is a simple example to get us started. Imagine you’re building a log parser for a web server, and you need to process the first few entries of a large log stored in a vector:
fn main() {
let logs = vec!["GET /home", "POST /login", "GET /profile", "PUT /settings"];
let recent_logs = &logs[0..2]; // Slice of the first two entries
println!("Recent logs: {:?}", recent_logs); // Prints: ["GET /home", "POST /login"]
}
In the above code, recent_logs is a slice (&[&str]) that references just the first two elements of the logs vector. No copying happens, and Rust’s borrow checker ensures you don’t accidentally modify the original vector if you’re only borrowing it. Let’s move on to how slices are created and used in more complex scenarios, like those you’d encounter in a real project.
Creating and Using Slices
Slices are created using the .. range syntax, which specifies the start and end indices of the portion you want.
The syntax is flexible: you can write 0..2 for the first two elements, 1.. to start at index 1 and go to the end, or even ..3 to start from the beginning up to index 2. This flexibility is handy when you’re dealing with dynamic data.
Suppose you’re working on a music streaming app, and you need to display a subset of songs from a playlist based on user input. The playlist is stored as a vector, and the user wants to see songs 3 through 5. Here’s how you might handle it:
fn get_playlist_slice(playlist: &[String], start: usize, end: usize) -> &[String] {
&playlist[start..end]
}
fn main() {
let playlist = vec![
String::from("Song A"),
String::from("Song B"),
String::from("Song C"),
String::from("Song D"),
String::from("Song E"),
];
let user_selection = get_playlist_slice(&playlist, 2, 5);
println!("User's selection: {:?}", user_selection); // Prints: ["Song C", "Song D", "Song E"]
}
This function above takes a slice of the playlist as input (so it works with vectors or arrays) and returns a slice of the requested range. Notice how we pass &playlist to avoid moving the vector, and the function returns a slice (&[String]) that borrows the data. This is a common pattern in Rust: functions that process data often take slices as arguments to be flexible and avoid ownership issues.
String slices (&str) work similarly but are suited for text. Let’s say you’re parsing a CSV file for a data analytics tool, and you need to extract the header row’s first column. The CSV is stored as a string, and you want just the part before the first comma:
fn get_first_column(csv: &str) -> &str {
csv.split(',').next().unwrap_or("")
}
fn main() {
let csv_data = "name,age,city\nAlice,30,New York";
let first_column = get_first_column(csv_data);
println!("First column: {}", first_column); // Prints: "name"
}
In the code above, get_first_column returns a &str slice of the input string, pointing to the substring before the first comma. Since it’s a slice, there’s no allocation or copying, just a reference to the original string’s data. Now that we’ve seen how to create and use slices, let’s look at some real-world scenarios where they’re particularly useful.
Real-World Scenarios for Slices
Slices shine in situations where you need to process parts of data efficiently. Here are a few practical examples inspired by real-world tasks:
1. Filtering Log Data in a Monitoring System
Imagine you’re building a monitoring system for a cloud service, and you need to analyze recent error logs from a massive log array. You only want errors from the last hour, which correspond to the last 100 entries. Slices make this easy:
fn get_recent_errors(logs: &[String], count: usize) -> &[String] {
let start = logs.len().saturating_sub(count);
&logs[start..]
}
fn main() {
let logs = vec![
String::from("INFO: Server started"),
String::from("ERROR: Connection failed"),
String::from("ERROR: Timeout"),
String::from("INFO: Request processed"),
];
let recent_errors = get_recent_errors(&logs, 2);
println!("Recent errors: {:?}", recent_errors); // Prints: ["ERROR: Connection failed", "ERROR: Timeout"]
}
The saturating_sub method ensures we don’t underflow if the log is shorter than count, and the slice &logs[start..] gives us the last two entries efficiently.
2. Processing Image Data in a Graphics Editor
In a graphics editor, you might need to apply a filter to a specific region of an image, represented as a 1D array of pixels. Slices let you focus on just the region of interest:
fn apply_grayscale(pixels: &mut [u8], start: usize, end: usize) {
for pixel in pixels[start..end].iter_mut() {
*pixel = (*pixel as u32 * 3 / 10) as u8; // Simple grayscale conversion
}
}
fn main() {
let mut image_data = vec![255, 128, 64, 192, 32];
apply_grayscale(&mut image_data, 1, 4);
println!("Processed pixels: {:?}", image_data); // Prints: [255, 38, 19, 57, 32]
}
Here, the slice &mut image_data[1..4] lets us modify a specific range of pixels without affecting the rest of the image.
3. Parsing API Responses
When working with JSON responses in a web app, you might need to extract a specific field from a string. For example, suppose an API returns a JSON string, and you want to grab the value of a "user_id" field. String slices can help you avoid unnecessary allocations:
fn extract_user_id(json: &str) -> Option<&str> {
json.find("\"user_id\":")
.map(|start| {
let value_start = start + "\"user_id\":".len();
let value_end = json[value_start..].find(',').map(|i| i + value_start).unwrap_or(json.len());
json[value_start..value_end].trim()
})
}
fn main() {
let json_response = r#"{"user_id": "12345", "name": "Alice"}"#;
if let Some(user_id) = extract_user_id(json_response) {
println!("User ID: {}", user_id); // Prints: "12345"
}
}
This function uses string slices to extract the user_id value without copying the string, keeping memory usage low.
These examples we have seen above show how slices fit naturally into tasks like log analysis, image processing, and API parsing. Next, let’s talk about some common pitfalls and how to avoid them.
Avoiding Common Pitfalls Slices are powerful, but Rust’s strict borrow checker can trip you up if you’re not careful. Here are some issues you might hit in real projects and how to handle them:
1. Lifetime Issues
Since slices are references, they’re tied to the lifetime of the data they point to. If the underlying data (like a vector) is dropped, the slice becomes invalid. For example:
fn bad_slice() -> &[i32] {
let numbers = vec![1, 2, 3];
&numbers[0..2] // Error: `numbers` doesn't live long enough
}
The code above won’t compile because the vector numbers is dropped when the function ends, but the slice tries to reference it. To fix this, ensure the data outlives the slice, perhaps by returning the vector along with the slice or using a static lifetime.
2. Index Out of Bounds
Accessing a slice with invalid indices causes a panic. Imagine you’re slicing user input data, and the user provides bad indices:
fn safe_slice(data: &[i32], start: usize, end: usize) -> Option<&[i32]> {
if start <= end && end <= data.len() {
Some(&data[start..end])
} else {
None
}
}
fn main() {
let numbers = vec![1, 2, 3, 4];
match safe_slice(&numbers, 2, 5) {
Some(slice) => println!("Slice: {:?}", slice),
None => println!("Invalid range"),
} // Prints: "Invalid range"
}
By checking the indices, you avoid panics and make your code more reliable, especially in user-facing applications.
3. Mutable vs. Immutable Borrowing
Rust enforces that you can’t have a mutable and immutable borrow of the same data at the same time. This can bite you when working with mutable slices:
fn main() {
let mut data = vec![1, 2, 3];
let slice1 = &data[0..2];
let slice2 = &mut data[1..3]; // Error: cannot borrow `data` mutably
println!("{:?}, {:?}", slice1, slice2);
}
To fix this, always ensure mutable slices don’t overlap with other borrows, or use separate scopes to limit borrow lifetimes. That was all on slices
Read the full article here: https://medium.com/rustaceans/slices-c43e8a33d0bc