Slices in Rust: &[T] vs &Vec<T> & Indexing Guide

Max WellsMax WellsFounder of Rustify

TL;DR: A slice (&[T]) is a view into a contiguous block of memory, it holds a pointer and a length, but doesn't own the data. You get a slice by borrowing a portion of a Vec<T>, an array [T; N], or any contiguous collection. &str is a string slice, a &[u8] that's guaranteed to be valid UTF-8. Slices are one of Rust's most important types: functions should accept &[T] instead of &Vec<T> whenever possible.


What Is a Slice?

A slice &[T] is a fat pointer: a pointer to the first element plus a length. It borrows a view into some contiguous data without owning it.

fn main() {
    let v = vec![1, 2, 3, 4, 5];
 
    let all: &[i32] = &v;          // slice of the entire Vec
    let part: &[i32] = &v[1..4];   // slice of elements 1, 2, 3
 
    println!("{:?}", all);   // [1, 2, 3, 4, 5]
    println!("{:?}", part);  // [2, 3, 4]
    println!("len = {}", part.len()); // 3
}

The slice part borrows from v, it becomes invalid when v is dropped. No data is copied.


How Do You Slice Different Types?

Arrays, Vec, and String all deref to slices, any type that stores contiguous elements can be sliced.

// From an array
let arr = [10, 20, 30, 40];
let slice: &[i32] = &arr[1..3]; // [20, 30]
 
// From a Vec
let v = vec!["a", "b", "c"];
let slice: &[&str] = &v[..2];   // ["a", "b"]
 
// String slice (&str)
let s = String::from("hello world");
let word: &str = &s[0..5];      // "hello"
let s_ref: &str = &s;           // entire string as &str
 
// Arrays coerce to slices automatically
fn sum(nums: &[i32]) -> i32 { nums.iter().sum() }
sum(&arr);      // [i32; 4] coerces to &[i32]
sum(&v[..]);    // Vec<i32> coerces to &[i32]

Why Accept &[T] Instead of &Vec<T>?

Functions that accept &[T] work with any contiguous data source: Vec, arrays, and other slices. &Vec<T> forces the caller to have a Vec specifically.

// BAD; only works with Vec<i32>
fn print_nums(v: &Vec<i32>) {
    for n in v { print!("{n} "); }
}
 
// GOOD; works with Vec, arrays, or any slice
fn print_nums(v: &[i32]) {
    for n in v { print!("{n} "); }
}
 
fn main() {
    let v = vec![1, 2, 3];
    let a = [4, 5, 6];
 
    print_nums(&v);    // ✅ Vec coerces to &[i32]
    print_nums(&a);    // ✅ array coerces to &[i32]
    print_nums(&v[1..]); // ✅ partial slice
}

The same rule applies to strings: prefer &str over &String in function parameters.


How Does Slice Indexing Work?

Indexing a slice with a single index returns a reference to the element. Indexing with a range returns a sub-slice. Out-of-bounds indexing panics.

let v = vec![10, 20, 30, 40, 50];
let s: &[i32] = &v;
 
println!("{}", s[0]);       // 10; returns &i32, but auto-deref prints value
println!("{:?}", &s[1..3]); // [20, 30]
println!("{:?}", &s[..2]);  // [10, 20]; from start
println!("{:?}", &s[3..]);  // [40, 50]; to end
 
// Safe indexing without panicking
match s.get(10) {
    Some(val) => println!("{val}"),
    None => println!("out of bounds"),
}

Use .get(i) when the index might be out of bounds, it returns Option<&T> instead of panicking.


What Are Mutable Slices?

&mut [T] gives mutable access to the elements of a slice, you can modify the values but not the length or allocation.

fn double_all(slice: &mut [i32]) {
    for x in slice.iter_mut() {
        *x *= 2;
    }
}
 
fn main() {
    let mut v = vec![1, 2, 3, 4];
    double_all(&mut v);
    println!("{:?}", v); // [2, 4, 6, 8]
 
    // Split a mutable slice; get two non-overlapping mutable slices
    let (left, right) = v.split_at_mut(2);
    left[0] = 99;
    right[0] = 100;
    println!("{:?}", v); // [99, 4, 100, 8]
}

Frequently Asked Questions

[T] is a dynamically-sized type (DST): you can't create a value of type [T] directly because its size isn't known at compile time. &[T] is a fat pointer (pointer + length) to a [T], this has a known size and is how you actually use slices.

&str is a slice of bytes (&[u8]) that is guaranteed to be valid UTF-8. Rust's type system enforces this: you need unsafe to create a &str from raw bytes that haven't been validated. For arbitrary bytes, use &[u8].

Yes, &[] is a valid empty slice. slice.is_empty() checks for this. Empty slices are perfectly safe and have zero length.

Vec<T> implements Deref<Target = [T]>, so &Vec<T> can automatically coerce to &[T] where needed. This deref coercion is why you can pass a &Vec to a function expecting &[T].


Sources


  • Vec: Vec<T> derefs to &[T], the owned counterpart to slices
  • String: String derefs to &str, the owned counterpart to string slices
  • Ownership: Slices are borrowed views, they don't own the data they point to
  • Lifetime: A slice's lifetime is tied to the collection it borrows from
  • Iterator: slice.iter() produces an iterator over &T references

Keep Reading

Ready to Land a $80-120k Rust Job?