Understanding Slices in Rust
Introduction
Slices are one of Rust's most powerful features for working with sequences of data. They allow you to reference a contiguous sequence of elements within a collection without taking ownership of the data. Think of a slice as a "view" into a sequence of data, similar to looking at a section of a microscope slide.
String Slices
What is a String Slice?
A string slice (&str
) is a reference to a part of a String. It's the most common type of slice in Rust.
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // slice containing "hello"
let world = &s[6..11]; // slice containing "world"
println!("{} + {} = {}", hello, world, s);
}
String Slice Syntax
Rust provides flexible syntax for creating slices:
let s = String::from("hello");
let slice1 = &s[0..2]; // "he"
let slice2 = &s[..2]; // "he" (starting from 0)
let slice3 = &s[3..]; // "lo" (until the end)
let slice4 = &s[..]; // "hello" (the whole string)
String Literals vs String Types
In Rust, there are two main string types:
-
String Slice (
&str
)- Immutable reference to UTF-8 text
- Fixed size, known at compile time
- Stored directly in the program binary
let s = "hello"; // Type is &str
-
String Type (
String
)- Growable, mutable string type
- Heap-allocated
- Owned type
let s = String::from("hello"); // Type is String
Converting Between String Types
// From String to &str
let owned_string = String::from("hello");
let string_slice = &owned_string[..]; // or &owned_string
// From &str to String
let string_literal = "hello";
let owned_string = string_literal.to_string();
// or
let owned_string = String::from(string_literal);
String Slice Safety
String slices must occur at valid UTF-8 character boundaries:
let s = String::from("hello");
let slice = &s[0..1]; // OK - slices at character boundary
// let slice = &s[0..2]; // OK
// let bad_slice = &s[1..2]; // OK in this case (ASCII)
// let bad_slice = &s[1..4]; // OK in this case (ASCII)
// But with Unicode:
let s = String::from("नमस्ते");
// let bad_slice = &s[0..1]; // PANIC! Slices must occur at UTF-8 boundaries
let good_slice = &s[0..3]; // OK - slices at character boundary
Other Types of Slices
Array Slices
fn main() {
let numbers = [1, 2, 3, 4, 5];
let slice = &numbers[1..4];
println!("Slice: {:?}", slice); // [2, 3, 4]
// Iterate over slice
for num in slice.iter() {
println!("{}", num);
}
}
Vector Slices
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let slice = &numbers[1..4];
println!("Slice: {:?}", slice); // [2, 3, 4]
// Using slice in a function
print_slice(slice);
}
fn print_slice(slice: &[i32]) {
for num in slice {
println!("{}", num);
}
}
Common Patterns and Best Practices
1. Function Parameters
Use slices to accept different types of string arguments:
fn first_word(text: &str) -> &str {
let bytes = text.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &text[0..i];
}
}
&text[..]
}
fn main() {
// Works with String
let owned = String::from("hello world");
let word = first_word(&owned);
// Works with string literal
let literal = "hello world";
let word = first_word(literal);
}
2. Returning Slices
fn get_middle_section(data: &[i32]) -> &[i32] {
let mid = data.len() / 2;
&data[mid-1..mid+1]
}
3. Working with Mutable Slices
fn capitalize_first_char(slice: &mut [u8]) {
if !slice.is_empty() {
if slice[0].is_ascii_lowercase() {
slice[0] = slice[0].to_ascii_uppercase();
}
}
}
fn main() {
let mut bytes = String::from("hello").into_bytes();
capitalize_first_char(&mut bytes);
let capitalized = String::from_utf8(bytes).unwrap();
println!("{}", capitalized); // "Hello"
}
Common Pitfalls
1. Dangling Slices
fn main() {
let slice = create_slice(); // Error!
println!("{:?}", slice);
}
fn create_slice() -> &[i32] {
let numbers = vec![1, 2, 3];
&numbers[..] // Error: returns a reference to data owned by the function
}
2. Modifying While Slicing
fn main() {
let mut s = String::from("hello world");
let word = &s[..5];
s.clear(); // Error! Cannot modify s while word is borrowing from it
println!("word: {}", word);
}
3. Invalid Slice Indices
fn main() {
let s = String::from("hello");
let slice = &s[..10]; // Panic! Index out of bounds
}
Performance Considerations
-
Zero-Cost Abstraction
- Slices have no runtime overhead
- Bounds checking is done at compile time when possible
-
Memory Efficiency
- Slices don't own data
- Multiple slices can reference the same data
- No memory allocation/deallocation
-
Cache Friendly
- Contiguous memory access
- Efficient iteration
Advanced Topics
1. Generic Slices
fn print_slice<T: std::fmt::Debug>(slice: &[T]) {
println!("{:?}", slice);
}
2. Custom Types with Slices
struct Buffer<'a> {
data: &'a [u8],
}
impl<'a> Buffer<'a> {
fn new(data: &'a [u8]) -> Buffer<'a> {
Buffer { data }
}
}
3. DST (Dynamically Sized Types)
// Slices are DSTs - they don't have a known size at compile time
fn process_slice(slice: &[u8]) {
println!("Slice length: {}", slice.len());
}
Next Steps
After mastering slices:
- Learn about string manipulation
- Study vector operations
- Explore custom collection types
- Understand lifetimes in depth
Remember: Slices are a fundamental building block in Rust for working with sequences of data safely and efficiently. They provide a way to reference a portion of a collection without taking ownership, making them ideal for functions that need to operate on a subset of data.