Rust - Arrays
Overview
Estimated time: 45–60 minutes
Master Rust's fixed-size array type for compile-time known collections. Learn initialization patterns, safe access methods, and when to choose arrays over vectors.
Learning Objectives
- Create and initialize arrays with various patterns
- Use safe indexing methods and understand bounds checking
- Iterate over arrays efficiently
- Choose between arrays and vectors appropriately
Prerequisites
What are Arrays?
Arrays in Rust are fixed-size collections where the size is known at compile time. Unlike vectors, arrays are allocated on the stack and have a fixed length that cannot change.
Array Type Syntax
fn main() {
// Array of 5 integers
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
// Array of 3 strings
let words: [&str; 3] = ["hello", "world", "rust"];
// Array with repeated values
let zeros: [i32; 10] = [0; 10]; // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
println!("Numbers: {:?}", numbers);
println!("Words: {:?}", words);
println!("Zeros: {:?}", zeros);
}
Expected output:
Numbers: [1, 2, 3, 4, 5]
Words: ["hello", "world", "rust"]
Zeros: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Array Initialization Patterns
Direct Initialization
fn main() {
// Explicit type annotation
let explicit: [i32; 4] = [10, 20, 30, 40];
// Type inference
let inferred = [1.1, 2.2, 3.3]; // [f64; 3]
// Mixed initialization
let mixed: [i32; 5] = [1, 2, 3, 4, 5];
println!("Explicit: {:?}", explicit);
println!("Inferred: {:?}", inferred);
println!("Mixed: {:?}", mixed);
}
Repeated Value Initialization
fn main() {
// All elements the same value
let ones = [1; 8]; // [1, 1, 1, 1, 1, 1, 1, 1]
let text = ["default"; 5]; // ["default", "default", "default", "default", "default"]
// Useful for buffers
let buffer: [u8; 1024] = [0; 1024];
println!("Ones: {:?}", ones);
println!("Text: {:?}", text);
println!("Buffer length: {}", buffer.len());
}
Accessing Array Elements
Indexing with Bounds Checking
fn main() {
let colors = ["red", "green", "blue", "yellow"];
// Safe indexing - panics if out of bounds
println!("First color: {}", colors[0]);
println!("Last color: {}", colors[3]);
// Using get() for safe access
match colors.get(2) {
Some(color) => println!("Third color: {}", color),
None => println!("Index out of bounds"),
}
// Safe access to potentially invalid index
match colors.get(10) {
Some(color) => println!("Color at 10: {}", color),
None => println!("No color at index 10"),
}
}
Expected output:
First color: red
Last color: yellow
Third color: blue
No color at index 10
Mutable Arrays
fn main() {
let mut scores = [85, 90, 78, 92, 88];
println!("Original scores: {:?}", scores);
// Modify individual elements
scores[1] = 95;
scores[4] = 90;
println!("Updated scores: {:?}", scores);
// Using get_mut for safe mutable access
if let Some(score) = scores.get_mut(2) {
*score += 5; // Bonus points
}
println!("Final scores: {:?}", scores);
}
Expected output:
Original scores: [85, 90, 78, 92, 88]
Updated scores: [85, 95, 78, 92, 90]
Final scores: [85, 95, 83, 92, 90]
Array Iteration
Basic Iteration Patterns
fn main() {
let numbers = [1, 2, 3, 4, 5];
// Iterate by value
println!("By value:");
for num in numbers {
println!(" {}", num);
}
// Iterate by reference
println!("By reference:");
for num in &numbers {
println!(" {}", num);
}
// Iterate with index
println!("With index:");
for (i, num) in numbers.iter().enumerate() {
println!(" Index {}: {}", i, num);
}
}
Iterator Methods
fn main() {
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Filter and collect
let evens: Vec = numbers.iter()
.filter(|&&n| n % 2 == 0)
.cloned()
.collect();
// Map and sum
let sum_of_squares: i32 = numbers.iter()
.map(|&n| n * n)
.sum();
// Find element
let first_big = numbers.iter()
.find(|&&n| n > 5);
println!("Original: {:?}", numbers);
println!("Evens: {:?}", evens);
println!("Sum of squares: {}", sum_of_squares);
println!("First > 5: {:?}", first_big);
}
Expected output:
Original: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Evens: [2, 4, 6, 8, 10]
Sum of squares: 385
First > 5: Some(6)
Array Slicing
Creating Slices from Arrays
fn main() {
let data = [10, 20, 30, 40, 50, 60, 70, 80];
// Full slice
let full_slice: &[i32] = &data;
// Partial slices
let first_half = &data[0..4];
let last_half = &data[4..];
let middle = &data[2..6];
println!("Full array: {:?}", data);
println!("Full slice: {:?}", full_slice);
println!("First half: {:?}", first_half);
println!("Last half: {:?}", last_half);
println!("Middle: {:?}", middle);
// Slice methods work on arrays
println!("Contains 30: {}", data.contains(&30));
println!("Starts with [10, 20]: {}", data.starts_with(&[10, 20]));
}
Multi-dimensional Arrays
2D Arrays
fn main() {
// 3x3 matrix
let matrix: [[i32; 3]; 3] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
// Access elements
println!("Element at (1,2): {}", matrix[1][2]);
// Iterate through matrix
println!("Matrix:");
for row in &matrix {
for &element in row {
print!("{:3}", element);
}
println!();
}
// Initialize with same value
let zeros: [[i32; 4]; 3] = [[0; 4]; 3];
println!("Zeros matrix: {:?}", zeros);
}
Expected output:
Element at (1,2): 6
Matrix:
1 2 3
4 5 6
7 8 9
Zeros matrix: [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
Arrays vs Vectors
When to Use Arrays
fn main() {
// Arrays: Fixed size, stack allocated, compile-time known size
let coordinates: [f64; 3] = [1.0, 2.0, 3.0]; // 3D coordinate
let rgb: [u8; 3] = [255, 128, 0]; // Color values
let weekdays: [&str; 7] = [
"Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday"
];
// Vectors: Dynamic size, heap allocated, runtime flexibility
let mut dynamic_list = Vec::new();
dynamic_list.push(1);
dynamic_list.push(2);
println!("Coordinate: {:?}", coordinates);
println!("RGB color: {:?}", rgb);
println!("Weekdays: {:?}", weekdays);
println!("Dynamic list: {:?}", dynamic_list);
// Arrays have fixed size known at compile time
println!("Array size is known: {}", coordinates.len());
// This would not compile: coordinates.push(4.0);
}
Performance Characteristics
Memory Layout and Performance
use std::mem;
fn main() {
let array: [i32; 5] = [1, 2, 3, 4, 5];
let vector: Vec = vec![1, 2, 3, 4, 5];
println!("Array size: {} bytes", mem::size_of_val(&array));
println!("Vector size: {} bytes", mem::size_of_val(&vector));
// Array elements are contiguous in memory
println!("Array memory layout:");
for (i, element) in array.iter().enumerate() {
println!(" Element {}: {:p}", i, element);
}
// Arrays are always on the stack
println!("Array is on stack: {}", is_on_stack(&array));
}
fn is_on_stack(_item: &T) -> bool {
// This is a simplified check - in real code, determining
// stack vs heap allocation is more complex
true // Arrays are always on stack
}
Common Pitfalls
Array Size Mismatch
fn main() {
// This will not compile - size mismatch
// let wrong: [i32; 3] = [1, 2, 3, 4, 5];
// Correct way
let correct: [i32; 5] = [1, 2, 3, 4, 5];
// Arrays of different sizes are different types
let small: [i32; 3] = [1, 2, 3];
let large: [i32; 5] = [1, 2, 3, 4, 5];
// This won't compile - different types
// let mixed = if true { small } else { large };
println!("Small: {:?}", small);
println!("Large: {:?}", large);
}
Index Out of Bounds
fn main() {
let data = [1, 2, 3];
// This will panic at runtime
// println!("{}", data[5]);
// Safe alternative
match data.get(5) {
Some(value) => println!("Value: {}", value),
None => println!("Index 5 is out of bounds"),
}
// Or use unwrap_or for default values
let value = data.get(5).unwrap_or(&-1);
println!("Value at index 5 (or default): {}", value);
}
Real-World Examples
Coordinate System
#[derive(Debug)]
struct Point3D {
coords: [f64; 3],
}
impl Point3D {
fn new(x: f64, y: f64, z: f64) -> Self {
Point3D {
coords: [x, y, z],
}
}
fn distance_from_origin(&self) -> f64 {
self.coords.iter()
.map(|&c| c * c)
.sum::()
.sqrt()
}
fn add(&self, other: &Point3D) -> Point3D {
let mut result = [0.0; 3];
for i in 0..3 {
result[i] = self.coords[i] + other.coords[i];
}
Point3D { coords: result }
}
}
fn main() {
let p1 = Point3D::new(1.0, 2.0, 3.0);
let p2 = Point3D::new(4.0, 5.0, 6.0);
println!("Point 1: {:?}", p1);
println!("Distance from origin: {:.2}", p1.distance_from_origin());
let sum = p1.add(&p2);
println!("Sum: {:?}", sum);
}
Game Board
#[derive(Debug, Clone, Copy, PartialEq)]
enum Cell {
Empty,
X,
O,
}
struct TicTacToe {
board: [[Cell; 3]; 3],
}
impl TicTacToe {
fn new() -> Self {
TicTacToe {
board: [[Cell::Empty; 3]; 3],
}
}
fn make_move(&mut self, row: usize, col: usize, player: Cell) -> bool {
if row < 3 && col < 3 && self.board[row][col] == Cell::Empty {
self.board[row][col] = player;
true
} else {
false
}
}
fn display(&self) {
for row in &self.board {
for &cell in row {
let symbol = match cell {
Cell::Empty => ".",
Cell::X => "X",
Cell::O => "O",
};
print!("{} ", symbol);
}
println!();
}
}
fn check_winner(&self) -> Option {
// Check rows
for row in &self.board {
if row[0] != Cell::Empty && row[0] == row[1] && row[1] == row[2] {
return Some(row[0]);
}
}
// Check columns
for col in 0..3 {
if self.board[0][col] != Cell::Empty &&
self.board[0][col] == self.board[1][col] &&
self.board[1][col] == self.board[2][col] {
return Some(self.board[0][col]);
}
}
// Check diagonals
if self.board[0][0] != Cell::Empty &&
self.board[0][0] == self.board[1][1] &&
self.board[1][1] == self.board[2][2] {
return Some(self.board[0][0]);
}
if self.board[0][2] != Cell::Empty &&
self.board[0][2] == self.board[1][1] &&
self.board[1][1] == self.board[2][0] {
return Some(self.board[0][2]);
}
None
}
}
fn main() {
let mut game = TicTacToe::new();
game.make_move(0, 0, Cell::X);
game.make_move(1, 1, Cell::O);
game.make_move(0, 1, Cell::X);
game.make_move(1, 0, Cell::O);
game.make_move(0, 2, Cell::X);
println!("Game board:");
game.display();
match game.check_winner() {
Some(Cell::X) => println!("X wins!"),
Some(Cell::O) => println!("O wins!"),
_ => println!("Game continues..."),
}
}
|
Expected output:
Game board:
X X X
O O .
. . .
X wins!
Best Practices
Choosing Array Size
- Use arrays when the size is known at compile time and won't change
- Prefer vectors for dynamic collections
- Consider arrays for small, fixed collections (coordinates, colors, etc.)
- Use const generics for flexible array functions
Safe Access Patterns
- Use
get()
method for potentially invalid indices - Prefer iterator methods over manual indexing
- Use slicing to work with portions of arrays
- Consider bounds checking for user input
Checks for Understanding
Question 1: Array Initialization
Q: How would you create an array of 100 zeros of type f32?
Click to see answer
A: let zeros: [f32; 100] = [0.0; 100];
The syntax [value; size]
creates an array where all elements are initialized to the same value.
Question 2: Safe Access
Q: What's the difference between array[index]
and array.get(index)
?
Click to see answer
A: array[index]
panics if index is out of bounds, while array.get(index)
returns Option<T>
- Some(value)
for valid indices or None
for invalid ones.
Question 3: Type System
Q: Are [i32; 3]
and [i32; 5]
the same type?
Click to see answer
A: No, they are different types. Array types include both the element type and the size, so [i32; 3]
and [i32; 5]
are distinct types that cannot be used interchangeably.