4.5 · Slices and Arrays: The Foundation Types
Domain 4 — Collections Deep Dive Duration: ~15 minutes Library components: primitive
[T](slice), primitive[T; N](array),std::slice,std::array
Introduction
Each contiguous collection in Rust has two primitive types as its base: slices ([T]) and arrays ([T; N]). A slice is a dynamically-sized view into a contiguous sequence of T values. An array is a fixed-size, stack-allocated sequence, and its length is part of its type.
You never own a bare [T]. You use it through a reference (&[T], &mut [T]) or a boxed pointer (Box<[T]>). Arrays are different, because they are values. [i32; 4] is a concrete type, and its value is in the location where you put it. An array is Copy if T is Copy.
Vec<T> implements Deref<Target = [T]>, so each slice method is automatically available on a vector. Thus the slice methods are not a specialized subject. They are the base of all work with sequential data in Rust. This is true for each backing store:
- a
Vec - an array
- a
Box<[T]> - a memory-mapped buffer
This tutorial shows:
- How to sort a slice with
sort,sort_by, andsort_unstable. - How to search a sorted slice with
binary_searchandpartition_point. - Chunks, windows, and splits:
chunks,chunks_exact,rchunks,windows,split,splitn,split_first,split_last,contains,starts_with. array_windowsandelement_offset(stabilized in 1.94).- In-place manipulation:
rotate_left,rotate_right,fill,fill_with,swap,swap_with_slice,reverse,copy_from_slice,clone_from_slice,repeat,concat,join. - Array utilities:
array::from_fn,each_ref,each_mut,map, and const generic arrays. array::try_from_fn(still unstable in 1.99) and the alternative that you use on stable Rust.
Slice indexing uses range syntax (a..b, a..=b, ..) frequently. Tutorial 14.2 describes the RangeBounds trait and the Bound type that this syntax uses. For advanced slice algorithms (partition-point, unstable select, group-by), see Tutorial 25.2.
Sorting and Searching
sort and sort_by
sort() puts the elements in ascending order. It requires T: Ord. It is a stable sort: equal elements keep their initial relative order. sort_by takes a comparator closure, so you fully control the order.
let mut scores = vec![85, 92, 78, 92, 88, 78, 95];
scores.sort(); // stable sort, ascending order
// scores is now [78, 78, 85, 88, 92, 92, 95]
A comparator is necessary for an order that is not the default order. These sorts all use sort_by:
- a case-insensitive sort of strings
- a sort in descending order
- a sort of structs on more than one field
let mut words = vec!["Charlie", "alice", "Bob", "diana"];
// The comparator compares the lowercase forms, so the case has no effect on the order.
words.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
// words is now ["alice", "Bob", "Charlie", "diana"]
// A plain sort() gives ["Bob", "Charlie", "alice", "diana"]: uppercase letters sort first.
let mut nums = vec![3, 1, 4, 1, 5, 9];
nums.sort_by(|a, b| b.cmp(a)); // b before a: descending order
// nums is now [9, 5, 4, 3, 1, 1]
For structs, sort_by lets you sort on one field. sort_by_key is a shorter form of the same sort: its closure returns the sort key. The two methods are stable. When two students have the same grade, they stay in their initial order:
// students: Vec<Student>. A Student has a `name: &'static str` and a `grade: u32`.
// Initial order: Alice 88, Bob 95, Carol 72, Dave 88.
students.sort_by(|a, b| a.grade.cmp(&b.grade));
// The same sort with a key function. `04_15_slice_sorting.rs` uses this form.
students.sort_by_key(|s| s.grade);
// Sorted order: Carol 72, Alice 88, Dave 88, Bob 95. Alice stays before Dave.
sort_unstable
sort_unstable() is typically faster than sort(). It does not allocate temporary storage, and it does not guarantee that equal elements keep their initial order. Use it when stability is not important. Also use it when the elements are simple scalars, for which the initial order of equal elements has no meaning.
For floating-point data, you cannot call sort or sort_unstable directly. f64 implements PartialOrd but not Ord, because of NaN. Use sort_by or sort_unstable_by with partial_cmp and unwrap:
let mut floats = vec![2.72, 1.41, 0.58, 1.73];
// partial_cmp returns None if an operand is NaN, and then unwrap panics. This data has no NaN.
floats.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
// floats is now [0.58, 1.41, 1.73, 2.72]
After the sort, is_sorted() returns true. This method only examines the order. It does not sort the slice.
binary_search and partition_point
When a slice is in sorted order, binary_search finds an element in O(log n). If it finds the value, it returns Ok(index). If it does not find the value, it returns Err(index). That index is the position where you can insert the value and keep the sorted order:
let data = [2, 4, 6, 8, 10, 12, 14, 16]; // must be in sorted order
assert_eq!(data.binary_search(&10), Ok(4)); // 10 is at index 4
assert_eq!(data.binary_search(&7), Err(3)); // no 7: its insertion point is index 3
partition_point is a related method. It takes a predicate and returns the index of the first element for which the predicate returns false. Use it to divide a sorted slice into two parts at a boundary value:
// `data` is the sorted array from the previous snippet.
let cut = data.partition_point(|&x| x < 10); // 4
// data[..cut] is [2, 4, 6, 8]: each element is < 10
// data[cut..] is [10, 12, 14, 16]: each element is >= 10
04_15_slice_sorting.rs prints:
before sort: [85, 92, 78, 92, 88, 78, 95]
after sort: [78, 78, 85, 88, 92, 92, 95]
case-insensitive sort: ["alice", "Bob", "Charlie", "diana"]
descending: [9, 5, 4, 3, 1, 1]
sorted by grade:
Carol — 72
Alice — 88
Dave — 88
Bob — 95
sort_unstable: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
sorted floats: [0.58, 1.41, 1.73, 2.72]
[0..9] is_sorted = true
binary_search(&10) = Ok(4)
binary_search(&7) = Err(3)
7 would be inserted at index 3
partition_point(< 10) = 4
below: [2, 4, 6, 8]
at/above: [10, 12, 14, 16]
All assertions passed.
Chunks, Windows, and Splitting
Figure: chunks(3) vs windows(3) vs split
chunks and chunks_exact
chunks(n) divides a slice into non-overlapping sub-slices of length n. The last chunk may be shorter if the slice length is not a multiple of n. chunks_exact(n) is stricter. It yields only full-size chunks, and its remainder() method returns the elements that remain:
let data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for chunk in data.chunks(3) { // chunk: &[i32]
println!(" {chunk:?}");
}
// [1, 2, 3], [4, 5, 6], [7, 8, 9], [10]: the last chunk has only 1 element
let exact = data.chunks_exact(3); // yields [1, 2, 3], [4, 5, 6], [7, 8, 9]
let remainder = exact.remainder(); // [10]: the element that does not fill a chunk
rchunks(n) starts at the right end of the slice. Thus the chunk at the left end may be short, and not the chunk at the right end. The iterator yields that chunk last.
windows
windows(n) yields overlapping sub-slices of length n. Each step moves forward by one element. Use it for sliding-window algorithms such as moving averages, pairwise comparisons, and pattern detection:
let prices = [100.0, 102.0, 98.0, 105.0, 103.0, 99.0, 107.0];
let averages: Vec<f64> = prices
.windows(3) // w: &[f64], always 3 prices
.map(|w| w.iter().sum::<f64>() / 3.0) // the average of one window
.collect();
// 7 prices give 5 windows. To 1 decimal place: [100.0, 101.7, 102.0, 102.3, 103.0]
windows is different from chunks: it never yields a partial slice. If the slice has fewer than n elements, the iterator is empty.
array_windows::<N>() (stabilized in 1.94) is the same operation with a window length that is part of the type. Each window is a &[T; N] array, not a &[T] slice. Thus a closure can destructure the window, and the compiler checks the number of elements:
let readings = [12, 15, 19, 18, 14, 16];
// windows(2): w is &[i32], so the closure uses indexes.
let with_windows: Vec<i32> = readings.windows(2).map(|w| w[1] - w[0]).collect();
// array_windows: each window is &[i32; 2]. The pattern [a, b] sets N = 2.
let deltas: Vec<i32> = readings.array_windows().map(|[a, b]| b - a).collect();
assert_eq!(deltas, [3, 4, -1, -4, 2]);
assert_eq!(deltas, with_windows);
element_offset (stabilized in 1.94) answers a related question: which index does this element reference have? It compares addresses, not values. It returns None for a reference that does not point to an element of the slice:
let duplicates = [7, 3, 7];
let last: &i32 = &duplicates[2]; // a reference to the third element
// position compares VALUES: the first element that is equal to 7 is at index 0.
assert_eq!(duplicates.iter().position(|value| value == last), Some(0));
// element_offset compares ADDRESSES: `last` points to index 2.
assert_eq!(duplicates.element_offset(last), Some(2));
let outside = 7; // an equal value that is not in the slice
assert_eq!(duplicates.element_offset(&outside), None);
04_21_array_windows_element_offset.rs prints:
deltas = [3, 4, -1, -4, 2]
peaks = 1
windows of 3 in a slice of 2: 0
hottest reading 19 is at index 2
an equal value outside the slice: None
position = Some(0), element_offset = Some(2)
All assertions passed.
Tutorial 25.2 uses array_windows again for pairwise processing.
split and splitn
split(predicate) divides a slice at each element that matches the predicate. str::split divides a string at a pattern in a similar way. The sub-slices do not include the elements that match. Two adjacent elements that match give an empty slice between them:
let data = [1, 0, 2, 3, 0, 0, 4, 5];
// Each 0 is a delimiter. The result does not contain the delimiters.
let segments: Vec<&[i32]> = data.split(|&x| x == 0).collect();
// segments is [[1], [2, 3], [], [4, 5]]: the two adjacent zeros give the empty slice
splitn(n, predicate) gives a maximum of n sub-slices. The last sub-slice contains the remainder of the slice, which the method does not split.
split_first, split_last, contains, starts_with
These convenience methods are small, but you use them frequently:
split_first()returnsSome((first_element, rest_of_slice)), orNoneif the slice is empty.split_last()returnsSome((last_element, init_of_slice)), orNoneif the slice is empty.contains(&value)does a linear scan for an element that is equal tovalue.starts_with(&[T])andends_with(&[T])tell you if a slice starts or ends with the given sub-slice.
04_16_slice_chunks_windows.rs prints:
chunks(3):
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
[10]
chunks_exact(3): [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
remainder: [10]
rchunks(3): [[8, 9, 10], [5, 6, 7], [2, 3, 4], [1]]
windows(3):
[10, 20, 30]
[20, 30, 40]
[30, 40, 50]
3-period moving average: [100.0, 101.7, 102.0, 102.3, 103.0]
split on zeros:
[1]
[2, 3]
[]
[4, 5]
splitn(3, 0): [[1], [2], [3, 0, 4]]
split_first: first=10, rest=[20, 30, 40]
split_last: last=40, init=[10, 20, 30]
data.contains(&5) = true
starts_with [1,2,3] = true
All assertions passed.
In-Place Slice Manipulation
rotate_left and rotate_right
rotate_left(n) moves each element n positions to the left. The first n elements go to the end of the slice. rotate_right(n) does the opposite operation. The two methods run in O(len) time with O(1) additional space:
let mut buf = [1, 2, 3, 4, 5];
buf.rotate_left(2); // 1 and 2 go to the end
// buf is now [3, 4, 5, 1, 2]
Rotations are useful for ring buffers, cyclic permutations, and algorithms that reorder elements in place.
fill and fill_with
fill(value) sets each element of the slice to a clone of value. fill_with(f) calls a closure for each element. Use fill_with when each position needs a different value, or when the type is not Clone:
let mut buf = [0u8; 8]; // 8 bytes, each one is 0
buf.fill(0xFF); // 0xFF is 255
// buf is now [255, 255, 255, 255, 255, 255, 255, 255]
let mut counter = 0;
let mut data = [0; 5];
// fill_with calls the closure one time for each element, from first to last.
data.fill_with(|| { counter += 10; counter });
// data is now [10, 20, 30, 40, 50]
swap and swap_with_slice
swap(i, j) exchanges two elements of the same slice. It panics if one of the indexes is out of bounds. swap_with_slice(other) exchanges the contents of two mutable slices of equal length. It does for slices what std::mem::swap does for single values:
let mut letters = ['a', 'b', 'c', 'd', 'e'];
letters.swap(1, 3); // exchanges 'b' (index 1) and 'd' (index 3)
// letters is now ['a', 'd', 'c', 'b', 'e']
let mut left = [1, 2, 3];
let mut right = [7, 8, 9];
left.swap_with_slice(&mut right); // panics if the two lengths are different
// left=[7, 8, 9], right=[1, 2, 3]
reverse
reverse() reverses the order of the elements in place. To reverse only a part of the slice, first take a mutable sub-slice:
let mut data = [1, 2, 3, 4, 5];
data.reverse();
// data is now [5, 4, 3, 2, 1]
let mut partial = [10, 20, 30, 40, 50];
partial[1..4].reverse(); // reverses only the elements at indexes 1, 2, and 3
// partial is now [10, 40, 30, 20, 50]
copy_from_slice and clone_from_slice
copy_from_slice(src) copies the elements of src into the slice. The two slices must have the same length. This method is the safe wrapper around memcpy for Copy types. clone_from_slice(src) does the same operation, but it calls clone() on each element. Thus it also works for types that are not Copy.
repeat, concat, and join
repeat(n) makes a new Vec that contains the elements of the slice n times. concat() and join(sep) operate on a slice of slices. concat flattens the inner slices into one Vec. join does the same and puts a separator between the inner slices:
let pattern = [1, 2, 3];
let repeated: Vec<i32> = pattern.repeat(3); // [1, 2, 3, 1, 2, 3, 1, 2, 3]
// A slice of slices. The inner slices can have different lengths.
let slices: &[&[i32]] = &[&[1, 2], &[3, 4], &[5]];
let concatenated: Vec<i32> = slices.concat(); // [1, 2, 3, 4, 5]
let joined: Vec<i32> = slices.join(&0); // [1, 2, 0, 3, 4, 0, 5]
04_17_slice_manipulation.rs prints:
before rotate_left(2): [1, 2, 3, 4, 5]
after rotate_left(2): [3, 4, 5, 1, 2]
rotate_right(2): [4, 5, 1, 2, 3]
fill(0xFF): [255, 255, 255, 255, 255, 255, 255, 255]
fill_with(counter): [10, 20, 30, 40, 50]
before swap(1, 3): ['a', 'b', 'c', 'd', 'e']
after swap(1, 3): ['a', 'd', 'c', 'b', 'e']
before swap_with_slice: left=[1, 2, 3], right=[7, 8, 9]
after swap_with_slice: left=[7, 8, 9], right=[1, 2, 3]
before reverse: [1, 2, 3, 4, 5]
after reverse: [5, 4, 3, 2, 1]
reverse [1..4]: [10, 40, 30, 20, 50]
copy_from_slice into [1..4]: [0, 100, 200, 300, 0]
clone_from_slice: ["a", "b"]
[1,2,3].repeat(3) = [1, 2, 3, 1, 2, 3, 1, 2, 3]
concat: [1, 2, 3, 4, 5]
join(&0): [1, 2, 0, 3, 4, 0, 5]
All assertions passed.
Array Utilities and Const Generics
array::from_fn and array::try_from_fn
std::array::from_fn makes an array of length N. It calls a closure one time for each index. The compiler infers the array length from the return type or from a turbofish annotation. This function is the idiomatic way to initialize an array with computed values:
// The closure gets each index `i` (a usize), from 0 to N - 1.
let squares: [i32; 10] = std::array::from_fn(|i| (i * i) as i32);
// [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
let powers: [u32; 8] = std::array::from_fn(|i| 1 << i);
// [1, 2, 4, 8, 16, 32, 64, 128]
// b'a' is the byte 97. The index adds 0 to 25, which gives the bytes of 'a' to 'z'.
let alphabet: [char; 26] = std::array::from_fn(|i| (b'a' + i as u8) as char);
// ['a', 'b', 'c', ..., 'z']
// The turbofish sets the length (5). No type annotation is necessary.
let ids = std::array::from_fn::<_, 5, _>(|i| format!("#{}", i + 1));
// ids: [String; 5] = ["#1", "#2", "#3", "#4", "#5"]
array::try_from_fn is the fallible counterpart. Its closure returns a Result (or an Option). If one call of the closure fails, the full construction fails. This function is still unstable in Rust 1.99 (nightly feature array_try_from_fn), so stable Rust rejects it.
On stable Rust, collect the elements into a Result<Vec<T>, E>. Then convert the Vec into an array with try_into:
let inputs = ["10", "20", "30", "40"];
// Nightly only: std::array::try_from_fn(|i| inputs[i].parse::<i32>())
// Stable: collect stops at the first Err and returns it.
let parsed: Result<Vec<i32>, _> = inputs.iter().map(|s| s.parse::<i32>()).collect();
// Vec<i32> to [i32; 4]: try_into fails if the Vec does not have exactly 4 elements.
let parsed: [i32; 4] = parsed.unwrap().try_into().unwrap();
// parsed is [10, 20, 30, 40]
let bad: Result<Vec<i32>, _> = ["1", "oops", "3"].iter().map(|s| s.parse::<i32>()).collect();
// bad is Err(ParseIntError { kind: InvalidDigit }): "oops" is not a number
Fallible construction is especially useful when you parse fixed-size input in which each element might be invalid.
each_ref and each_mut
each_ref() converts a &[T; N] into a [&T; N]: an array of references, one for each element. each_mut() does the same for mutable references. These methods are useful when you must pass references to single elements to an API that expects &T:
let values = [10, 20, 30];
let refs: [&i32; 3] = values.each_ref(); // borrows each element, does not move `values`
assert_eq!(*refs[1], 20);
let mut mutable = [1, 2, 3];
let [a, b, c] = mutable.each_mut(); // a, b, c: &mut i32, one for each element
*a *= 10;
*b *= 10;
*c *= 10;
assert_eq!(mutable, [10, 20, 30]); // the writes changed the array itself
map on arrays
Arrays have a map method. It transforms each element by value and returns a new array of the same length. It is different from the iterator map: it is eager, and the result is still an array:
let names = ["Alice", "Bob", "Carol"];
let lengths: [usize; 3] = names.map(|n| n.len()); // one length for each name
// [5, 3, 5]
let ints = [1, 2, 3, 4, 5];
// map can change the element type (i32 to String). The length stays 5.
let strings: [String; 5] = ints.map(|n| format!("#{n}"));
// ["#1", "#2", "#3", "#4", "#5"]
Converting between slices and arrays
[T; N] implements TryFrom<&[T]> when T is Copy. Thus you can convert a slice reference to an array when the length matches. The conversion copies the elements. If the slice has the wrong length, you get an Err(TryFromSliceError):
// The annotation makes `slice` a real slice. Without it, the type is &[i32; 3].
let slice: &[i32] = &[10, 20, 30];
let arr: [i32; 3] = slice.try_into().unwrap(); // Ok: the slice has exactly 3 elements
let wrong: Result<[i32; 5], _> = slice.try_into(); // 3 elements, but the target needs 5
// wrong is Err(TryFromSliceError(()))
Const generic arrays
With const generics, you can write a function that is generic over the array length N. This gives type-safe, zero-cost abstractions over fixed-size data:
// N is a const generic parameter. The two arrays must have the same length N.
fn dot_product<const N: usize>(a: &[f64; N], b: &[f64; N]) -> f64 {
let mut sum = 0.0;
for i in 0..N {
sum += a[i] * b[i];
}
sum
}
dot_product(&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]); // N = 3: 1*4 + 2*5 + 3*6 = 32.0
dot_product(&[1.0, 0.0], &[0.0, 1.0]); // N = 2: 1*0 + 0*1 = 0.0
// dot_product(&[1.0, 2.0], &[1.0]); // error[E0308]: mismatched types
The compiler monomorphizes the function for each array length that the call sites use. Thus there is no runtime dispatch, and the compiler knows the loop bounds at compile time.
You can also zip two arrays into one array of pairs. Use std::array::from_fn together with indexing:
let keys = ["x", "y", "z"];
let vals = [10, 20, 30];
// The closure reads index `i` of each array. The annotation sets the length to 3.
let zipped: [(&str, i32); 3] = std::array::from_fn(|i| (keys[i], vals[i]));
// zipped is [("x", 10), ("y", 20), ("z", 30)]
04_18_array_utilities.rs prints:
squares: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
powers of 2: [1, 2, 4, 8, 16, 32, 64, 128]
alphabet: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
parsed: [10, 20, 30, 40]
bad parse: Err(ParseIntError { kind: InvalidDigit })
each_ref: [10, 20, 30]
each_mut after *10: [10, 20, 30]
name lengths: [5, 3, 5]
mapped to strings: ["#1", "#2", "#3", "#4", "#5"]
slice→array: [10, 20, 30]
wrong size: Err(TryFromSliceError(()))
dot_product([1,2,3], [4,5,6]) = 32
dot_product([1,0], [0,1]) = 0
zipped: [("x", 10), ("y", 20), ("z", 30)]
All assertions passed.
Summary
| Concept | Key APIs |
|---|---|
| Stable sort | sort(), sort_by(cmp): equal elements keep their order |
| Unstable sort | sort_unstable(): typically faster, no allocation, no stability guarantee |
| Binary search | binary_search(&val): Ok(index) or Err(insert_point) on a sorted slice |
| Partition point | partition_point(pred): the index where the predicate changes from true to false |
| Chunking | chunks(n), chunks_exact(n), rchunks(n): non-overlapping sub-slices |
| Windowing | windows(n): overlapping sub-slices of length n. array_windows::<N>() (1.94): overlapping &[T; N] arrays |
| Element index | element_offset(&elem) (1.94): the index of a reference that points into the slice, by address |
| Splitting | split(pred), splitn(n, pred), split_first, split_last |
| Membership | contains(&val), starts_with(&[T]), ends_with(&[T]) |
| Rotation | rotate_left(n), rotate_right(n): cyclic shift in O(len) |
| Fill | fill(val), fill_with(f): set all the elements |
| Swap | swap(i, j), swap_with_slice(other): exchange elements |
| Reverse | reverse(): in-place reversal |
| Copy | copy_from_slice, clone_from_slice: bulk copy between slices of the same length |
| Repeat/join | repeat(n), concat(), join(sep): make a new Vec from slices |
| Array construction | array::from_fn(f): make an array from a closure. array::try_from_fn(f) is the fallible form (unstable in 1.99) |
| Array transforms | each_ref, each_mut, map: element-wise operations that keep the array type |
| Slice to array | <[T; N]>::try_from(&[T]): fallible conversion, the lengths must match |
| Const generics | fn foo<const N: usize>(arr: [T; N]): generic over the array length |
Code Examples
| File | Description |
|---|---|
04_15_slice_sorting.rs | sort, sort_by, sort_by_key, sort_unstable, is_sorted, binary_search, partition_point |
04_16_slice_chunks_windows.rs | chunks, chunks_exact, rchunks, windows, split, splitn, contains |
04_17_slice_manipulation.rs | rotate_left/right, fill, swap, reverse, copy_from_slice, repeat, concat, join |
04_18_array_utilities.rs | array::from_fn, the stable alternative to try_from_fn (unstable in 1.99), each_ref, each_mut, map, slice-to-array try_into, const generics |
04_21_array_windows_element_offset.rs | array_windows and element_offset (1.94): windows as arrays, and the index of an element reference |