Compound Types
A compound type groups multiple values into a single composite value. While scalar types (i32, f64, bool, char) represent a single atomic value, Rust has two built-in primitive compound types: tuples ((T1, T2, ...)) and arrays ([T; N]).
Scalar vs Primitive Compound Types
Rust Primitive Types
|
+-------------------+-------------------+
| |
v v
Scalar Types Compound Types
(Single value in slot) (Multiple values grouped)
- Integers (i32, u64, ...) - Tuples: (i32, f64, char)
- Floats (f32, f64) [Fixed length, heterogeneous types]
- Booleans (bool) - Arrays: [i32; 4]
- Characters (char) [Fixed length, homogeneous type]
Tuples vs Arrays at a Glance
Both tuples and arrays have a fixed length known at compile time and are stored directly inline on the stack (without heap allocation), but they differ in element typing and how elements are accessed:
| Feature | Tuple (T1, T2, ...) |
Array [T; N] |
|---|---|---|
| Element types | Heterogeneous (each element can have a different type) | Homogeneous (every element must have the exact same type T) |
| Length | Fixed at compile time (part of the type) | Fixed at compile time (N is part of the type [T; N]) |
| Type syntax | (i32, f64, bool) |
[i32; 3] |
| Value syntax | (42, 3.14, true) |
[10, 20, 30] or repeat [0; 3] |
| Element access | Dot-index syntax (tuple.0, tuple.1) — compile-time index only |
Bracket-index syntax (array[i]) — supports runtime usize index i |
Iteration (for x in ...) |
Not iterable (because elements have different types) | Iterable (for x in array) |
| Empty (zero-element) form | Unit type () (size 0 bytes) |
Empty array [T; 0] (size 0 bytes) |
fn main() {
// 1. Tuple: groups heterogeneous types together
let user_tuple: (u32, &str, bool) = (101, "ferris", true);
// 2. Array: groups homogeneous elements of the same type together
let scores_array: [i32; 4] = [88, 92, 79, 95];
println!("Tuple elements: id={}, name={}, active={}", user_tuple.0, user_tuple.1, user_tuple.2);
println!("Array elements: first={}, last={}, all={scores_array:?}", scores_array[0], scores_array[3]);
}
Tuple elements: id=101, name=ferris, active=true
Array elements: first=88, last=95, all=[88, 92, 79, 95]
Contiguous Stack Memory Layout
Neither a tuple nor an array allocates a separate buffer on the heap (unless one of its elements is itself a heap-owning type such as String or Vec<T>). Their elements live directly inline inside the variable's stack frame:
Stack Layout of `[u16; 4] = [10, 20, 30, 40]` (8 bytes total, contiguous):
+--------+--------+--------+--------+
| 10 | 20 | 30 | 40 |
+--------+--------+--------+--------+
[0] [1] [2] [3]
Stack Layout of `(u32, u16, u8, bool)` (8 bytes total):
+-----------------+--------+----+------+
| u32 | u16 | u8 | bool |
| (4 bytes) | (2 b) |(1b)| (1b) |
+-----------------+--------+----+------+
use std::mem::size_of_val;
fn main() {
let arr: [u16; 4] = [10, 20, 30, 40];
let tup: (u32, u16, u8, bool) = (1000, 200, 30, true);
println!("Size of [u16; 4]: {} bytes", size_of_val(&arr));
println!("Size of (u32, u16, u8, bool): {} bytes", size_of_val(&tup));
}
Size of [u16; 4]: 8 bytes
Size of (u32, u16, u8, bool): 8 bytes
Note: For arrays
[T; N], the total size in bytes is always strictlysize_of::<T>() * N, and the elements are always laid out in declaration order[0], [1], ..., [N - 1]. For tuples(T1, T2, ...), the compiler may insert alignment padding bytes between fields of different sizes and may reorder tuple fields in memory to minimize padding waste.
Mutability of Compound Types
In Rust, mutability is an all-or-nothing property of the binding that owns the compound value:
- If a tuple or array is declared with
let(immutable), none of its individual elements can be modified. - If a tuple or array is declared with
let mut, any of its individual elements can be modified in place (tup.0 = ...orarr[i] = ...).
fn main() {
let mut point: (i32, i32) = (0, 0);
point.0 = 10;
point.1 = -5;
let mut rgb: [u8; 3] = [0, 0, 0];
rgb[0] = 255;
println!("point = {point:?}, rgb = {rgb:?}");
}
point = (10, -5), rgb = [255, 0, 0]
Nested Compound Types
Compound types compose freely: you can create an array of tuples ([(i32, i32); 3]), a tuple of arrays (([u8; 4], [u8; 4])), or a 2D matrix array of arrays ([[i32; 3]; 2]):
fn main() {
// Array of 3 coordinate tuples:
let waypoints: [(&str, f64, f64); 2] = [
("Start", 23.62, 90.50),
("Goal", 23.81, 90.41),
];
for (name, lat, lon) in waypoints {
println!("{name}: ({lat}, {lon})");
}
// 2x3 matrix (an array of 2 rows, where each row is `[i32; 3]`):
let matrix: [[i32; 3]; 2] = [
[1, 2, 3],
[4, 5, 6],
];
println!("Row 1, Col 2 = {}", matrix[1][2]);
}
Start: (23.62, 90.5)
Goal: (23.81, 90.41)
Row 1, Col 2 = 6
Trait Implementations (Copy, Clone, Debug, PartialEq)
A tuple or array automatically implements standard traits whenever all of its element types implement that trait:
- If every element type is
Copy(such asi32,f64,bool,char), then the entire tuple or array is alsoCopyand is copied implicitly on assignment. - If any element type is not
Copy(such asString), then the entire tuple or array uses move semantics.
fn main() {
let copyable_pair: (i32, bool) = (42, true);
let second_copy = copyable_pair; // Copied implicitly because both `i32` and `bool` are `Copy`
println!("Both valid: {copyable_pair:?} and {second_copy:?}");
let owned_pair: (i32, String) = (42, String::from("heap text"));
let moved_pair = owned_pair; // Moved because `String` is not `Copy`
println!("Moved pair: {moved_pair:?}");
}
Both valid: (42, true) and (42, true)
Moved pair: (42, "heap text")
Beyond Primitive Compound Types
When you need named fields, named variants, or a dynamically resizable collection, Rust provides higher-level compound types built on top of these primitives:
| Need | Construct |
|---|---|
Named fields instead of .0, .1 |
struct (custom product type) |
| One of several possible variants | enum (custom sum type) |
| Borrowed view into a contiguous sequence of unknown compile-time length | Slice (&[T] / &mut [T]) |
| Growable or shrinkable sequence on the heap | Vec<T> |