What is a slice?
It’s just a reference, but of course that is being over simplistic.
Let’s delve deeper into what a slice type is.
According to the official Rust documentation and cookbook,
A slice can be defined as:
Slices let you reference a contiguous sequence of elements in a collection. A slice is a kind of reference, so it does not have ownership.
One notable word to really understand what a slice is the word collection. So what is a collection?
According to the standard library documentation:
In simple terms, it is a well-articulated list of most-used heap-based data structures. Collections are of different variants and groups but they all have one thing in common, and what is that?
All collections store data on the heap, allowing them to be easily extensible and making compile-time size allocation easy as collection types only keep key information on the stack, like size.
Now we have that out of the way, you can easily define a slice type. If you asked me, I would say it is simply a ranged reference to contiguous data. Because slices themselves are stored as a pointer and length (on the stack if local), the core data itself could be an array (on the stack), a Vec
(on the heap), a string, or other contiguous memory.
Why and when should we use a slice?
Now we know what it is, let’s explore when to use and why we could need a slice.
Slices are mostly about viewing data without taking ownership. You don’t always need to copy or move things around just to work with them. Instead, a slice gives you a window into the data, so you can read or mutate part of it while keeping the original owner intact.
They shine in scenarios like:
Working with substrings or sub-arrays: Instead of creating a new string or array for a subset, you just borrow a slice of what you need.
Avoiding unnecessary copies: Since slices are lightweight references, you don’t pay extra performance costs for moving or cloning data.
Function parameters: Instead of writing separate functions for
String
and&str
, orVec<T>
and&[T]
, you can just take a slice parameter and handle both cases with the same code.Iteration with safety: Slices enforce boundaries — you won’t run past the edge of your data. This is both memory-safe and avoids panics when used with safe methods.
So in short, use slices whenever you only need to look at or operate on part of a data structure without owning or duplicating it. They’re a way of being precise with memory and scope while still staying flexible in how your functions and APIs accept input.
Slice of Strings.
Since strings are a good example of contiguous memory , lets take it as an example of how to make a slice of a string and with this example you can learn the basics of working with slices ; we would of course then go into deeper categories like making slices of other collection groups.
Let’s look at this example. In this example, we would create a string, take a slice of the string and pass it to a macro called println
! to print it to the standard output.
fn main() {
let s1 : String = String::from("Hello world!");
let s1slice = &s1[..5];
println!("{s1slice}");
}
In the above example you can see we can create a slice of a string by simply using the & ref and passing in a range, so in code a slice looks more like a reference with a range.
Safety with Slices
Indexing and panics: Even though our previous example is totally ok , it is prone to runtime errors, why? If the given range is bigger than the size of the collection in respect, there would be a runtime error.
In order to fix this, we can ensure we supply correct ranges or simply use helper methods that help avoid this entirely, as they would return an option
type of Some(slice) or None when the range is too big, or any other possible issues arise.
Here is an example of our previous function, but with a safer alternative using the .get
method.
fn main() {
let s1 : String = String::from("Hello world!");
let s1slice = s1.get(0..5).unwrap_or("Hello");
// I use unwrap_or as a fall back, incases of a none.
println!("{s1slice}");
}
The way this works may differ slightly across collections, but the principle is the same: direct indexing can panic, so prefer safe methods when possible.
Note: Since
String
is essentially aVec<u8>
with UTF-8 rules, slicing at a non-UTF-8 boundary also causes a panic.
Mutability Rules
Yes, you can have mutable slices. As mentioned earlier, slices are references with a range, so you can also take a mutable slice (&mut [T]
or &mut str).
However, the borrow checker applies the usual rules: you can have either one mutable slice or many immutable slices at the same time, but not both. This ensures safety when modifying data through slices.
Here is an example showing this:
fn main() {
let mut numbers = [1, 2, 3, 4, 5];
let slice = &mut numbers[1..4]; // mutable slice of part of the array
for n in slice.iter_mut() {
*n *= 2;
}
println!("{:?}", numbers); // [1, 4, 6, 8, 5]
}
Lifetime & Ownership
Even though slices reference owned data, they themselves do not own it. A slice is just a fat pointer (a reference plus a length). For example, &[u8]
means “a reference to a sequence of u8s
.”
The underlying elements are not duplicated — the slice just points to them.
This means:
If you slice a vector of values, you get a slice of those values.
If you slice a vector of references, you get a slice of references.
Let’s look at some examples that expose this:
fn main() {
let v1: Vec<i32> = vec![1, 2, 3, 4];
let v2: Vec<&i32> = vec![&1, &2, &3, &4];
let slicev1: Option<&[i32]> = v1.get(1..3);
let slicev2: Option<&[&i32]> = v2.get(1..3);
println!("{:?}", slicev1); // Some([2, 3])
println!("{:?}", slicev2); // Some([&2, &3])
}
Notice the type signatures:
slicev1
has typeOption<&[i32]>
, meaning a slice ofi32s
.slicev2
has typeOption<&[&i32]>
, meaning a slice of references toi32
s.
This shows that slices preserve the nature of what they point to — values stay values, and references stay references.
So the core ownership rules for references are still in effect, and just like ownership rules lifetime rules are stay the same — slices cannot live longer than their owners the compiler enforces this.
Borrow Checker
Just like other points related to references, you can’t bend the borrow checking rules around slices either.
The same restrictions apply: you can have either many immutable borrows or one mutable borrow at a time, but never both. Attempting to break this rule results in a compiler error, not a runtime panic.
For example, if you try to mutate a slice while still holding an immutable reference to it, the borrow checker will reject it at compile time.
Likewise, if you attempt to create two overlapping mutable slices of the same data, Rust prevents it.
These rules ensure that slices are always safe to use without data races or undefined behavior.
Safe Iteration
Instead of direct indexing of slices, you might want to use iterator methods such as .iter()
and .iter_mut()
when you want to read or modify elements in sequence. Iterators avoid the risk of indexing mistakes and make the code more expressive. In cases where you need ownership of the elements, .into_iter()
is also available.
Additionally, when working with ranges, the .get()
method is a good choice because it returns an Option
and avoids panics if the range is invalid. This is especially useful for dynamic scenarios where you cannot guarantee the bounds at compile time.
Limitations
As seen so far, slices and their usages are tied to contiguous memory data structures such as arrays, vectors, and strings. They provide a safe view into a block of sequential elements. However, not all collections in Rust are contiguous — for example, HashMap
, HashSet
, and LinkedList
are not laid out sequentially in memory.
For these collections, you cannot take a slice directly. Instead, you rely on their own iterator methods or conversion functions to access data safely. If you require slice-like functionality, you would often need to first collect the elements into a contiguous container like a Vec
.
Next Steps
In the next section, we will show some examples of getting slices out of popular collections, highlighting how they behave with vectors, arrays, and strings.
The Four Groups of Collections and How to Take Slices of Them
Rust’s collections can be grouped into four major categories. Out of these, only the contiguous-memory collections support slicing directly. The others provide access through iteration or conversion. Let’s walk through them one by one.
1. Sequence Collections
These are contiguous-memory collections such as arrays, vectors, and strings. They support slicing directly.
fn main() {
// Array slice
let arr = [10, 20, 30, 40, 50];
let arr_slice: &[i32] = &arr[1..4];
println!("{:?}", arr_slice); // [20, 30, 40]
// Vector slice
let v = vec![1, 2, 3, 4, 5];
let v_slice: &[i32] = &v[0..3];
println!("{:?}", v_slice); // [1, 2, 3]
// String slice
let s = String::from("Hello, world!");
let s_slice: &str = &s[0..5];
println!("{}", s_slice); // "Hello"
}
2. Map Collections
Examples: HashMap<K, V>
, BTreeMap<K, V>
.
These are not contiguous, so you cannot slice them directly. Instead, you use iterators or collect into a Vec
to slice.
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert("a", 1);
map.insert("b", 2);
map.insert("c", 3);
// Collect into a Vec for slicing
let mut entries: Vec<_> = map.iter().collect();
entries.sort_by_key(|(k, _)| *k); // ensure order before slicing
let slice = &entries[0..2];
println!("{:?}", slice); // e.g. [("a", 1), ("b", 2)]
}
3. Set Collections
Examples: HashSet<T>
, BTreeSet<T>
.
Like maps, sets are not contiguous, but you can convert them to a Vec
and then slice.
use std::collections::HashSet;
fn main() {
let set: HashSet<i32> = [1, 2, 3, 4, 5].iter().cloned().collect();
// Collect into Vec for slicing
let mut values: Vec<_> = set.iter().collect();
values.sort(); // ensure stable order
let slice = &values[1..4];
println!("{:?}", slice);
}
4. Linked Collections
Examples: LinkedList<T>
.
Linked structures are inherently non-contiguous, so slicing is not supported. Instead, you rely on iteration or manual traversal.
use std::collections::LinkedList;
fn main() {
let mut list: LinkedList<i32> = LinkedList::new();
list.push_back(10);
list.push_back(20);
list.push_back(30);
// Collect into Vec for slicing
let values: Vec<_> = list.iter().collect();
let slice = &values[0..2];
println!("{:?}", slice); // [&10, &20]
}
Summary
Sequences (arrays, Vec, String): Direct slicing supported.
Maps, Sets, Linked structures: No direct slicing. Must iterate or collect into a
Vec
first, then slice.
As you can see, this shows how slicing is tightly connected to contiguous memory layouts, which is why only certain groups of collections support it directly — in other words, you would have to convert those data types first before slicing.
Now you know when to use slices and how to use them, and what they really are; feel free to make those modifications to your algorithms and function signatures.
If you have any questions , let me know if you have any questions.
Source: Read MoreÂ