ZetCode

Rust slices

last modified October 25, 2026

In this article we show how to work with slices in Rust.

A slice is a view into a contiguous sequence of elements owned by something else: an array, a vector, or a string. A slice does not own its data; it borrows it. The slice type is written [T], and since its length is not known at compile time, it is almost always used behind a reference: &[T] for a shared slice and &mut [T] for a mutable one. A string slice has its own type, &str.

A slice reference is a fat pointer: it stores a pointer to the first element and the number of elements. Slices are the common currency of Rust APIs. Functions that only need to read a sequence take &[T] or &str and thus work with arrays, vectors, and strings alike. See also Rust arrays and Rust vectors.

The examples were tested with Rust 1.97 and the 2024 edition. They are run as Cargo projects with cargo run -q.

Creating slices with ranges

A slice is created by indexing a collection with a range.

main.rs
fn main() {
    let vals = [10, 20, 30, 40, 50, 60];

    let a = &vals[1..4];
    let b = &vals[..2];
    let c = &vals[3..];
    let d = &vals[..];
    let e = &vals[2..=4];
    let f = &vals[3..3];

    println!("[1..4]  {a:?}");
    println!("[..2]   {b:?}");
    println!("[3..]   {c:?}");
    println!("[..]    {d:?}");
    println!("[2..=4] {e:?}");
    println!("[3..3]  {f:?}");

    println!("a.len() = {}, a[0] = {}", a.len(), a[0]);
}

We create several slices of an array.

let a = &vals[1..4];

The range 1..4 selects the elements at indexes 1, 2, and 3. The start is included and the end is excluded. The resulting slice has three elements.

let b = &vals[..2];
let c = &vals[3..];
let d = &vals[..];

Either bound can be omitted. Without a start, the slice begins at index 0; without an end, it goes to the last element. The full range .. covers the whole array.

let e = &vals[2..=4];
let f = &vals[3..3];

The ..= range includes its end. A range with equal bounds gives an empty slice.

println!("a.len() = {}, a[0] = {}", a.len(), a[0]);

Slices are indexed from zero, relative to their own start. The first element of a is the second element of the array.

$ cargo run -q
[1..4]  [20, 30, 40]
[..2]   [10, 20]
[3..]   [40, 50, 60]
[..]    [10, 20, 30, 40, 50, 60]
[2..=4] [30, 40, 50]
[3..3]  []
a.len() = 3, a[0] = 20

Slices as function parameters

A function taking &[T] accepts arrays, vectors, and parts of them.

main.rs
fn average(vals: &[f64]) -> Option<f64> {
    if vals.is_empty() {
        return None;
    }
    Some(vals.iter().sum::<f64>() / vals.len() as f64)
}

fn main() {
    let arr = [1.5, 2.5, 3.0, 4.0];
    let vec = vec![10.0, 20.0, 30.0];

    println!("{:?}", average(&arr));
    println!("{:?}", average(&vec));
    println!("{:?}", average(&vec[1..]));
    println!("{:?}", average(&[]));
}

The average function computes the mean of a slice of floats.

fn average(vals: &[f64]) -> Option<f64> {

The function returns None for an empty slice, since the mean of no values is undefined.

println!("{:?}", average(&arr));
println!("{:?}", average(&vec));
println!("{:?}", average(&vec[1..]));
println!("{:?}", average(&[]));

A reference to an array coerces to a slice, and so does a reference to a vector, through deref coercion. Had we declared the parameter as &Vec<f64>, the function could not be called with an array or with a part of a vector.

$ cargo run -q
Some(2.75)
Some(20.0)
Some(25.0)
None

Mutable slices

A mutable slice lets us modify a part of a collection in place.

main.rs
fn double_all(vals: &mut [i32]) {
    for v in vals.iter_mut() {
        *v *= 2;
    }
}

fn main() {
    let mut vals = [5, 1, 4, 2, 3, 9, 8, 7];

    double_all(&mut vals[..3]);
    println!("{vals:?}");

    vals[3..].sort();
    println!("{vals:?}");

    vals[..4].reverse();
    println!("{vals:?}");

    vals.swap(0, 7);
    vals[2..6].fill(0);
    println!("{vals:?}");
}

We change different parts of an array.

fn double_all(vals: &mut [i32]) {
    for v in vals.iter_mut() {
        *v *= 2;
    }
}

The function doubles every element of the slice it gets. We pass it only the first three elements; the rest of the array is not affected.

vals[3..].sort();
...
vals[..4].reverse();

Methods of slices can be called on a range of an array directly. Here we sort the tail and reverse the head.

vals.swap(0, 7);
vals[2..6].fill(0);

The swap method exchanges two elements and fill sets all elements of a slice to one value.

$ cargo run -q
[10, 2, 8, 2, 3, 9, 8, 7]
[10, 2, 8, 2, 3, 7, 8, 9]
[2, 8, 2, 10, 3, 7, 8, 9]
[9, 8, 0, 0, 0, 0, 8, 2]

Accessing elements

Slices offer both panicking and non-panicking access to elements.

main.rs
fn main() {
    let vals = [3, 1, 4, 1, 5, 9, 2, 6];
    let s = &vals[2..6];

    println!("{s:?}");
    println!("first: {:?}, last: {:?}", s.first(), s.last());
    println!("get(1): {:?}, get(10): {:?}", s.get(1), s.get(10));
    println!("get(1..3): {:?}, get(2..9): {:?}", s.get(1..3), s.get(2..9));

    let empty: &[i32] = &[];
    println!("empty first: {:?}, is_empty: {}", empty.first(), empty.is_empty());

    println!("contains 9: {}", s.contains(&9));
    println!("starts_with [4, 1]: {}", s.starts_with(&[4, 1]));
    println!("ends_with [9]: {}", s.ends_with(&[9]));

    let idx = 7;
    println!("{}", s[idx]);
}

The slice s contains the four middle elements of the array.

println!("first: {:?}, last: {:?}", s.first(), s.last());
println!("get(1): {:?}, get(10): {:?}", s.get(1), s.get(10));
println!("get(1..3): {:?}, get(2..9): {:?}", s.get(1..3), s.get(2..9));

The first, last, and get methods return an Option, which is None for an empty slice or an invalid index. The get method also accepts a range and returns a subslice.

println!("starts_with [4, 1]: {}", s.starts_with(&[4, 1]));

The contains, starts_with, and ends_with methods test the contents of a slice.

let idx = 7;
println!("{}", s[idx]);

Indexing with square brackets panics when the index is out of bounds. The array has eight elements, but the slice has only four, and the index is checked against the slice. The number in parentheses after 'main' is the thread ID; it differs between runs.

$ cargo run -q
[4, 1, 5, 9]
first: Some(4), last: Some(9)
get(1): Some(1), get(10): None
get(1..3): Some([1, 5]), get(2..9): None
empty first: None, is_empty: true
contains 9: true
starts_with [4, 1]: true
ends_with [9]: true

thread 'main' (50834) panicked at src/main.rs:18:20:
index out of bounds: the len is 4 but the index is 7
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

String slices

A string slice, &str, is a slice of UTF-8 encoded bytes that is guaranteed to hold valid text.

main.rs
fn main() {
    let s = String::from("hello world");

    let hello: &str = &s[0..5];
    let world: &str = &s[6..];
    println!("{hello} | {world}");

    let lit: &str = "a string literal";
    println!("{}", &lit[2..8]);

    let text = "čerešňa";
    println!("bytes: {}, chars: {}", text.len(), text.chars().count());
    println!("{}", &text[0..2]);

    for (i, c) in text.char_indices() {
        print!("{i}:{c} ");
    }
    println!();

    println!("{:?}", text.get(0..1));
    println!("{:?}", text.get(0..2));
}

We create string slices from a String and from string literals.

let hello: &str = &s[0..5];
let world: &str = &s[6..];

Ranges on strings work with byte offsets, not character positions. For ASCII text, the two are the same.

let lit: &str = "a string literal";

String literals are string slices that point into the program binary.

let text = "čerešňa";
println!("bytes: {}, chars: {}", text.len(), text.chars().count());
println!("{}", &text[0..2]);

The Slovak word for cherry has seven characters but ten bytes, because č, š, and ň take two bytes each in UTF-8. The len method returns the number of bytes. The first character is the slice 0..2.

for (i, c) in text.char_indices() {

The char_indices method yields each character together with its byte offset. These offsets are always valid slice boundaries.

println!("{:?}", text.get(0..1));
println!("{:?}", text.get(0..2));

The get method returns None when a range does not fall on character boundaries.

$ cargo run -q
hello | world
string
bytes: 10, chars: 7
č
0:č 2:e 3:r 4:e 5:š 7:ň 9:a 
None
Some("č")

Slicing inside a character

Slicing a string in the middle of a character panics.

main.rs
fn main() {
    let text = "čerešňa";
    println!("{}", &text[0..1]);
}

Byte 1 lies inside the two-byte character č. A string slice must always contain valid UTF-8, so Rust refuses to create it.

$ cargo run -q

thread 'main' (50856) panicked at src/main.rs:3:25:
end byte index 1 is not a char boundary; it is inside 'č' (bytes 0..2 of string)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Returning string slices

A function can return a slice of its input without copying any text.

main.rs
fn first_word(s: &str) -> &str {
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

fn main() {
    let owned = String::from("quick brown fox");
    let literal = "lazy dog";

    println!("{}", first_word(&owned));
    println!("{}", first_word(&owned[6..]));
    println!("{}", first_word(literal));
    println!("{}", first_word("single"));

    let word = first_word(&owned).to_string();
    drop(owned);
    println!("owned copy survives: {word}");
}

The first_word function returns the text up to the first space.

fn first_word(s: &str) -> &str {
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

The find method returns the byte offset of the space, which we use to slice the input. If there is no space, the whole input is the first word. The result borrows from the argument; how the compiler tracks this is explained in Rust lifetimes.

println!("{}", first_word(&owned));
println!("{}", first_word(&owned[6..]));
println!("{}", first_word(literal));

Because the parameter is &str, the function works with a String, a part of it, and a literal.

let word = first_word(&owned).to_string();
drop(owned);

A slice cannot outlive its owner. If we need the result after the original string is gone, we copy it into a new String.

$ cargo run -q
quick
brown
lazy
single
owned copy survives: quick

Splitting slices

Several methods divide a slice into smaller slices.

main.rs
fn main() {
    let vals = [1, 2, 3, 4, 5, 6, 7];

    let (left, right) = vals.split_at(3);
    println!("{left:?} {right:?}");

    if let Some((head, tail)) = vals.split_first() {
        println!("head {head}, tail {tail:?}");
    }

    if let Some((last, init)) = vals.split_last() {
        println!("last {last}, init {init:?}");
    }

    let data = [1, 0, 2, 3, 0, 4, 0];
    let groups: Vec<&[i32]> = data.split(|&x| x == 0).collect();
    println!("{groups:?}");

    let mut arr = [1, 2, 3, 4, 5, 6];
    let (a, b) = arr.split_at_mut(3);
    a[0] = 100;
    b[0] = 400;
    a.swap_with_slice(b);
    println!("{arr:?}");
}

The example splits an array in various ways.

let (left, right) = vals.split_at(3);

The split_at method divides a slice at an index into two non-overlapping parts.

if let Some((head, tail)) = vals.split_first() {

The split_first method returns the first element and the rest; split_last returns the last element and the rest. Both return None for an empty slice.

let groups: Vec<&[i32]> = data.split(|&x| x == 0).collect();

The split method divides the slice at elements that match a predicate; the separators are not included. The array ends with a zero, so the last group is empty.

let (a, b) = arr.split_at_mut(3);
a[0] = 100;
b[0] = 400;
a.swap_with_slice(b);

The borrow checker does not allow two mutable references to the same array. The split_at_mut method solves this: it returns two mutable slices that are guaranteed not to overlap. We modify both and then swap their contents.

$ cargo run -q
[1, 2, 3] [4, 5, 6, 7]
head 1, tail [2, 3, 4, 5, 6, 7]
last 7, init [1, 2, 3, 4, 5, 6]
[[1], [2, 3], [4], []]
[400, 5, 6, 100, 2, 3]

windows and chunks

The windows and chunks methods iterate over subslices of a fixed size.

main.rs
fn main() {
    let temps = [12, 14, 13, 17, 19, 18, 21];

    for w in temps.windows(3) {
        let avg = w.iter().sum::<i32>() as f64 / 3.0;
        println!("{w:?} avg {avg:.2}");
    }

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

    for c in temps.chunks(3) {
        println!("chunk {c:?}");
    }

    let mut it = temps.chunks_exact(3);
    for c in it.by_ref() {
        println!("exact {c:?}");
    }
    println!("remainder {:?}", it.remainder());

    let bytes = b"ABCDEFGH";
    let rows: Vec<&[u8]> = bytes.rchunks(3).collect();
    println!("{rows:?}");
}

We analyse a week of temperatures.

for w in temps.windows(3) {

The windows method yields overlapping subslices, moving one element at a time. We compute a three-day moving average.

let rising = temps.windows(2).filter(|w| w[1] > w[0]).count();

Windows of two elements give pairs of neighbours. We count the days on which the temperature rose.

for c in temps.chunks(3) {

The chunks method yields non-overlapping subslices. The last chunk can be shorter.

let mut it = temps.chunks_exact(3);
for c in it.by_ref() {
    println!("exact {c:?}");
}
println!("remainder {:?}", it.remainder());

The chunks_exact method yields only full chunks; the leftover elements are available through remainder. The by_ref adapter lets the loop borrow the iterator, so we can use it after the loop.

let bytes = b"ABCDEFGH";
let rows: Vec<&[u8]> = bytes.rchunks(3).collect();

The rchunks method starts at the end; the short chunk is the last one yielded.

$ cargo run -q
[12, 14, 13] avg 13.00
[14, 13, 17] avg 14.67
[13, 17, 19] avg 16.33
[17, 19, 18] avg 18.00
[19, 18, 21] avg 19.33
rising days: 4
chunk [12, 14, 13]
chunk [17, 19, 18]
chunk [21]
exact [12, 14, 13]
exact [17, 19, 18]
remainder [21]
[[70, 71, 72], [67, 68, 69], [65, 66]]

Slice patterns

Slices can be destructured in match expressions and let statements.

main.rs
fn describe(vals: &[i32]) -> String {
    match vals {
        [] => "empty".to_string(),
        [x] => format!("one element: {x}"),
        [first, second] => format!("two elements: {first}, {second}"),
        [first, .., last] => format!("{} elements, from {first} to {last}", vals.len()),
    }
}

fn sum(vals: &[i32]) -> i32 {
    match vals {
        [] => 0,
        [head, tail @ ..] => head + sum(tail),
    }
}

fn main() {
    println!("{}", describe(&[]));
    println!("{}", describe(&[7]));
    println!("{}", describe(&[7, 8]));
    println!("{}", describe(&[1, 2, 3, 4, 5]));

    println!("sum: {}", sum(&[1, 2, 3, 4]));

    let args = ["copy", "a.txt", "b.txt"];
    if let ["copy", src, dst] = args {
        println!("copying {src} to {dst}");
    }
}

The patterns match slices by their length and contents.

match vals {
    [] => "empty".to_string(),
    [x] => format!("one element: {x}"),
    [first, second] => format!("two elements: {first}, {second}"),
    [first, .., last] => format!("{} elements, from {first} to {last}", vals.len()),
}

The .. rest pattern matches any number of elements, including zero. The match is exhaustive: every possible length is covered.

[head, tail @ ..] => head + sum(tail),

The @ binding captures the rest as a subslice. This allows a recursive definition of the sum, as in functional languages.

if let ["copy", src, dst] = args {

Patterns can mix literals and bindings. The command is matched only if the array has exactly three elements and the first one is "copy". More patterns are shown in Rust match expression.

$ cargo run -q
empty
one element: 7
two elements: 7, 8
5 elements, from 1 to 5
sum: 10
copying a.txt to b.txt

Sorting and searching

Sorting and searching methods are defined on slices, so they work on arrays and vectors.

main.rs
fn main() {
    let mut vals = [41, 7, 19, 3, 25, 12, 7];

    vals.sort_unstable();
    println!("{vals:?}");

    println!("search 19: {:?}", vals.binary_search(&19));
    println!("search 20: {:?}", vals.binary_search(&20));

    let pos = vals.binary_search(&20).unwrap_or_else(|i| i);
    println!("20 would be inserted at {pos}");

    let idx = vals.partition_point(|&x| x < 10);
    println!("values below 10: {:?}", &vals[..idx]);

    vals.rotate_left(2);
    println!("rotate_left(2): {vals:?}");

    println!("max: {:?}", vals.iter().max());
    println!("position of 25: {:?}", vals.iter().position(|&x| x == 25));
}

We sort an array and search in it.

vals.sort_unstable();

The sort_unstable method is usually faster than sort and does not allocate. It may reorder equal elements, which does not matter for plain integers.

println!("search 19: {:?}", vals.binary_search(&19));
println!("search 20: {:?}", vals.binary_search(&20));

The binary_search method requires a sorted slice. It returns Ok(index) when the value is found and Err(index) with the position where the value could be inserted to keep the order.

let idx = vals.partition_point(|&x| x < 10);
println!("values below 10: {:?}", &vals[..idx]);

The partition_point method finds the first index where the predicate becomes false, using binary search. Slicing up to that index gives all values below 10.

vals.rotate_left(2);

The rotate_left method moves the first two elements to the end. All iterator methods are available too, through iter.

$ cargo run -q
[3, 7, 7, 12, 19, 25, 41]
search 19: Ok(4)
search 20: Err(5)
20 would be inserted at 5
values below 10: [3, 7, 7]
rotate_left(2): [7, 12, 19, 25, 41, 3, 7]
max: Some(41)
position of 25: Some(3)

Copying and joining slices

Slices can be copied into other slices or into new owned collections.

main.rs
fn main() {
    let src = [1, 2, 3];
    let mut dst = [0; 6];

    dst[..3].copy_from_slice(&src);
    dst[3..].copy_from_slice(&src);
    println!("{dst:?}");

    let owned: Vec<i32> = dst[2..5].to_vec();
    println!("{owned:?}");

    let names = [String::from("Anna"), String::from("Ben")];
    let mut copies = vec![String::new(); 2];
    copies.clone_from_slice(&names);
    println!("{copies:?}");

    let parts = [[1, 2], [3, 4], [5, 6]];
    println!("{:?}", parts.concat());

    let words = ["sky", "is", "blue"];
    println!("{}", words.join(" "));
    println!("{}", words.concat());
}

We copy elements between slices and join nested slices.

dst[..3].copy_from_slice(&src);
dst[3..].copy_from_slice(&src);

The copy_from_slice method copies all elements from another slice of the same length; it panics if the lengths differ. It works for Copy types.

let owned: Vec<i32> = dst[2..5].to_vec();

The to_vec method creates a new vector with copies of the elements. The vector owns its data and is independent of the array.

copies.clone_from_slice(&names);

For types that are not Copy, such as String, the clone_from_slice method clones each element.

println!("{:?}", parts.concat());
println!("{}", words.join(" "));
println!("{}", words.concat());

The concat method flattens a slice of slices into one vector; for string slices it produces a String. The join method inserts a separator between the parts.

$ cargo run -q
[1, 2, 3, 1, 2, 3]
[3, 1, 2]
["Anna", "Ben"]
[1, 2, 3, 4, 5, 6]
sky is blue
skyisblue

Slices are fat pointers

A slice reference carries the length along with the pointer.

main.rs
use std::mem::size_of;

fn main() {
    println!("&i32:   {} bytes", size_of::<&i32>());
    println!("&[i32]: {} bytes", size_of::<&[i32]>());
    println!("&str:   {} bytes", size_of::<&str>());
    println!("Vec:    {} bytes", size_of::<Vec<i32>>());

    let vals = [10, 20, 30, 40, 50];
    let s = &vals[1..4];

    println!("array starts at {:p}", vals.as_ptr());
    println!("slice starts at {:p}", s.as_ptr());
    println!("difference: {} bytes", s.as_ptr() as usize - vals.as_ptr() as usize);
}

We compare the sizes of references and show where a slice points.

println!("&i32:   {} bytes", size_of::<&i32>());
println!("&[i32]: {} bytes", size_of::<&[i32]>());
println!("&str:   {} bytes", size_of::<&str>());
println!("Vec:    {} bytes", size_of::<Vec<i32>>());

On a 64-bit system, a plain reference is eight bytes. Slice references are sixteen bytes: the pointer and the length. A vector adds a third word, the capacity, and owns its buffer.

println!("difference: {} bytes", s.as_ptr() as usize - vals.as_ptr() as usize);

The as_ptr method returns the address of the first element. The slice &vals[1..4] starts one i32, four bytes, after the array. Creating a slice copies nothing; it only computes a new pointer and length. The printed addresses differ between runs.

$ cargo run -q
&i32:   8 bytes
&[i32]: 16 bytes
&str:   16 bytes
Vec:    24 bytes
array starts at 0x7ffc30b57624
slice starts at 0x7ffc30b57628
difference: 4 bytes

Byte slices

A byte slice, &[u8], holds raw binary data that may or may not be valid text.

main.rs
fn main() {
    let s = "hello";
    let bytes: &[u8] = s.as_bytes();
    println!("{bytes:?}");

    let lit: &[u8; 5] = b"world";
    println!("{lit:?}");

    let raw = [82, 117, 115, 116];
    match std::str::from_utf8(&raw) {
        Ok(text) => println!("valid UTF-8: {text}"),
        Err(e) => println!("error: {e}"),
    }

    let mut bad = *b"Rust";
    bad[1] = 0xff;
    match std::str::from_utf8(&bad) {
        Ok(text) => println!("valid UTF-8: {text}"),
        Err(e) => println!("error: {e}"),
    }

    println!("{}", String::from_utf8_lossy(&bad));

    let count = bytes.iter().filter(|&&b| b == b'l').count();
    println!("number of l bytes: {count}");
}

We convert between strings and bytes.

let bytes: &[u8] = s.as_bytes();

The as_bytes method gives a view of a string's UTF-8 bytes. No copy is made.

let lit: &[u8; 5] = b"world";

A byte string literal has the type of a reference to a byte array, which coerces to &[u8].

match std::str::from_utf8(&raw) {
    Ok(text) => println!("valid UTF-8: {text}"),
    Err(e) => println!("error: {e}"),
}

The std::str::from_utf8 function checks whether a byte slice is valid UTF-8 and returns a &str view of it. Byte 0xff never occurs in UTF-8, so the second conversion fails.

println!("{}", String::from_utf8_lossy(&bad));

The from_utf8_lossy function replaces invalid sequences with the replacement character U+FFFD.

let count = bytes.iter().filter(|&&b| b == b'l').count();

A byte literal such as b'l' has the type u8.

$ cargo run -q
[104, 101, 108, 108, 111]
[119, 111, 114, 108, 100]
valid UTF-8: Rust
error: invalid utf-8 sequence of 1 bytes from index 1
R�st
number of l bytes: 2

Source

Primitive type slice

The Rust Programming Language: The Slice Type

In this article we have worked with slices in Rust.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.