6.5 Slices: Borrowing Contiguous Data
Beyond references to entire values, Rust provides slices, which are references to a contiguous sequence of elements within a collection, rather than the whole collection. Slices provide a non-owning view (a borrow) into data owned by something else (like a String
, Vec<T>
, array, or even another slice). They are crucial for writing efficient code that accesses portions of data without needing to copy it or take ownership.
Internally, a slice is typically a fat pointer, storing two pieces of information:
- A pointer to the start of the sequence segment.
- The length of the sequence segment.
Because slices borrow data, they strictly adhere to Rust’s borrowing rules: you can have multiple immutable slices of the same data, or exactly one mutable slice, but not both at the same time if they could overlap.
6.5.1 Immutable and Mutable Slices
There are two primary kinds of slices, mirroring the two kinds of references:
- Immutable Slice (
&[T]
): Provides read-only access to a sequence of elements of typeT
. - Mutable Slice (
&mut [T]
): Provides read-write access to a sequence of elements of typeT
.
The type T
represents the element type (e.g., i32
, u8
).
6.5.2 Array Slices
Slices are commonly used with arrays (fixed-size lists on the stack) and vectors (growable lists on the heap).
fn main() { let numbers: [i32; 5] = [10, 20, 30, 40, 50]; // An array // Create immutable slices using range syntax let all: &[i32] = &numbers[..]; // Slice of the whole array let first_two: &[i32] = &numbers[0..2]; // Slice of elements 0 and 1 ([10, 20]) let last_three: &[i32] = &numbers[2..]; // Slice of elements 2, 3, 4 ([30, 40, 50]) println!("All: {:?}", all); println!("First two: {:?}", first_two); println!("Last three: {:?}", last_three); // Create a mutable slice (requires the owner to be mutable) let mut mutable_numbers = [1, 2, 3]; let mutable_slice: &mut [i32] = &mut mutable_numbers[1..]; // Slice of elements 1 and 2 // Index access refers to the slice itself: index 0 of the slice is index 1 of the array. mutable_slice[0] = 99; // mutable_numbers is now [1, 99, 3] println!("Modified numbers: {:?}", mutable_numbers); }
Note: The ..
range syntax creates slices: ..
is the whole range, start..end
includes start
but excludes end
, start..
goes from start
to the end, and ..end
goes from the beginning up to (excluding) end
. This syntax works on arrays, vectors, and existing slices.
6.5.3 String Slices (&str
)
A string slice, written &str
, is a specific type of immutable slice that always refers to a sequence of valid UTF-8 encoded bytes. It’s the most primitive string type in Rust. You can create string slices by borrowing from String
s, other string slices, or string literals using range syntax with byte indices.
fn main() { let s_ascii: String = String::from("hello world"); // ASCII string // Slicing ASCII text is straightforward as byte indices match character boundaries let hello: &str = &s_ascii[0..5]; // Slice referencing "hello" let world: &str = &s_ascii[6..11]; // Slice referencing "world" println!("Slice 1: {}", hello); println!("Slice 2: {}", world); // With multi-byte UTF-8 characters, indices must respect character boundaries let s_utf8 = String::from("你好"); // "Nǐ hǎo" - 6 bytes total, each char is 3 bytes // let invalid_slice = &s_utf8[0..1]; // PANIC! 1 is not a character boundary. // let invalid_slice = &s_utf8[0..2]; // PANIC! 2 is not a character boundary. let first_char: &str = &s_utf8[0..3]; // OK: Slice referencing the first character "你" let second_char: &str = &s_utf8[3..6]; // OK: Slice referencing the second character "好" println!("First char: {}", first_char); println!("Second char: {}", second_char); }
Because &str
must always point to valid UTF-8 sequences, creating string slices using byte indices ([start..end]
) has an important restriction: the start
and end
indices must fall on valid UTF-8 character boundaries. Attempting to create a slice where an index lies in the middle of a multi-byte character sequence is a runtime error and will cause your program to panic (a controlled crash indicating a program bug).
For the simpler examples in this chapter introducing slices, we often use ASCII text where each character is conveniently one byte long, making byte indices align with character boundaries. When working with text that may contain multi-byte characters, slicing using direct byte indices requires careful validation; often, iterating over characters or using methods designed for UTF-8 processing is a safer approach than direct byte-index slicing. Operations that could break the UTF-8 invariant (like arbitrary byte mutation within a &mut str
) are also carefully controlled, as discussed later.
6.5.4 String Literals
Now we can understand string literals (e.g., "hello"
). They are essentially string slices (&str
) whose data is stored directly in the program’s compiled binary and is therefore valid for the entire program’s execution. Their type is &'static str
, where 'static
is a special lifetime indicating validity for the whole program runtime.
fn main() { let literal_slice: &'static str = "I am stored in the binary"; println!("{}", literal_slice); }
6.5.5 Slices in Functions
One of the most common uses for slices is in function arguments. Accepting a slice (&[T]
or &str
) instead of an owned type (like Vec<T>
or String
) makes a function more flexible and efficient, as it can operate on different kinds of data sources without taking ownership or requiring data copying.
// Function accepting an array/vector slice fn sum_slice(slice: &[i32]) -> i32 { let mut total = 0; for &item in slice { // Iterate over elements in the slice total += item; } total } // Function accepting a string slice fn first_word(text: &str) -> &str { // Iterate over bytes, find first space for (i, &byte) in text.as_bytes().iter().enumerate() { if byte == b' ' { return &text[0..i]; // Return slice up to space } } &text[..] // No space found, return whole slice } fn main() { // Array slice example let numbers = [1, 2, 3, 4, 5]; // Can pass reference to array directly (coerces to slice) println!("Sum of numbers: {}", sum_slice(&numbers)); // Or pass explicit slice println!("Sum of part: {}", sum_slice(&numbers[1..4])); // String slice example let sentence = String::from("hello wonderful world"); println!("First word: {}", first_word(&sentence)); // Pass slice of String let literal = "goodbye"; println!("First word: {}", first_word(literal)); // Pass a string literal directly }
Note: Due to automatic deref coercions (discussed later), functions expecting &[T]
can often directly accept references to arrays (&[T; N]
) or Vec<T>
s. Similarly, functions expecting &str
can accept &String
.
6.5.6 Mutable Slices (&mut [T]
and &mut str
)
Mutable slices (&mut [T]
) allow modification of the elements within the borrowed sequence:
fn main() { let mut data = [10, 20, 30]; let slice: &mut [i32] = &mut data[..]; slice[0] = 15; slice[1] *= 2; println!("Modified data: {:?}", data); // Prints: [15, 40, 30] }
Mutable string slices (&mut str
) exist but are more restricted. Because a &str
(and &mut str
) must always contain valid UTF-8, arbitrary byte modifications are disallowed. Furthermore, the length of a string slice cannot be changed, as this would require modifying the owner (e.g., reallocating a String
), which a borrow cannot do. This prevents simple appending operations directly on a &mut str
.
Mutable string slices are primarily useful for in-place modifications that preserve UTF-8 validity and length, such as changing case via methods like make_ascii_uppercase()
. For operations that need to change string length or might temporarily invalidate UTF-8, working directly with an owned String
or a mutable byte slice (&mut [u8]
) is necessary.
fn main() { let mut s = String::from("hello"); { // Limit scope of mutable borrow let slice: &mut str = &mut s[..]; slice.make_ascii_uppercase(); // In-place modification allowed } // Mutable borrow ends here println!("Uppercase: {}", s); // Prints: HELLO }
Remember that all slice operations must respect the borrowing rules – particularly the exclusivity of mutable borrows for potentially overlapping data.