Rust Data Types

Array Types

An array ([T; N]) is a fixed-size collection of N elements of the exact same type T stored contiguously inline in memory. Because an array's length N is part of its compile-time type signature, arrays live directly on the stack with zero heap allocation overhead.

Array Type and Literal Syntax

Rust provides two ways to write an array literal:

  1. Comma-separated list [e1, e2, ..., eN]: Lists each element explicitly.
  2. Repeat expression [value; N]: Creates an array of length N filled with N copies of value (where value implements Copy or is a const expression).

The type annotation for an array is written [ElementType; Length], using a semicolon (;) between the type and the compile-time usize length:

fn main() {
    // 1. Explicit element list with type annotation `[i32; 5]`
    let primes: [i32; 5] = [2, 3, 5, 7, 11];

    // 2. Repeat expression: 8 zeros of type `u8` (`[u8; 8]`)
    let buffer = [0_u8; 8];

    // 3. Inferred element type and length (`[&str; 3]`)
    let days = ["Mon", "Tue", "Wed"];

    println!("primes: {primes:?}");
    println!("buffer: {buffer:?}");
    println!("days:   {days:?}");
}
primes: [2, 3, 5, 7, 11]
buffer: [0, 0, 0, 0, 0, 0, 0, 0]
days:   ["Mon", "Tue", "Wed"]

Initializing Non-Copy Arrays (std::array::from_fn and const)

In [value; N], if value is a runtime variable of a non-Copy type (such as String), the compiler cannot copy it N times implicitly. However, you can either use an inline const expression (such as [const { String::new() }; 4]) or compute each element from its index 0..N using std::array::from_fn:

fn main() {
    let empty_strings: [String; 3] = [const { String::new() }; 3];

    let labels: [String; 4] = std::array::from_fn(|i| format!("item_{i}"));

    println!("empty_strings: {empty_strings:?}");
    println!("labels:        {labels:?}");
}
empty_strings: ["", "", ""]
labels:        ["item_0", "item_1", "item_2", "item_3"]

Memory Layout (size_of::<T>() * N)

An array [T; N] is a single flat block of size_of::<T>() * N bytes with no length header, capacity field, or pointer indirection:

Stack Layout of `let a: [i32; 4] = [10, 20, 30, 40];` (16 bytes total):
+-------------+-------------+-------------+-------------+
|     10      |     20      |     30      |     40      |
|  (4 bytes)  |  (4 bytes)  |  (4 bytes)  |  (4 bytes)  |
+-------------+-------------+-------------+-------------+
  a[0]          a[1]          a[2]          a[3]

Because N is part of the type itself, [i32; 3] and [i32; 4] are two completely distinct, incompatible types. A function expecting [i32; 3] will reject [i32; 4] (to accept arrays of any length, take a slice &[i32] or use a const generic parameter fn process<const N: usize>(arr: [i32; N])).

Indexing and Runtime Bounds Checking

Array elements are accessed with 0-based usize indices inside square brackets (arr[index]):

  • arr[index]: Returns the element at index, or panics at runtime if index >= arr.len(). (If index is a compile-time constant out of bounds, rustc catches it at compile time!)
  • arr.get(index): Safely returns Some(&element) if index < arr.len(), or None if index is out of bounds, without ever panicking.
fn main() {
    let mut scores = [85, 90, 95];

    // Read and modify via `usize` index
    scores[1] = 92;
    println!("First: {}, Second: {}, Len: {}", scores[0], scores[1], scores.len());

    // Safe bounds-checked access with `.get()`
    println!("scores.get(2):  {:?}", scores.get(2));
    println!("scores.get(99): {:?}", scores.get(99));
}
First: 85, Second: 92, Len: 3
scores.get(2):  Some(95)
scores.get(99): None

Compile-Time vs Runtime Out-of-Bounds Detection

When a literal index is out of bounds, rustc refuses to compile the program (unconditional_panic):

// ❌ does not compile
fn main() {
    let data = [10, 20, 30];
    println!("{}", data[5]);
}
error: this operation will panic at runtime
 --> src/main.rs:3:20
  |
3 |     println!("{}", data[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

If the index comes from a runtime variable and exceeds len - 1, Rust panics cleanly at runtime instead of reading out-of-bounds memory as C would:

thread 'main' panicked at src/main.rs:4:20:
index out of bounds: the len is 3 but the index is 5

Useful Array and Slice Methods

Arrays automatically coerce to slices (&[T] and &mut [T]), giving every array access to all slice methods as well as native value-transforming array methods (.map(), .each_ref()):

Method Example on arr = [3, 1, 2] Result / Behavior
arr.len() arr.len() 3 (number of elements)
arr.is_empty() arr.is_empty() false (true only for [T; 0])
arr.first() / arr.last() arr.first() Some(&3) / Some(&2)
arr.contains(&x) arr.contains(&1) true
arr.map(f) arr.map(|x| x * 10) [30, 10, 20] (transforms [T; N] into [U; N] on the stack!)
arr.each_ref() arr.each_ref() [&3, &1, &2] ([&T; N])
arr.sort() arr.sort() Sorts elements in place ([1, 2, 3])
arr.swap(i, j) arr.swap(0, 2) Swaps arr[i] and arr[j] in place
fn main() {
    let numbers: [i32; 4] = [1, 2, 3, 4];

    // `.map()` transforms a fixed-size array `[i32; 4]` directly into `[i32; 4]` without heap allocation!
    let squares: [i32; 4] = numbers.map(|n| n * n);

    println!("numbers: {numbers:?}");
    println!("squares: {squares:?}");
}
numbers: [1, 2, 3, 4]
squares: [1, 4, 9, 16]

Multidimensional Arrays

A 2D array in Rust is an array whose elements are themselves arrays: [[ElementType; Columns]; Rows]. Note that in the type signature [[T; COLS]; ROWS], the inner array length (COLS) comes first and the outer array length (ROWS) comes second:

fn main() {
    // 2 rows, each containing 3 `i32` columns -> type is `[[i32; 3]; 2]`
    let grid: [[i32; 3]; 2] = [
        [10, 20, 30],
        [40, 50, 60],
    ];

    println!("Row 0, Col 2 = {}", grid[0][2]);
    println!("Row 1, Col 1 = {}", grid[1][1]);
}
Row 0, Col 2 = 30
Row 1, Col 1 = 50