An slice is a reference to a contiguous part of a collection or string.
We already know that Ownership controls who cleans up memory and Borrowing allows us to lend data. Sometimes we don’t want to lend the entire object, but only a part of it.
Imagine you have a huge book (String).
- If you pass the book by value (Move), you give it away.
- If you pass a reference to the book (
&String), you let them read the whole thing. - What if you only want them to read chapter 3?
One option would be to copy the text of chapter 3 into a new variable, but that duplicates memory. In Rust, we can use slices to create a borrowed view.
A slice is a reference to a contiguous sequence of elements rather than to the entire collection. It’s like a “window” over the original data: it doesn’t own the elements and stores a pointer along with a length.
String slices (&str)
The most common use of slices is with text strings. You’ve probably noticed that sometimes we use String and other times &str. Let’s finally understand the difference.
Suppose we have a String on the Heap:
fn main() {
let s = String::from("Hello World");
let hello = &s[0..5]; // Slice of indices 0, 1, 2, 3, 4
let world = &s[6..11]; // Slice of indices 6 to 10
println!("First part: {}", hello);
}The syntax [start..end] creates a range.
hellois a&strslice.- Internally,
hellocontains two pieces of data on the Stack:
- A pointer to the byte where the ‘H’ starts.
- A length (5 bytes).
We haven’t copied “Hello”. The slice still points to the bytes that belong to s, so the compiler prevents incompatible modification of the original string while we use that reference.
Syntax shortcuts Rust is smart with ranges:
[0..2]is the same as[..2](from the beginning).[3..len]is the same as[3..](to the end).[..]is a slice of the entire content.
str literals
Here comes an important revelation. When you write this:
let s = "Hello world";What type is s? It’s not a String. It’s a &str.
It’s a &'static str: a reference to UTF-8 bytes included in the program’s static data and available throughout its execution. That’s why a string literal is immutable and doesn’t need dynamic allocation.
String vs &str
This is one of the most common questions.
String: It owns a buffer on the Heap. It can grow, shrink, and be modified, and allocating memory has a cost.&str: It’s a view (Slice). It’s lightweight (just pointer + length). It doesn’t own the data. This is usually what you want to pass as an argument to your functions.
Practical guideline:
If you write a function that only needs to read a text, use &str as the parameter, not &String.
// ❌ Restrictive: only accepts Strings from the Heap
fn greet(name: &String) { ... }
// ✅ Flexible: accepts String, parts of String, and literals
fn greet(name: &str) { ... }Other slices (&[T])
Slices are not exclusive to text strings. They work with contiguous sequences, like arrays and vectors.
Imagine you have an array of 5 numbers and you want to pass only the 3 middle ones to a function.
fn main() {
let a = [10, 20, 30, 40, 50];
// Type: &[i32]
let slice = &a[1..4];
assert_eq!(slice, &[20, 30, 40]);
}The type is denoted as &[T], where T is the data type (in this case &[i32]).
This is tremendously powerful for writing efficient APIs. If your function accepts &[i32], you can call it by passing:
- A reference to an entire array (
&[1, 2]). - A reference to a vector (
&vec). - A partial slice of either.
Slices and the Borrow Checker
Since slices are references, they are subject to the Borrowing rules we saw in the previous article. And this is where Rust saves you from serious memory errors.
Imagine this scenario (common in C++):
- You have a Vector.
- You create a pointer to the first element.
- You destroy the vector or force a reallocation of its buffer.
- You try to read the pointer.
- Result in C++: Use After Free (Crash or vulnerability).
Let’s see what happens in Rust:
fn main() {
let mut s = String::from("hello world");
let word = &s[..5]; // Immutable borrow (slice)
s.clear(); // ❌ COMPILATION ERROR
println!("The word is: {}", word);
}The error will be: “cannot borrow s as mutable because it is also borrowed as immutable”.
wordborrowss(immutably).s.clear()attempts to borrows(mutably) to clear it.- Rust says: “Stop!”. As long as
wordis alive (used in theprintln), you cannot modifys.wordwould stop pointing to valid data.
The borrowing system guarantees that a slice of safe code won’t end up pointing to freed or invalidated memory.
Beware of UTF-8
A final warning specific to Strings. Rust uses UTF-8 by default. Some characters (like emojis or accented letters) take up more than 1 byte.
If you try to make a slice that cuts a character in half, Rust will panic at runtime.
let s = "Mañana"; // The 'ñ' occupies 2 bytes
// let slice = &s[0..3]; // 💀 Danger: You could be splitting the ñFortunately, it’s rare to have to manually manipulate string indices in Rust. We usually use iterators or methods that handle UTF-8 correctly.