Data structures are how you organize information in your program. Rust provides a rich set of built-in types: structs for grouping named fields, enums for types with multiple variants, Option for representing the presence or absence of a value, arrays for fixed-length collections, and vectors for dynamic collections. Each is designed to work seamlessly with Rust's ownership and type system.
Structs
A struct (structure) groups related data together under a single name. Structs in Rust are similar to structs in C, but with the addition of methods and trait implementations.
Defining a Struct
struct Point {
x: f64,
y: f64,
}
Struct of Structs
Structs can contain other structs:
struct Line {
start: Point,
end: Point,
}
Using Structs
fn structures() {
let p = Point { x: 3.0, y: 4.0 };
println!("point p is at ({}, {})", p.x, p.y);
// point p is at (3.0, 4.0)
let p2 = Point { x: 5.0, y: 10.0 };
let myline = Line { start: p, end: p2 };
println!("Line from ({}, {}) to ({}, {})",
myline.start.x, myline.start.y,
myline.end.x, myline.end.y);
// Line from (3.0, 4.0) to (5.0, 10.0)
}
Fields are accessed with dot notation. Note that once p is used as start: p, ownership moves to myline — p can no longer be used directly.
Struct Update Syntax
If you want to create a new struct based on an existing one, updating only some fields:
let p3 = Point { x: 7.0, ..p2 }; // keeps p2.y = 10.0, changes x to 7.0
Tuple Structs
Rust also supports tuple structs, which are structs with unnamed fields:
struct Color(u8, u8, u8); // RGB
let red = Color(255, 0, 0);
println!("Red channel: {}", red.0); // access by index
Enums
An enum (enumeration) defines a type that can be one of several variants. Unlike C enums which are just integers, Rust enums can carry data with each variant.
Basic Enum
enum Direction {
North,
South,
East,
West,
}
let heading = Direction::North;
By default, enum variants start at 0 and increment, but Rust enums are far more powerful than this.
Enums with Data
Enum variants can carry data in tuple format or struct format:
enum Color {
Red,
Green,
Blue,
RGBColor(u8, u8, u8), // tuple variant
RGBAColor { red: u8, green: u8, blue: u8, alpha: u8 }, // struct variant
}
Matching on Enums
The match statement is the primary way to work with enums:
fn enums() {
let c: Color = Color::RGBColor(128, 0, 255);
match c {
Color::Red => println!("Red"),
Color::Green => println!("Green"),
Color::Blue => println!("Blue"),
Color::RGBColor(0, 0, 0) => println!("Black"),
Color::RGBColor(r, g, b) => println!("rgb({}, {}, {})", r, g, b),
Color::RGBAColor { red: _, green: _, blue: _, alpha: 0 } => println!("transparent"),
_ => () // catch remaining variants, do nothing
}
// rgb(128, 0, 255)
}
The compiler guarantees that every variant is handled. If you add a new variant to the enum, every match in your codebase that does not have a _ wildcard will produce a compile error, guiding you to update all the relevant code.
Enum Variant Memory Layout:
+----------------------------+
| Color::Red | (just a tag)
| Color::RGBColor(r, g, b) | (tag + 3 bytes of data)
| Color::RGBAColor{...} | (tag + 4 bytes of data)
+----------------------------+
The enum takes the size of its largest variant.
Option``
Rust has no null. The absence of a value is represented by the Option<T> enum, defined in the standard library as:
pub enum Option<T> {
None,
Some(T),
}
None: No value is presentSome(T): A value of typeTis present
This forces you to handle the "no value" case explicitly, eliminating null pointer dereferences at compile time.
Using Option
let a: Option<i32> = None;
let b: Option<i32> = Some(10);
println!("{:?}", a); // None
println!("{:?}", b); // Some(10)
Option in a Function
A function that might fail to produce a result returns Option<T>:
fn divide(x: f64, y: f64) -> Option<f64> {
if y != 0.0 {
Some(x / y)
} else {
None
}
}
fn option_example() {
let x = 3.0;
let y = 2.0;
let result: Option<f64> = divide(x, y);
// Using match to handle Option
match result {
Some(z) => println!("{} / {} = {}", x, y, z), // 3.0 / 2.0 = 1.5
None => println!("cannot divide {} by {}", x, y),
}
// Shorthand: if let
if let Some(z) = result {
println!("result = {}", z); // result = 1.5
}
}
Common Option Methods
| Method | Description |
|---|---|
option.unwrap() | Returns the value or panics if None |
option.unwrap_or(default) | Returns the value or a default |
option.is_some() | Returns true if Some |
option.is_none() | Returns true if None |
option.map(f) | Applies a function if Some |
let opt: Option<i32> = Some(5);
let doubled = opt.map(|x| x * 2); // Some(10)
let value = opt.unwrap_or(0); // 5
Arrays
An array is a fixed-length, stack-allocated collection of elements of the same type. The size is part of the type and must be known at compile time:
use std::mem;
fn arrays() {
let mut a: [i32; 5] = [1, 2, 3, 4, 5];
println!("a has {} elements, first is {}", a.len(), a[0]);
a[0] = 321;
println!("a[0] = {}", a[0]); // a[0] = 321
println!("{:?}", a); // [321, 2, 3, 4, 5]
// Array comparison
if a == [321, 2, 3, 4, 5] {
println!("match!");
}
// Initialize all elements to the same value: [1u16; 10]
let b = [1u16; 10];
println!("b took up {} bytes", mem::size_of_val(&b)); // 20 bytes (10 * 2)
// 2D array (matrix)
let mtx: [[f32; 3]; 2] = [
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
];
// Print diagonal elements
for i in 0..mtx.len() {
for j in 0..mtx[i].len() {
if i == j {
println!("mtx[{}][{}] = {}", i, j, mtx[i][j]);
// mtx[0][0] = 1.0
// mtx[1][1] = 5.0
}
}
}
}
Array Memory Layout (stack):
[i32; 5] = 5 * 4 bytes = 20 bytes contiguous on the stack
+----+----+----+----+----+
| 1 | 2 | 3 | 4 | 5 |
+----+----+----+----+----+
Arrays are fast (contiguous memory, cache-friendly) but inflexible (fixed size). When you need a resizable collection, use a vector.
Vectors
A Vec<T> is a dynamically-sized, heap-allocated array. It is one of the most frequently used types in Rust. The vector manages its own memory: it allocates a buffer on the heap, grows it automatically as needed, and frees it when the vector is dropped.
fn vectors() {
let mut a: Vec<i32> = Vec::new();
a.push(1);
a.push(2);
a.push(3);
println!("a = {:?}", a); // a = [1, 2, 3]
// Index access
let idx: usize = 0;
a[idx] = 312;
println!("a[0] = {}", a[idx]); // a[0] = 312
// Safe access with get() — returns Option<&T>
match a.get(10) { // index 10 doesn't exist
Some(x) => println!("a[10] = {}", x),
None => println!("error: no such element"), // this branch runs
}
// Remove and return last element: pop() returns Option<T>
let last = a.pop();
println!("popped: {:?}, remaining: {:?}", last, a);
// popped: Some(3), remaining: [312, 2]
// Drain remaining elements
while let Some(x) = a.pop() {
println!("{}", x);
// 2
// 312
}
}
Vector Initialization
// Empty vector
let v: Vec<i32> = Vec::new();
// With initial elements (macro)
let v = vec![1, 2, 3, 4, 5];
// Filled with a value
let v = vec![0; 10]; // ten zeros
Vector vs. Array Comparison
| Property | Array [T; N] | Vector Vec<T> |
|---|---|---|
| Size | Fixed at compile time | Dynamic at runtime |
| Memory | Stack | Heap |
| Allocation | Automatic | Managed by Vec |
| Performance | Slightly faster | Minor overhead |
| Use case | Known, fixed size | Unknown or growing size |
Summary: Core Data Structures
Data Structure Overview:
+------------------+---------------------------------------------+
| Type | Description |
+------------------+---------------------------------------------+
| struct | Named, grouped fields (like a record) |
| enum | One of several variants, can carry data |
| Option<T> | Some(value) or None (replaces null) |
| [T; N] | Fixed-length stack array |
| Vec<T> | Dynamic heap-backed array |
+------------------+---------------------------------------------+
Conclusion
Rust's core data structures are designed to be both expressive and safe. Structs model real-world entities with named fields and ownership semantics. Enums model alternatives cleanly, and match ensures every variant is handled. Option<T> replaces null entirely, making "missing value" an explicit part of the type system. Arrays are fast, fixed, and stack-allocated. Vectors are flexible, heap-backed, and automatically managed.
Together, these types cover the vast majority of data modeling needs in systems programming, and they compose naturally with Rust's ownership and borrowing system to produce correct, efficient code.