Reference contiguous sequences of data with Rust's slice types.
Slices let you reference a portion of a collection without taking ownership. They are one of Rust's most useful tools for writing flexible, efficient code.
By the end of this lesson, you'll understand slices, string slices (&str), array slices (&[T]), and why slice types make better function parameters than owned types.
A slice is a reference to a contiguous sequence of elements within a collection. It does not own the data it points to.
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
The hello and world variables are slices. They borrow part of the string s without taking ownership. When you see &s[start..end], you are creating a slice from index start up to (but not including) index end.
Rust provides shortcuts for common slice patterns:
let s = String::from("hello world");
// From the beginning
let hello = &s[..5]; // Same as &s[0..5]
// To the end
let world = &s[6..]; // Same as &s[6..11]
// The entire string
let whole = &s[..]; // Same as &s[0..11]
These shortcuts make your code cleaner. If you want everything from the start, omit the first number. If you want everything to the end, omit the second number.
The .. syntax creates a range. 0..5 includes 0, 1, 2, 3, 4 (not 5). If you want to include the end, use ..= instead: 0..=5 includes 0 through 5.
Consider a function that finds the first word in a string. Without slices, you might return an index:
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
This works, but there is a problem. The returned index has no connection to the string. If the string changes, the index becomes meaningless:
fn main() {
let mut s = String::from("hello world");
let word_end = first_word(&s); // Returns 5
s.clear(); // Empties the string
// word_end is still 5, but s is now empty!
// Using word_end would be a bug.
}
The compiler cannot catch this. The index and the string are separate values with no enforced relationship.
Slices are references. The borrow checker ensures they remain valid:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
Now the return value is tied to the input. Try to misuse it and the compiler stops you:
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // Immutable borrow
s.clear(); // ERROR! Mutable borrow while immutable exists
println!("the first word is: {word}");
}
The borrow checker prevents you from clearing s while word still references part of it. The bug that was invisible before is now a compile error.
String slices must occur at valid UTF-8 character boundaries. If you slice in the middle of a multi-byte character, your program will panic at runtime. For ASCII text this is never a problem, but be careful with Unicode.
Slices are sometimes called "fat pointers" because they store two pieces of data: a pointer to the first element and a length. A regular reference is just a pointer. A slice needs both the starting location and how many elements it spans.
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[1..4]; // [2, 3, 4]
The slice variable contains a pointer to the element 2 and the length 3. Together, these define the slice.
What is the type of a string slice in Rust?
String slices have a special type: &str. You have already seen this type without realizing it:
let s = "Hello, world!"; // Type is &str
String literals are slices. They are references to text stored in the compiled binary. This is why they are immutable and have the type &str, not String.
The difference between String and &str:
| Type | Ownership | Storage | Mutability |
|---|---|---|---|
String | Owned | Heap-allocated | Mutable |
&str | Borrowed | Points to String, literal, or binary | Immutable |
A String owns its data. A &str borrows it. Under the hood, str is a specialized [u8] (slice of bytes) with a guarantee that the bytes form valid UTF-8.
Consider this function signature:
fn process_text(s: &String) {
// ...
}
This works, but it limits what you can pass:
let owned = String::from("hello");
let literal = "world";
process_text(&owned); // Works
process_text(literal); // ERROR! literal is &str, not &String
Now compare this signature:
fn process_text(s: &str) {
// ...
}
This accepts both:
let owned = String::from("hello");
let literal = "world";
process_text(&owned); // Works! &String coerces to &str
process_text(literal); // Works! Already &str
Using &str as a parameter type makes your function more flexible. It accepts owned strings, string literals, and slices with no extra work.
When a function only needs to read a string, use &str as the parameter type. This is considered best practice in Rust. You will see this pattern throughout the standard library and ecosystem.
The same concept applies to arrays and vectors. A slice of integers has type &[i32]:
fn sum(numbers: &[i32]) -> i32 {
let mut total = 0;
for n in numbers {
total += n;
}
total
}
This function accepts arrays, vectors, and slices:
let vec = vec![1, 2, 3];
let arr = [4, 5, 6];
sum(&vec); // &Vec<i32> coerces to &[i32]
sum(&arr); // &[i32; 3] coerces to &[i32]
sum(&vec[0..2]); // Slice of first two elements
Just like &str for strings, &[T] is the idiomatic parameter type when you need to read a sequence of values.
You may have noticed that &String converts to &str and &Vec<T> converts to &[T] automatically. This is called deref coercion.
Rust performs deref coercion when a reference to one type can be converted to a reference to another type. The standard library implements Deref traits that enable these conversions:
&String coerces to &str&Vec<T> coerces to &[T]&Box<T> coerces to &TYou do not need to understand the implementation details. The important point is that you get this flexibility for free when you use slice types in function signatures.
By using &str and &[T] as parameter types, your functions automatically accept the widest variety of input types. Callers can pass owned values, references, or slices without any explicit conversion.
Here is a more realistic example. This function finds the longest word in a sentence:
fn longest_word(sentence: &str) -> &str {
let mut longest = "";
for word in sentence.split_whitespace() {
if word.len() > longest.len() {
longest = word;
}
}
longest
}
fn main() {
let text = String::from("The quick brown fox");
let result = longest_word(&text);
println!("Longest word: {result}"); // "quick"
}
The function takes &str and returns &str. It works with any string type. The returned slice borrows from the input, so the borrow checker ensures the input outlives the result.
Write a function signature that takes a string slice and returns a string slice. The function should be named first_word.
Slices are references to contiguous sequences within collections. They borrow data without taking ownership.
Key takeaways:
&str. Array slices have type &[T].&str instead of &String for string parameters.&[T] instead of &Vec<T> for slice parameters.&String and &Vec<T> automatically convert to slice types.When writing functions that read collections without modifying them, use slice types (&str, &[T]) as parameters. This makes your functions maximally flexible and idiomatic.
Slices complete our exploration of references and borrowing. You now understand ownership, borrowing, and slices. These three concepts form the foundation of Rust's memory safety guarantees. In the next lesson, we will use these concepts as we build custom data types with structs.