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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
The Rust Programming Language: The Slice Type
In this article we have worked with slices in Rust.
Author
List all Rust tutorials.