Arrays and fixed-size buffers
In Rust an array is a fixed number of values stored inline, with the type [T; N] and the length part of the type. It has no capacity and no heap allocation, so it is the right container for a table whose size the program knows. A vector can grow; an array cannot, and the compiler knows that at every index.
The length is a compile-time constant, which makes some checks possible that a vector can only do at run time. The last section shows the clearest case: an index the compiler can prove is out of range is an error at build time, not a panic.
A fixed-size array
The literal [10, 20, 30] has the type [i32; 3], and the three elements are copied into the value. Reading an element uses the same square brackets as a vector, and the length comes from len.
The program prints the three elements and the length. The array lives on the stack, and the debug output for the whole value would show the same three numbers in square brackets.
fn main() {
let values = [10, 20, 30];
println!("{} {} {}", values[0], values[1], values[2]);
println!("len {}", values.len());
}
rustc --edition 2024 literal.rs
./literal
10 20 30
len 3
An array is a value like any other: assigning it copies it when the element type is Copy, and passing it to a function passes the whole buffer. The lesson on slices shows how to pass a view instead.
Walking an array
A for loop over an array takes it by value when the element type is Copy, so the loop gets a copy of each element and the original stays usable. Adding the values is the smallest complete use of that form.
The program sums three integers and prints fifteen. The loop variable is an i32, not a reference, because the array was moved into the loop and the elements are Copy.
fn main() {
let values = [3, 5, 7];
let mut total = 0;
for value in values {
total += value;
}
println!("total {total}");
}
rustc --edition 2024 iterate.rs
./iterate
total 15
The loop is the same shape as the one for a vector, and the compiler unrolls nothing: it is an ordinary loop over three adjacent values. A very large array on the stack can overflow it before the program starts, because the value is allocated as part of the frame. Iterating over &values would borrow the elements instead, which the lesson on iterators turns into a chain. For an array of String, the by-value loop would move the strings out and the array could not be used again.
Changing an element
An element is assigned with the same square brackets, and the binding must be mutable. The assignment values[0] = 10 replaces the first value, and values[2] += 5 uses the compound form.
The program prints the array after both changes. The length did not move, because nothing was added or removed: only the bytes of the two elements changed.
fn main() {
let mut values = [1, 2, 3];
values[0] = 10;
values[2] += 5;
println!("{values:?}");
}
rustc --edition 2024 mutate.rs
./mutate
[10, 2, 8]
Writing an index that is out of bounds is a panic, and a constant one is a compile error, which the last section reads. The vector lesson shows the same rule with a length that is only known at run time.
A function over any length
A function can take the length as a const generic parameter: fn first<const N: usize>(values: [i32; N]) -> i32. The type says that the length is known when the function is compiled, and one function then serves arrays of any size.
The program calls it with three elements and with one, and the compiler generates a version for each length. The output is the first element of each call.
fn first<const N: usize>(values: [i32; N]) -> i32 {
values[0]
}
fn main() {
println!("{}", first([7, 8, 9]));
println!("{}", first([4]));
}
rustc --edition 2024 generic_len.rs
./generic_len
7
4
The const parameter is the same idea as a type parameter, with a number instead of a type. The lesson on generics covers the type side, and the one on slices the version that accepts any length at run time.
An index the compiler knows
When the index is a literal and the length is part of the type, rustc can prove the access is out of range and refuses the program. The error is the unconditional-panic lint, denied by default, and the note names the length and the index.
The program below reads element five of a three-element array. Nothing runs: the build stops, which is the strongest form of a bounds check.
fn main() {
let values = [1, 2, 3];
println!("{}", values[5]);
}
rustc --edition 2024 bounds_err.rs
error: this operation will panic at runtime
--> bounds_err.rs:3:20
|
3 | println!("{}", values[5]);
| ^^^^^^^^^ index out of bounds: the length is 3 but the index is 5
|
= note: `#[deny(unconditional_panic)]` on by default
error: aborting due to 1 previous error
An index computed at run time gets a panic instead, with the same message in the panic text. The lesson on panics shows how a panic is caught and what it leaves behind.
Exercises
- Write an array of five booleans and count how many are true. Which loop form do you need?
- Turn the length-generic function into one that returns the last element. Which index expression works for every N?
- Change the last program to a variable index and run it. What does the panic message say?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is the type of
[10, 20, 30]?Answer
[i32; 3]
The element type and the length are both part of the array type.
-
Where does an array store its elements?
Answer
Inline where the value lives, usually the stack
An array has no separate allocation, which is what makes its size part of the type.
-
What does a const generic parameter such as
const N: usizeallow?Answer
One function to accept arrays of any length known at compile time
The length is a parameter, so the compiler builds a version of the function per length.
-
What happens for a constant index past the end of an array?
Answer
The compiler refuses the program
The length is part of the type, so rustc can prove the access is out of range.
-
What does this program print?
fn main() { let values = [10, 20, 30]; println!("{} {} {}", values[0], values[1], values[2]); }Answer
10 20 30
The three indices read the three elements in order.
-
What does this program print?
fn main() { let values = [3, 5, 7]; let mut total = 0; for value in values { total += value; } println!("total {total}"); }Answer
total 15
The loop takes each Copy element by value, and the three add up to fifteen.
-
What does this program print?
fn main() { let mut values = [1, 2, 3]; values[0] = 10; values[2] += 5; println!("{values:?}"); }Answer
[10, 2, 8]
The first element is replaced and the third is increased by five.
-
What does this program print?
fn first<const N: usize>(values: [i32; N]) -> i32 { values[0] } fn main() { println!("{}", first([7, 8, 9])); println!("{}", first([4])); }Answer
7 / 4
Each call picks the version for its length, and both return the first element.
-
What is the first line of the compiler output for this program?
fn main() { let values = [1, 2, 3]; println!("{}", values[5]); }Answer
error: this operation will panic at runtime
The constant index is provably out of range, and the note names the length and the index.
-
What does this program print?
fn main() { let values = [1, 2, 3]; let slice = &values[1..]; println!("{slice:?}"); }Answer
[2, 3]
The range starts at index one and runs to the end, and the debug form prints the two values.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev