Types & Memory: How Rust Thinks About Data 🧱

Rust is obsessed with knowing exactly how big things are and where they live in memory. This obsession is what makes Rust fast and safe.

🏗️ The Two Places Data Lives

Think of memory like a restaurant:

  • Stack = The counter (fast, organized, fixed-size orders)
  • Heap = The dining room (slower, flexible, any-size parties)

Stack Data (Fast & Simple)

#![allow(unused)]
fn main() {
let x: i32 = 42;        // 4 bytes, lives on stack
let y: bool = true;     // 1 byte, lives on stack
let point = (10, 20);   // 8 bytes, lives on stack
}

Heap Data (Flexible but Slower)

#![allow(unused)]
fn main() {
let s = String::from("hello");  // Data on heap, pointer on stack
let v = vec![1, 2, 3];         // Data on heap, pointer on stack
let b = Box::new(42);          // 42 on heap, Box pointer on stack
}

📊 Common Types Quick Reference

TypeSizeStack/HeapExampleWhat It's For
i32, u324 bytesStack42Integers
i64, u648 bytesStack1_000_000Large integers
f32, f644/8 bytesStack3.14Decimals
bool1 byteStacktrueTrue/false
char4 bytesStack'a'Single Unicode character
&strPointerStack"hello"String slice (borrowed)
StringPointerHeapString::from("hi")Owned string
Vec<T>PointerHeapvec![1,2,3]Dynamic array
[T; N]N × size of TStack[1, 2, 3]Fixed array
Option<T>T + 1 byteDepends on TSome(42)Maybe value
Result<T,E>Larger of T/E + tagDependsOk(42)Success or error

🔍 Recognizing Type Patterns

Pattern 1: Number Types

#![allow(unused)]
fn main() {
let a = 42;         // i32 by default
let b = 42i64;      // Explicitly i64
let c = 42_u8;      // Explicitly u8 (unsigned, 0-255)
let d = 3.14;       // f64 by default
let e = 3.14f32;    // Explicitly f32
}

Quick Rule: i = signed integer, u = unsigned, f = float

Pattern 2: Strings - The Two Kinds

#![allow(unused)]
fn main() {
let s1: &str = "I'm borrowed";           // String slice, fixed
let s2: String = String::from("I'm owned"); // Owned, can grow

// Converting between them
let owned = s1.to_string();              // &str -> String
let borrowed = &s2;                      // String -> &str (auto)
let borrowed2 = s2.as_str();            // String -> &str (explicit)
}

Think of it as:

  • &str = Looking at someone else's text
  • String = Having your own notebook

Pattern 3: Collections

#![allow(unused)]
fn main() {
// Arrays - fixed size, stack
let arr: [i32; 3] = [1, 2, 3];           // Exactly 3 elements

// Vectors - dynamic size, heap
let mut vec: Vec<i32> = vec![1, 2, 3];   // Can grow
vec.push(4);                             // Now has 4 elements

// Slices - borrowed view
let slice: &[i32] = &vec[1..3];          // Borrows elements 1 and 2
}

🎯 Type Annotations: When Rust Needs Help

Usually Rust figures out types, but sometimes you need to help:

#![allow(unused)]
fn main() {
// Rust can't guess
let numbers: Vec<i32> = Vec::new();  // What type of Vec?

// Rust can guess
let numbers = vec![1, 2, 3];         // Obviously Vec<i32>

// Collecting needs a hint
let parsed: Vec<i32> = "1,2,3"
    .split(',')
    .map(|s| s.parse().unwrap())
    .collect();  // Need to specify Vec<i32>
}

🎁 Box, Rc, Arc: Smart Pointers

When you see these, think "special heap storage":

TypeWhat It MeansUse Case
Box<T>Single owner on heapLarge data, recursive types
Rc<T>Reference countedMultiple owners, single thread
Arc<T>Atomic ref countedMultiple owners, multiple threads
RefCell<T>Interior mutabilityMutate through immutable reference
#![allow(unused)]
fn main() {
// Box - simple heap allocation
let b = Box::new(5);

// Rc - multiple owners (single thread)
use std::rc::Rc;
let shared = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&shared);  // Not a deep copy!

// Arc - thread-safe multiple owners
use std::sync::Arc;
let shared = Arc::new(data);
}

🔄 Type Conversions

Common Patterns You'll See:

#![allow(unused)]
fn main() {
// String conversions
let s = 42.to_string();              // Anything to String
let n: i32 = "42".parse().unwrap();  // String to number

// as for numeric casts
let a = 42i32;
let b = a as i64;                    // Widen
let c = 300u16 as u8;                // Narrow (truncates to 44!)

// Into/From traits
let s: String = "hello".into();      // &str into String
let s = String::from("hello");       // Same thing

// Dereferencing
let x = Box::new(5);
let y = *x;                          // Dereference to get value
}

🚩 Red Flags in Type Usage

The .unwrap() Bomb

#![allow(unused)]
fn main() {
let n: i32 = user_input.parse().unwrap();  // 💣 Panics on bad input!
}

Better:

#![allow(unused)]
fn main() {
let n: i32 = match user_input.parse() {
    Ok(num) => num,
    Err(_) => 0,  // Default value
};
}

Integer Overflow

#![allow(unused)]
fn main() {
let x: u8 = 255;
let y = x + 1;  // ⚠️ Wraps to 0 in release mode!
}

String Slicing Panics

#![allow(unused)]
fn main() {
let s = "hello";
let slice = &s[0..10];  // 💣 Panics! String is only 5 bytes
}

💡 Memory Patterns to Recognize

Pattern: Builder Pattern

#![allow(unused)]
fn main() {
let s = String::new()
    .push_str("Hello")    // Won't work! push_str returns ()
    
let mut s = String::new();
s.push_str("Hello");      // Correct: mutate in place
s.push_str(" World");
}

Pattern: Capacity Hints

#![allow(unused)]
fn main() {
let mut vec = Vec::with_capacity(100);  // Pre-allocate space
// Avoids reallocation as you add elements
}

Pattern: Slice Arguments

#![allow(unused)]
fn main() {
fn process(data: &[u8]) { }  // Takes any slice

let arr = [1, 2, 3];
let vec = vec![1, 2, 3];
process(&arr);               // Array to slice
process(&vec);               // Vec to slice
}

📏 Size Matters

Rust needs to know sizes at compile time for stack data:

#![allow(unused)]
fn main() {
// This won't compile:
// let arr = [0; n];  // ❌ n must be known at compile time

// Use Vec for runtime-sized collections:
let vec = vec![0; n];  // ✅ n can be a variable
}

🎯 Quick Recognition: Stack vs Heap

See ThisIt Means
String, Vec<T>, HashMap<K,V>Heap allocated
i32, f64, bool, [T; N]Stack allocated
Box<T>, Rc<T>, Arc<T>Heap with smart pointer
&T, &mut TBorrowing (wherever T lives)

✅ Quick Check

Can you spot the difference?

#![allow(unused)]
fn main() {
let a = [1, 2, 3];        // Array: fixed size, stack
let b = vec![1, 2, 3];    // Vector: dynamic, heap
let c = &[1, 2, 3];       // Slice: borrowed view
let d = "hello";          // &str: string slice
let e = String::from("hello"); // String: owned, heap
}

If yes, you understand Rust's type system!


Next: Pattern Matching - Rust's Swiss Army knife for control flow →