18.7Arrays and slices, revisited
You've met fixed-size arrays (lesson 4.x) and slices (lesson 9.6) in passing. Now that Vec is in hand, this lesson draws the full picture: when a fixed-length array beats a growable Vec, why slices are the parameter type that lets one function accept both, and how to build the nested structures you'd use for a grid. This absorbs a great deal of C++'s array material into one lesson, because Rust has no decaying C arrays to agonize over.
Arrays: fixed length, known at compile time
An array [T; N] holds exactly N values of type T, with N fixed at compile time. Its length is part of its type, [i32; 3] and [i32; 4] are different types, and because the size is known, an array can live entirely on the stack (lesson 8.2), no heap allocation:
fn main() {
let week = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
let zeros = [0; 5]; // [0, 0, 0, 0, 0]: five zeros
println!("{}", week[0]);
println!("{}", week.len()); // 7
println!("{zeros:?}");
}Mon
7
[0, 0, 0, 0, 0]
[0; 5] is the shorthand for "five copies of 0." Arrays are bounds-checked just like vectors: week[10] panics, week.get(10) returns None (lesson 18.2). The defining trait is the fixed length: you cannot push to an array, because its size can never change. Use an array when the count is genuinely fixed and known up front, the 7 days of a week, the 3 RGB channels of a color, a chess board's 64 squares, and you get stack allocation and a compile-time-known size for free. When the count varies, use a Vec.
Slices: a view into either
Here's the unifying concept. A slice &[T] is a borrowed view into a contiguous run of elements (lesson 9.6), and crucially it doesn't care whether those elements live in an array or a Vec. Both can be viewed as a slice, which makes &[T] the ideal parameter type for a function that just wants to read a sequence:
fn sum(numbers: &[i32]) -> i32 {
let mut total = 0;
for n in numbers {
total += n;
}
total
}
fn main() {
let array = [1, 2, 3];
let vector = vec![4, 5, 6];
println!("{}", sum(&array)); // array works
println!("{}", sum(&vector)); // vec works too
}6
15
sum takes &[i32], a slice, and accepts both &array and &vector. A &[i32; 3] (array reference) and a &Vec<i32> both coerce to &[i32] automatically, the same deref-coercion convenience from lesson 9.7 that lets &String pass where &str is wanted. So you write the function once against the slice and call it with arrays or vectors interchangeably. This is exactly why text functions take &str rather than &String: &str is a slice of string, &[T] is a slice of anything, and slices are the universal "I just need to read a sequence" parameter.
Best practice
Write functions to take &[T] (a slice), not &Vec<T> or &[T; N] (a specific container). The slice accepts arrays, vectors, and sub-ranges of either, so your function works for every caller without caring how they store their data. Take &Vec<T> only when you specifically need vector-only operations. This is the lesson 9.6 advice generalized: borrow the view, not the container.
Nested collections for 2D data
For a grid or table, you nest collections: a Vec of Vecs (dynamic in both dimensions) or an array of arrays (fixed grid):
fn main() {
let grid = vec![
vec![1, 2, 3],
vec![4, 5, 6],
];
println!("{}", grid[1][2]); // row 1, column 2 → 6
for row in &grid {
for cell in row {
print!("{cell} ");
}
println!();
}
}6
1 2 3
4 5 6
grid[1][2] indexes row 1, then column 2 within that row. A Vec<Vec<i32>> lets rows and columns each grow; a fixed [[i32; 3]; 2] (an array of two 3-element arrays) is a stack-allocated fixed grid. Nesting is how you represent any multi-dimensional data, and it's the same Vec/array choice at each level: dynamic dimensions use Vec, fixed dimensions use arrays. This is all Rust needs for the multidimensional data that C++ handles with raw 2D arrays and pointer arithmetic.
The C array material that isn't here
Worth noting what this lesson doesn't contain, because it's a chunk of a C++ course that simply has no Rust equivalent. There are no decaying arrays (a Rust array doesn't silently turn into a pointer and lose its length), no separate length to pass alongside a pointer (a slice carries its own length), and no manual bounds management (every access is checked). The whole category of C-array footguns, off-by-one buffer overruns, lost lengths, pointer arithmetic mistakes, is gone, replaced by arrays that know their size and slices that carry their length. That collapse is why this is one lesson where a C++ course needs many.
Quiz time
Question #1
What's the key difference between an array [i32; 3] and a Vec<i32>, and when do you choose each?
Show solution
An array has a fixed length known at compile time (part of its type) and can live on the stack; a Vec has a runtime-varying length and lives on the heap. Choose an array when the count is genuinely fixed and known up front (days of the week, RGB channels); choose a Vec when the count varies or isn't known until runtime, which is most of the time. You can't push to an array.
Question #2
Why should a function that reads a sequence take &[T] rather than &Vec<T>?
Show solution
Because &[T] (a slice) is a view that both arrays and vectors coerce to, so the function accepts &array, &vector, and sub-ranges of either, working for every caller regardless of how they store the data. &Vec<T> would accept only vectors. Taking the slice is the general, flexible choice, the same reason text functions take &str over &String (lesson 9.6).
Question #3
How would you represent a 2D grid whose dimensions can grow, and how do you index a cell?
Show solution
A Vec<Vec<T>>: a vector of rows, each row a vector of cells, so both dimensions can grow. Index a cell with grid[row][col] (first the row, then the column within it). For a fixed-size grid you'd use nested arrays like [[T; COLS]; ROWS] instead, stack-allocated with fixed dimensions.
You've now seen Vec, String, HashMap, arrays, and slices. The standard library has a few more collections for special cases. The last lesson is a decision guide: when to reach for VecDeque, HashSet, or BTreeMap instead of the core three.