Slices & Dynamically Sized Types

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. A slice is a view into a contiguous sequence of elements that someone else owns — part of an array, a Vec, a String. Slices are the most common way to pass sequences to functions, because one &[T] parameter accepts arrays, vectors and sub-ranges of either. They are also the main example of a dynamically sized type.

Slice types

Type Meaning

[T]

a sequence of T whose length is only known at run time — the slice type itself, a DST

&[T]

a shared slice: read access to some elements

&mut [T]

a mutable slice: read and write access, but the length cannot change

str

a UTF-8 string slice, a DST (like [u8] with a UTF-8 guarantee)

&str

a borrowed string slice; string literals are &'static str

Box<[T]>, Rc<str>, Arc<[T]>

owned or shared slices behind a smart pointer

You create a slice by borrowing a range of an array, Vec or another slice:

fn average(ratings: &[u8]) -> f64 {
    if ratings.is_empty() {
        return 0.0;
    }
    let sum: u32 = ratings.iter().map(|&r| u32::from(r)).sum();
    f64::from(sum) / ratings.len() as f64
}

fn main() {
    let fixed = [5, 4, 3, 5];                 // array [u8; 4]
    let growable = vec![2, 3, 4, 5, 5, 1];    // Vec<u8>
    println!("{}", average(&fixed));          // &[u8; 4] coerces to &[u8]
    println!("{}", average(&growable));       // &Vec<u8> coerces to &[u8]
    println!("{}", average(&growable[1..4])); // elements 1, 2, 3
    println!("{}", average(&[]));             // empty slice
}

Range indexing and its panics

Indexing with a range produces a slice. The forms mirror range expressions:

Expression Elements

&v[a..b]

from index a up to but not including b

&v[a..=b]

from a through b inclusive

&v[a..]

from a to the end

&v[..b]

from the start up to b

&v[..]

the whole sequence

Out-of-range bounds, or a start after the end, panic at run time (range end index 9 out of range for slice of length 6). For input-dependent ranges use get, which returns an Option:

fn main() {
    let titles = ["Dune", "Emma", "Kindred", "Beloved", "Ulysses"];
    let page_size = 2;
    for page in 0..4 {
        let start = page * page_size;
        match titles.get(start..start + page_size) {
            Some(items) => println!("page {page}: {items:?}"),
            None => match titles.get(start..) {
                Some(rest) if !rest.is_empty() => println!("page {page}: {rest:?} (last)"),
                _ => println!("page {page}: empty"),
            },
        }
    }
}
// page 0: ["Dune", "Emma"]
// page 1: ["Kindred", "Beloved"]
// page 2: ["Ulysses"] (last)
// page 3: empty

String slices are indexed in bytes and the bounds must fall on UTF-8 character boundaries; &"é"[0..1] panics with "byte index 1 is not a char boundary". Use char_indices, str::get or is_char_boundary when slicing text that is not ASCII — see Strings & Text.

Fat pointers and dynamically sized types

Most types are Sized: their size is known at compile time. A dynamically sized type (DST) is one whose size is only known at run time. Rust has two families of DSTs: slices ([T] and str) and trait objects (dyn Trait). DSTs cannot be stored in a local variable or passed by value; they always live behind a pointer, and that pointer is fat — two words wide — carrying the extra information needed to use the value:

Diagram of a slice as a fat pointer. A Vec v on the stack holds ptr, capacity 6 and length 6 and points to a heap buffer containing 10, 20, 30, 40, 50 and 60. The slice s = &v[1..4] on the stack is a 16-byte fat pointer holding a pointer to the element at index 1 and length 3, covering 20, 30 and 40. While s is alive, v is borrowed and v.push would be rejected because it could reallocate the buffer.
fn main() {
    println!("&u8        {}", size_of::<&u8>());          // 8
    println!("&[u8]      {}", size_of::<&[u8]>());        // 16: pointer + length
    println!("&str       {}", size_of::<&str>());         // 16
    println!("&[u8; 100] {}", size_of::<&[u8; 100]>());   // 8: the length is in the type
    println!("Box<[u8]>  {}", size_of::<Box<[u8]>>());    // 16
    println!("Vec<u8>    {}", size_of::<Vec<u8>>());      // 24: pointer + capacity + length
}

Sized and ?Sized

Every generic type parameter has an implicit Sized bound, because the compiler must know how much stack space a T needs. To accept DSTs as well, relax the bound with ?Sized ("maybe sized") and use T only behind a pointer:

use std::fmt::Display;

fn describe<T: Display + ?Sized>(value: &T) -> String {
    format!("<{value}>")
}

fn main() {
    println!("{}", describe("a str"));          // T = str, a DST
    println!("{}", describe(&42));              // T = i32
    let boxed: Box<dyn Display> = Box::new(1.5);
    println!("{}", describe(boxed.as_ref()));   // T = dyn Display, also a DST
}

Without ?Sized, describe("a str") would fail with E0277 ("the size for values of type str cannot be known at compilation time"). The standard library uses ?Sized widely — Box<T: ?Sized>, AsRef<T: ?Sized>, Borrow<T: ?Sized> — which is why Box<str> and Rc<[T]> work.

Mutable slices

A &mut [T] allows modifying elements in place, swapping, sorting and filling, but not changing the length (that needs the owning Vec). Functions that only need to rearrange or update elements should take &mut [T], which accepts arrays and vectors alike:

fn normalise(ratings: &mut [u8]) {
    for r in ratings.iter_mut() {
        *r = (*r).clamp(1, 5);
    }
}

fn main() {
    let mut a = [0, 3, 9];
    let mut v = vec![7, 1, 2, 6];
    normalise(&mut a);
    normalise(&mut v[1..]);             // only part of the vector
    v.swap(0, 3);
    v[..2].fill(0);
    println!("{a:?} {v:?}");            // [1, 3, 5] [0, 0, 2, 7]
}

Slice methods

Slices carry most of the sequence API; Vec and arrays get these methods through deref coercion. The most useful groups:

Group Methods

Inspecting

len, is_empty, first, last, get, contains, starts_with, ends_with, iter, iter_mut

Chunking

chunks(n), chunks_exact(n), rchunks(n), windows(n), array_windows::<N>() (1.94), as_chunks::<N>() (1.88), chunk_by(pred) (1.77)

Splitting

split_at(i), split_first, split_last, split(pred), splitn, split_inclusive, and the _mut variants

Searching

binary_search, binary_search_by_key, partition_point, iter().position(..)

Sorting

sort, sort_by, sort_by_key, sort_by_cached_key (stable), sort_unstable* (faster, not stable), select_nth_unstable, is_sorted (1.82), reverse

Rearranging

swap, rotate_left, fill, fill_with, copy_from_slice, clone_from_slice, dedup (on Vec)

Converting

to_vec, concat, join, repeat, into_vec (for Box<[T]>)

fn main() {
    let ratings = [3, 5, 4, 4, 2, 5, 5];

    for chunk in ratings.chunks(3) {
        print!("{chunk:?} ");                 // [3, 5, 4] [4, 2, 5] [5]
    }
    println!();

    let rising = ratings.windows(2).filter(|w| w[1] > w[0]).count();
    println!("rising steps: {rising}");       // 2

    // array_windows (1.94) yields fixed-size arrays, so patterns work:
    let peaks = ratings.array_windows().filter(|[a, b, c]| b > a && b > c).count();
    println!("peaks: {peaks}");               // 1 (the 5 at index 1)

    let mut sorted = ratings;
    sorted.sort_unstable();
    println!("{sorted:?}");                   // [2, 3, 4, 4, 5, 5, 5]
    println!("{:?}", sorted.binary_search(&4)); // Ok(2) or Ok(3)
    println!("{:?}", sorted.binary_search(&1)); // Err(0): where it would be inserted
    let at_least_4 = sorted.len() - sorted.partition_point(|&r| r < 4);
    println!("{at_least_4} ratings >= 4");    // 5

    let line = "Dune;Emma;;Kindred";
    let parts: Vec<&str> = line.split(';').filter(|p| !p.is_empty()).collect();
    println!("{parts:?}");                    // ["Dune", "Emma", "Kindred"]
}

Choosing a sort: sort and sort_by_key are stable (equal elements keep their order) and allocate a buffer; the sort_unstable family is in-place and usually faster. Since Rust 1.81, both detect inconsistent comparison functions (a cmp that does not implement a total order) and may panic rather than produce a garbage order. To sort floats, use sort_by(f64::total_cmp).

Arrays, slices and Vec

The three sequence types convert easily:

fn main() {
    let array: [u16; 3] = [1965, 1815, 1987];
    let slice: &[u16] = &array;                 // array -> slice (unsizing coercion)
    let vec: Vec<u16> = slice.to_vec();         // slice -> Vec (clones the elements)
    let back: &[u16] = &vec;                    // Vec -> slice (deref coercion), also vec.as_slice()
    let boxed: Box<[u16]> = vec.clone().into_boxed_slice(); // Vec -> Box<[T]> (drops spare capacity)

    let exact: [u16; 3] = back.try_into().expect("exactly 3 elements"); // slice -> array (checked)
    let first_two: &[u16; 2] = back[..2].try_into().unwrap();           // slice -> array reference
    let from_array: Vec<u16> = Vec::from(array);                        // array -> Vec

    println!("{slice:?} {vec:?} {boxed:?} {exact:?} {first_two:?} {from_array:?}");
}

As a parameter type, prefer &[T] over &Vec<T> and &str over &String: the slice accepts more callers and costs nothing (Clippy’s ptr_arg lint flags &Vec<T> parameters). Return a Vec<T> or String when the function creates new data.

Common errors and pitfalls

Problem Fix

panic: range end index ... out of range

use get(range) for input-dependent ranges

panic: byte index ... is not a char boundary

slice strings at boundaries from char_indices or find

E0277 size of [T]/str not known

put the DST behind a reference or Box, or add ?Sized to the generic bound

E0502 when pushing while holding a slice

the slice borrows the Vec; finish with the slice, or store indices instead

TryFromSliceError when converting to an array

the slice length does not match N