Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust Primitives

This chapter is for the most basic Rust mental models:

  • what a type is
  • what a value is
  • what actually takes memory
  • why User, &User, and &mut User mean different things

Types vs Values

A type is a description. A value is the actual thing that exists at runtime.

Example:

#![allow(unused)]
fn main() {
struct User {
    name: String,
}
}

Here:

  • User is a type
  • the type definition itself does not hold runtime data
  • it tells Rust what a User value looks like

Now compare that with:

#![allow(unused)]
fn main() {
let user = User {
    name: "Bobby".to_string(),
};
}

Here:

  • user is a value
  • that value takes memory at runtime

What Actually Takes Memory

The most important rule is:

Values take memory. Types describe values.

So these take memory at runtime:

  • user: User
  • x: i32
  • s: String
  • v: Vec<i32>
  • r: &User
  • p: *const i32

These do not hold runtime data by themselves:

  • User
  • i32
  • String
  • Vec<i32>

Those are types.

A Better Way To Think About It

Rust has:

  • types: descriptions of shape and rules
  • values: actual runtime data
  • operations: ways to move, borrow, mutate, or inspect those values

So it is not correct to say:

Only types take memory.

It is closer to the opposite:

Values take memory, and Rust types tell the compiler how those values are laid out and how they can be used.

User vs &User vs &mut User

These are different value forms with different ownership meanings.

user: User

#![allow(unused)]
fn main() {
fn print_user(user: User) {
    println!("{}", user.name);
}
}

This means:

  • the function takes ownership of the User
  • the value is moved into the function
  • the caller cannot use it afterward unless it is returned

user: &User

#![allow(unused)]
fn main() {
fn print_user(user: &User) {
    println!("{}", user.name);
}
}

This means:

  • the function borrows the User
  • the caller keeps ownership
  • the function can read from it
  • the function does not consume it

user: &mut User

#![allow(unused)]
fn main() {
fn deactivate(user: &mut User) {
    user.active = false;
}
}

This means:

  • the function borrows the User mutably
  • the caller still owns it
  • the function may modify it
  • while that mutable borrow exists, no competing borrows are allowed

Example

struct User {
    name: String,
    active: bool,
}

fn by_value(user: User) {
    println!("{}", user.name);
}

fn by_ref(user: &User) {
    println!("{}", user.name);
}

fn by_mut_ref(user: &mut User) {
    user.active = false;
}

fn main() {
    let mut user = User {
        name: "Bobby".to_string(),
        active: true,
    };

    by_ref(&user);
    println!("{}", user.name);

    by_mut_ref(&mut user);
    println!("{}", user.active);

    by_value(user);
    // user is moved here
}

Why References Also Take Memory

A reference is also a value. It is a value that points to another value.

Example:

fn main() {
    let x = 5;
    let r = &x;
}

Here:

  • x is a value of type i32
  • r is a value of type &i32
  • r also takes memory because it stores a reference to x

So references are not "just syntax." They are real values with meaning in the program.

Heap vs Stack Intuition

Very roughly:

  • simple fixed-size values often live directly in stack frames
  • String and Vec store a small control structure directly, and their dynamic contents live on the heap

Example:

#![allow(unused)]
fn main() {
let n = 5;
let s = String::from("rust");
}
  • n is a small integer value
  • s is a String value
  • the String value itself stores metadata
  • the actual text bytes are stored elsewhere on the heap

Tuples and Tuple Patterns

Tuple types in Rust are written structurally.

Example:

#![allow(unused)]
fn main() {
let pair: (i32, i32) = (19, 34);
}

Here:

  • pair is a value
  • (i32, i32) is the tuple type
  • (19, 34) is the tuple value

There is no built-in type named tuple. Instead, the tuple type is written directly by listing the element types in parentheses.

Tuple Matching

You can match tuples using tuple patterns:

fn main() {
    let pair: (i32, i32) = (19, 34);

    match pair {
        (0, 0) => println!("origin"),
        (a, b) => println!("{a}, {b}"),
    }
}

The key idea is:

  • (0, 0) is a specific tuple pattern
  • (a, b) is a general tuple pattern

That final arm is general for the tuple type being matched. It means:

match any value of type (i32, i32) and bind its two components to a and b

So in a tuple match, a pattern like (a, b) often acts as the tuple-shaped catch-all arm.

This is why the compiler sees it as exhaustive:

  • the matched type is a 2-tuple
  • (a, b) matches any value of that tuple type

That does not mean it matches "any tuple type" in all contexts. It means it matches any value of the tuple type currently being matched.

Short Mental Model

  • Type = description
  • Value = runtime thing
  • Reference = value that points to another value
  • Ownership = who controls the value
  • Borrowing = temporary access without transfer of ownership

Short Interview Answer

The core distinction is that types do not hold runtime data; values of those types do. Rust types describe layout and rules, while ownership, borrowing, and references describe how those values are accessed and moved through memory.