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

🦀 A 13-day walkthrough

The Rust Book, simplified.

A beginner-friendly retelling of the official Rust Book - word-to-word, code-to-code, everything important in less time. Runnable examples, bite-sized chapters, and zero fluff.

Progress 11 of 13 days complete

The curriculum

Coming soon

Day 12 Ch. 12
I/O Project

A small CLI that ties everything together.

Day 13 Ch. 13
Iterators & Closures

Functional Rust - lazy, expressive, surprisingly fast.

How to use this book

Work through the days in order - each builds on the last. Every code block with a Run button can be executed right in the browser via the Rust Playground, so you can tweak examples without leaving the page.

If you're coming from another language, spend extra time on Days 4–5 (Ownership) - it's the idea that makes everything else click. Chapter 11 (Writing Automated Tests) is not covered in this series; refer to the official Rust Book for that chapter.

Contributing

Spotted a typo, or want to help ship Day 12 and beyond? PRs and issues welcome on GitHub.

The Rust Programming Language

  • Authors: Steve Klabnik, Carol Nichols, Chris Krycho, and with help from the Rust community.

  • The part that will be covered here on day 1 from the official rust book is:

Screenshot 2025-10-14 at 4 16 33 PM

About This Edition

  • The book assumes, one is using Rust 1.85.0 or later, using the 2024 edition.

  • You set this in your Cargo.toml as:

    edition = "2024"
    

Foreword

Rust is about empowerment - helping any programmer write reliable and efficient code confidently.

Traditional Systems Programming (like C/C++)

Traditionally, systems programming involves manual memory management (you have to manually allocate and free memory for data), concurrency issues (problems that happen when multiple parts of a program run at the same time), and hard-to-detect bugs (errors that are tricky to find). Only a few experts could handle it safely, and even then, crashes or vulnerabilities were common.

Memory management is how a computer keeps track of data your program uses, allocates space for it, and cleans it up when it’s no longer needed. Concurrency is when a program does multiple things at the same time. It can speed things up but requires coordination to avoid mistakes.

Rust’s Promise

Rust eliminates most of these traditional pitfalls by:

  • Preventing memory and concurrency errors at compile time (the compiler checks for mistakes before the program runs).
  • Giving you low-level control over performance (you can manage resources like memory and CPU directly), but without the danger.

You can now “dip down” into low-level control safely - with no crashes, no security holes, and no headaches.

For Experienced Low-Level Developers

Rust allows you to go even further:

  • Introduce parallelism safely - the compiler guarantees thread safety (threads are parts of a program that can run simultaneously, and Rust ensures they don’t interfere with each other).
  • Optimize aggressively - without fear of introducing new bugs.

Not Just Low-Level

Rust is also ideal for:

  • Command-line applications (programs run from terminal/console)
  • Web servers (software that delivers web pages)
  • Embedded systems (small computers inside devices like watches, sensors, etc.)

The Spirit of Rust

Rust is not just a programming language; it is a tool that helps you become a more confident, fearless, and efficient programmer.


Introduction

This is the same text as The Rust Programming Language published by No Starch Press. Rust is designed to help you write fast and reliable software. It combines:

  • High-level ergonomics (easy and convenient to write)
  • Low-level control (efficient and powerful)

Who Rust Is For

Teams of Developers

  • The Rust compiler catches subtle bugs (especially concurrency issues) that might otherwise require complex testing.

  • Rust’s tools make collaboration productive:

    • Cargo – dependency and build manager (manages libraries and project builds)
    • Rustfmt – code formatter (automatically formats your code neatly)
    • rust-analyzer – IDE assistance (helps with auto-completion, hints, and error checking)

Students

  • Excellent for learning memory management, concurrency, and systems-level concepts.
  • The community is welcoming and the book makes hard concepts accessible.

Companies

Rust is used across many industries for:

  • Command-line tools
  • Web services
  • Embedded and IoT devices (Internet of Things, devices connected to the internet)
  • Cryptocurrencies(Solana)
  • Machine learning
  • Even parts of Firefox are written in Rust.

Open Source Developers

Rust invites contributions to the language, compiler, libraries, and tooling.

People Who Value Speed and Stability

Rust provides fast code and safe abstractions (high-level constructs that are safe to use but don’t slow down the program). It avoids “brittle legacy” issues and delivers zero-cost abstractions - high-level code that performs as fast as low-level implementations.

Rust’s Goal

Rust removes the trade-off between safety and speed, productivity and control. You can have both.


1. Getting Started


1.1 Installation

Rust’s official installer and version manager is called rustup. It is used to:

  • Install Rust
  • Update Rust
  • Manage toolchains (different versions of Rust for different projects)
  • Access documentation

Install Rust by running in your terminal:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Follow the on-screen instructions. It installs:

  • rustup – toolchain manager
  • cargo – package manager and build system

Verifying Installation

Restart your terminal and check:

rustc --version

If you see something like:

rustc 1.85.0 (or newer)

Your installation is complete.

Windows Users

Rust works on Linux, macOS, and Windows. On Windows, install Visual Studio Build Tools. If prompted during installation, choose “Yes” to install them.

Keeping Rust Updated

Rust releases updates every 6 weeks. To update:

rustup update

Uninstalling Rust

If you ever need to remove it:

rustup self uninstall

Local Documentation

The installation of Rust also includes a local copy of the documentation so that you can read it offline. Run:

rustup doc

This opens the documentation in your web browser from local files.

Whenever you’re unsure about a type or function from the standard library, consult the API documentation to learn how it works.


Text Editors and Integrated Development Environments (IDEs)

Rust does not assume any particular text editor. You can use any editor, but modern IDEs provide powerful support.

  • Visual Studio Code (with rust-analyzer)
  • IntelliJ IDEA (Rust plugin)
  • Vim/Neovim (with rust-analyzer LSP - Language Server Protocol, gives real-time feedback)
  • Emacs (rust-mode or LSP)
  • Sublime Text, Atom, etc.

Rust maintains a current list on its tools page.

What rust-analyzer Provides

  • Auto-completion (suggests code automatically)
  • Inline error checking (shows errors as you type)
  • Go-to-definition (jump to function or variable definition)
  • Type hints and inline documentation (helps understand types)
  • Real-time feedback from the compiler

Working Offline with This Book

Many examples in this book use crates beyond the standard library. (A crate is a package of Rust code, like a library in Python or Node.js.) To work through them offline, download dependencies ahead of time.

Run:

cargo new get-dependencies
cd get-dependencies
cargo add rand@0.8.5 trpl@0.2.0

This caches the crates so you won’t need the internet later. Once done, you can delete the project:

cd ..
rm -rf get-dependencies

When working offline, use:

cargo run --offline

or

cargo build --offline

This instructs Cargo to use cached crates only.

Summary Table

TaskCommandDescription
Open docs locallyrustup docOpens local documentation in the browser
Create dummy projectcargo new get-dependenciesPre-caches dependencies
Add cratescargo add rand@0.8.5 trpl@0.2.0Downloads example crates
Work offlinecargo run --offlineBuilds using cached dependencies only

1.2 Hello, World!

Now that Rust is installed, you can write your first Rust program.

Step 1: Create a File

mkdir hello_world
cd hello_world
touch main.rs

Step 2: Write the Code

In main.rs:

fn main() {
    println!("Hello, world!");
}

Step 3: Run It

Compile and execute:

rustc main.rs
./main     # macOS/Linux
main.exe   # Windows

Output:

Hello, world!

Explanation

fn main() {
  • fn defines a function.
  • main is the entry point of the program (where execution starts).
  • () means no parameters.
  • {} marks the function body.

Inside:

#![allow(unused)]
fn main() {
println!("Hello, world!");
}
  • println! is a macro (special code that generates code, indicated by !)
  • Prints text to the console
  • "Hello, world!" is a string literal (text enclosed in quotes)

Rust compiles directly to machine code - no interpreter involved.


1.3 Hello, Cargo!

Instead of using rustc manually, Rust provides Cargo, a powerful build system and package manager.

What is Cargo?

Cargo is Rust’s:

  • Package Manager (like npm or pip, manages libraries)
  • Build Tool (like make, compiles the program)
  • Project Manager (organizes the files and dependencies)

It handles dependencies, compilation, documentation, and testing.

Creating a Project with Cargo

Run:

cargo new hello_cargo
cd hello_cargo

Structure created:

hello_cargo/
├── Cargo.toml
└── src/
    └── main.rs

Cargo.toml

Configuration file written in TOML (Tom’s Obvious, Minimal Language, similar to INI files).

[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2024"

[dependencies]

Defines:

  • Package name
  • Version
  • Edition
  • Dependencies (external libraries)

src/main.rs

Automatically created with:

fn main() {
    println!("Hello, world!");
}

Building and Running with Cargo

Build:

cargo build

Run:

cargo run

Check code (faster, without building) :

cargo check

All builds are stored in the target/ directory.


Building for Release

For optimized builds (faster, smaller binary):

cargo build --release

Output is placed in:

target/release/

The release build runs faster but takes longer to compile.


Summary of Commands

TaskCommandDescription
Install RustrustupInstalls & manages Rust toolchains
Check versionrustc --versionConfirms Rust installation
Update Rustrustup updateUpdates to latest version
Run manuallyrustc main.rsCompiles to a binary
Create projectcargo new project_nameSets up Cargo project
Build projectcargo buildCompiles debug version
Run projectcargo runBuilds and runs the app
Check errorscargo checkChecks for errors quickly
Optimize buildcargo build --releaseProduces production binary

✅ Congrats, pat your back, next part is gonna be more amazing, up to this point, you’ve completed:

  • The Foreword

  • The Introduction

  • Chapter 1 → Getting Started, including:

    • 1.1 Installation
    • 1.2 Hello, World!
    • 1.3 Hello, Cargo!

The part that will be covered here on day 2 from the official rust book is:

Screenshot 2025-10-15 at 6 38 25 PM

Programming a Guessing Game

This is Rust’s first real project. By the end of this chapter, you’ll build a simple command-line Guessing Game that:

  1. Picks a random number.
  2. Asks the user to guess it.
  3. Tells you if your guess is too low, too high, or correct.

You’ll use:

  • Variables
  • Loops
  • Crates (external packages)
  • Error handling
  • Pattern matching (match)
  • Type conversion

So you’ll touch almost every fundamental Rust concept in one short program.


Step 1: Creating a New Project

(Please code along and push it on your GitHub; you can even reply to my tweet on X with the repository link.)

Use Cargo to start a new project:

cargo new guessing_game
cd guessing_game

This gives you a folder like:

guessing_game/
├── Cargo.toml
└── src/
    └── main.rs

By default, main.rs has this:

fn main() {
    println!("Hello, world!");
}

Let’s run it to make sure everything’s okay:

cargo run

Output:

Hello, world!

Great - now we’ll replace that with real code.


Step 2: Getting User Input

We want to ask the player for their guess. To read input from the terminal, we’ll use Rust’s standard library (std).

Code

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Explanation

1. use std::io;

This brings Rust’s I/O library (input/output) into scope. You’ll use it to read from the keyboard.


2. let mut guess = String::new();

  • let → declares a variable.
  • mut → makes it mutable (changeable).
  • String::new() → creates a new, empty string.

Without mut, we can’t modify guess later.


3. io::stdin().read_line(&mut guess)

  • io::stdin() → gets a handle to standard input (keyboard).
  • .read_line(&mut guess) → reads a line and appends it to guess.
  • &mut guess → passes a mutable reference, allowing the function to modify it.

This method returns a Result type - it can succeed or fail. That’s why we handle it with .expect() next.


4. .expect("Failed to read line");

This is error handling. If reading input fails, the program will crash with that message.


5. println!("You guessed: {guess}");

Prints the value of guess. In Rust 1.58+, you can use {guess} directly instead of {} and providing the variable after.


Summary So Far

  • Took user input.
  • Stored it in a mutable string.
  • Printed it back to the user.

You’ve used:

  • use (import modules)
  • let mut (mutable variables)
  • String::new()
  • read_line()
  • expect()
  • println!()

Step 3: Generating a Secret Number

Now we’ll generate a random number between 1 and 100.

Rust’s standard library doesn’t include random number generation - you get that via an external crate called rand.

Step 3.1: Add rand to Your Dependencies

Open your Cargo.toml and add this line under [dependencies]:

rand = "0.8.5"

Then save it and run:

cargo build

Cargo will download and compile the crate.


Step 3.2: Use rand in Your Code

use std::io;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");
}

Explanation

1. use rand::Rng;

The Rng trait provides methods like .gen_range() for generating random numbers. You must import a trait before using its methods.


2. rand::thread_rng()

Gives a random number generator local to the current thread and automatically seeded.


3. .gen_range(1..=100)

Generates a number between 1 and 100 inclusive. 1..=100 is a range expression:

  • 1..100 → 1 to 99
  • 1..=100 → 1 through 100 (inclusive)

Step 4: Comparing the Guess and Secret Number

Now we’ll get user input, convert it to a number, and compare it to the secret number.

Code

use std::io;
use rand::Rng;
use std::cmp::Ordering;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

Explanation

1. use std::cmp::Ordering;

The Ordering enum has three variants:

#![allow(unused)]
fn main() {
Ordering::Less
Ordering::Greater
Ordering::Equal
}

2. let guess: u32 = guess.trim().parse().expect("Please type a number!");

Step by step:

  • guess was a String.
  • trim() removes whitespace and newlines.
  • parse() converts it into another type - here, a u32.
  • expect() handles errors for invalid input.

3. match guess.cmp(&secret_number)

The cmp() method compares two values and returns an Ordering. We then pattern match it:

#![allow(unused)]
fn main() {
match guess.cmp(&secret_number) {
    Ordering::Less => println!("Too small!"),
    Ordering::Greater => println!("Too big!"),
    Ordering::Equal => println!("You win!"),
}
}

Step 5: Adding a Loop

We’ll wrap the main logic in a loop so the user can keep guessing.

Code

use std::io;
use rand::Rng;
use std::cmp::Ordering;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

Explanation

  1. loop { ... } runs indefinitely until break is used.
  2. The match on parse() ensures invalid input doesn’t crash the program:
#![allow(unused)]
fn main() {
let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};
}

If parsing fails, continue restarts the loop.

  1. break exits the loop when the player wins.

Step 6: Final Version

use std::io;
use rand::Rng;
use std::cmp::Ordering;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

Concepts You Just Learned

ConceptExplanation
use std::io;Importing from the standard library
mutMutable variable
String::new()Create a new, empty string
read_line()Read input from terminal
expect()Handle potential errors
rand::thread_rng()Get random number generator
.gen_range(1..=100)Generate a random number in range
cmp()Compare two numbers
OrderingEnum for comparison results
matchPattern matching (branching logic)
loop, break, continueControl flow tools
Result & Ok/ErrError handling system
trim(), parse()String to number conversion

Summary

Pat your back, you’ve just built a complete command-line game with:

  • Random number generation
  • Robust input handling
  • Pattern matching for game logic
  • Safe error management via Rust’s type system

This single program demonstrates why Rust is both safe and powerful.

Chapter 3 - Common Programming Concepts (Functions and Control Flow)

This chapter introduces core concepts that are present in nearly every programming language, explained through Rust’s syntax and semantics. Even though these concepts aren’t unique to Rust, understanding how Rust implements them helps you write safe, efficient, and reliable code. We will cover:

Screenshot 2025-10-16 at 8 22 00 PM

Keywords

Rust has a set of reserved keywords - special words that the compiler uses for specific tasks. You cannot use them as variable names or function identifiers.

Example keywords include:

fn, let, mut, const, if, else, loop, match, while, for, impl, trait, pub, use

A few are reserved for future use (they have no functionality yet).

Full list: Appendix A in the Rust Book


Variables and Mutability

By default, variables in Rust are immutable. Once you assign a value, you cannot change it and this is a feature, not a restriction.

Rust’s design encourages immutability to ensure safety and concurrency.

However, when needed, you can make a variable mutable using the keyword mut.


Immutable Example (This will fail to compile)

Filename: src/main.rs

fn main() {
    let x = 5;
    println!("The value of x is: {x}");
    x = 6; // Error: trying to reassign an immutable variable
    println!("The value of x is: {x}");
}

Run:

cargo run

Output:

error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         - first assignment to `x`
3 |     println!("The value of x is: {x}");
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut x = 5;
  |         +++

Explanation: Rust is telling you that you cannot reassign to x because it was declared without mut.

This compile-time safety prevents potential bugs where one part of the code assumes a value never changes, but another part changes it unexpectedly.


Mutable Example

Filename: src/main.rs

fn main() {
    let mut x = 5;
    println!("The value of x is: {x}");
    x = 6; // Allowed
    println!("The value of x is: {x}");
}

Output:

The value of x is: 5
The value of x is: 6

Tip: Use mut sparingly. Prefer immutability unless you explicitly need to mutate - it improves safety and readability.


Constants

Constants are always immutable, even more strictly than variables.

  • Declared using const
  • Must have a type annotation
  • Value must be known at compile time
  • Can be declared in any scope, even global

Example:

#![allow(unused)]
fn main() {
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
}

Explanation:

  • THREE_HOURS_IN_SECONDS is a constant expression.
  • Constants follow ALL_CAPS_WITH_UNDERSCORES naming.
  • The compiler evaluates constant expressions at compile time, ensuring performance and predictability.

Why Constants Matter

  • Used for configuration values (e.g., max limits, default sizes)
  • Stay in memory throughout program execution
  • Improve readability and maintainability

Example:

#![allow(unused)]
fn main() {
const MAX_POINTS: u32 = 100_000;
}

If you ever want to change it later - you change it once.


Shadowing

Rust allows redefining a variable with the same name - this is called shadowing.

This is not mutation. It’s creating a new variable that overrides the previous one in scope.


Example:

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

    println!("The value of x is: {x}");
}

Output:

The value of x in the inner scope is: 12
The value of x is: 6

How this works:

  • First x = 5
  • Second x shadows it = 6
  • Inner scope shadows it again = 12
  • After inner scope ends → back to x = 6

Shadowing vs mut

FeatureShadowingmut
Creates new variable
Allows type change
Same variable reused
Requires let keyword

Example of type change via shadowing:

#![allow(unused)]
fn main() {
let spaces = "   ";  // &str
let spaces = spaces.len(); // usize
}

If you tried this with mut:

#![allow(unused)]
fn main() {
let mut spaces = "   ";
spaces = spaces.len(); // Error: mismatched types
}

Data Types

Rust is statically typed - the compiler must know the type of every variable at compile time.

Most of the time, it can infer the type, but sometimes you need type annotations.

Example:

#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}

If you omit the type:

error[E0284]: type annotations needed

Scalar Types

Scalar = single value. Four primary scalar types in Rust:

  1. Integers
  2. Floating-point numbers
  3. Booleans
  4. Characters

1. Integer Types

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
Architecture dependentisizeusize

Signed → can store negative numbers Unsigned → only positive numbers

Example:

#![allow(unused)]
fn main() {
let x: i8 = -128;
let y: u8 = 255;
}

Integer Literals

FormatExample
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte (u8 only)b'A'

Integer Overflow

Example: u8 can hold 0–255. If you try 256, overflow happens.

In debug mode → program panics. In release mode → wraps around (256 → 0, 257 → 1).

Rust gives special methods to handle this safely:

  • wrapping_add
  • checked_add
  • overflowing_add
  • saturating_add

2. Floating-Point Types

  • f32 → 32-bit float
  • f64 → 64-bit float (default)

Example:

fn main() {
    let x = 2.0; // f64
    let y: f32 = 3.0;
}

Follows IEEE-754 standard.


3. Numeric Operations

fn main() {
    let sum = 5 + 10;
    let difference = 95.5 - 4.3;
    let product = 4 * 30;
    let quotient = 56.7 / 32.2;
    let truncated = -5 / 3; // -1
    let remainder = 43 % 5;
}

4. Booleans

fn main() {
    let t = true;
    let f: bool = false; // explicit type
}

Used mainly in conditionals (if, while, etc.).


5. Characters

Rust’s char type represents Unicode scalar values.

fn main() {
    let c = 'z';
    let z: char = 'ℤ';
    let heart_eyed_cat = '😻';
}

Each char is 4 bytes and supports emoji, accented letters, CJK characters, etc.


Compound Types

Rust provides two primitive compound types:

  1. Tuple
  2. Array

Tuples

Tuples group multiple values (of possibly different types).

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

Destructuring:

#![allow(unused)]
fn main() {
let (x, y, z) = tup;
println!("The value of y is: {y}");
}

Access by index:

#![allow(unused)]
fn main() {
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}

Empty tuple → () (unit type). Used when functions return nothing.


Arrays

All elements must have the same type and fixed length.

fn main() {
    let a = [1, 2, 3, 4, 5];
}

Type declaration:

#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}

Initialize all with same value:

#![allow(unused)]
fn main() {
let a = [3; 5]; // [3, 3, 3, 3, 3]
}

Access elements:

#![allow(unused)]
fn main() {
let first = a[0];
let second = a[1];
}

Invalid access → program panics safely:

use std::io;

fn main() {
    let a = [1, 2, 3, 4, 5];
    println!("Please enter an array index.");

    let mut index = String::new();
    io::stdin().read_line(&mut index).expect("Failed to read line");

    let index: usize = index.trim().parse().expect("Not a number");
    let element = a[index];

    println!("The value of the element at index {index} is: {element}");
}

If user enters invalid index:

index out of bounds: the len is 5 but the index is 10

Rust stops execution immediately instead of letting you access invalid memory - this is memory safety in action.


Functions in Rust

Functions in Rust are the building blocks of modularity and code reuse. They allow you to group logic under a name and call it from other parts of the program.

Defining a Function

fn main() {
    println!("Hello, world!");
    another_function();
}

fn another_function() {
    println!("Another function.");
}
  • fn → keyword used to define a function.
  • main() → the entry point of the program (like int main() in C).
  • {} → denotes the function body.
  • You can define functions anywhere in your file - before or after main() - as long as they are visible in scope.

Rust doesn’t care about order of function definitions because it resolves everything at compile time, not runtime.


Function Parameters

Functions can take parameters - variables passed into the function.

Example

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {x}");
}
  • x: i32 → defines a parameter x of type 32-bit integer.
  • Rust requires explicit type annotations for parameters - unlike Python or JavaScript. This makes type inference simpler elsewhere in the program.

Parameter vs Argument

TermMeaning
ParameterVariable name in the function definition (x in fn another_function(x: i32))
ArgumentActual value passed when calling the function (5 in another_function(5))

Multiple Parameters

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

Here, you see:

  • One i32 integer parameter.
  • One char parameter ('h').
  • println! supports placeholders that expand to variable values inside {}.

Statements vs Expressions

Rust makes a sharp distinction between statements and expressions. This distinction is what gives Rust flexibility in functional-style programming.

ConceptDescriptionReturns Value?
StatementPerforms an actionNo
ExpressionEvaluates to a valueYes

Example: Statement

#![allow(unused)]
fn main() {
let y = 6;
}
  • let y = 6; is a statement.
  • Statements do not return values.

You can’t do:

#![allow(unused)]
fn main() {
let x = (let y = 6);
}

because let y = 6 doesn’t return a value - it’s just an action.


Example: Expression

#![allow(unused)]
fn main() {
let y = {
    let x = 3;
    x + 1
};

println!("The value of y is: {y}");
}

Here:

  • { let x = 3; x + 1 } is an expression block.
  • It evaluates to 4 (the result of x + 1).
  • That value gets assigned to y.

⚠️ Important rule: If you put a semicolon ; after an expression, it becomes a statement, meaning it won’t return a value.

Example:

#![allow(unused)]
fn main() {
x + 1;  // now it returns nothing!
}

Functions with Return Values

Rust functions can return values - these are implicit unless you use the return keyword.

Example

fn five() -> i32 {
    5
}

fn main() {
    let x = five();
    println!("The value of x is: {x}");
}
  • -> i32 → specifies return type.
  • The final line (5) has no semicolon, meaning it’s an expression whose value is returned.

Another Example

#![allow(unused)]
fn main() {
fn plus_one(x: i32) -> i32 {
    x + 1
}
}

works fine, but if you add a semicolon:

#![allow(unused)]
fn main() {
fn plus_one(x: i32) -> i32 {
    x + 1;
}
}

you’ll get:

error[E0308]: mismatched types
expected `i32`, found `()`

because now it returns () (the unit type, meaning “nothing”).


Comments

Rust supports single-line and multi-line comments.

#![allow(unused)]
fn main() {
// This is a comment

// Another example:
// Explaining complex logic
}

You can also place comments inline:

#![allow(unused)]
fn main() {
let lucky_number = 7; // I'm feeling lucky today
}

These are ignored by the compiler, purely for readability.


Control Flow - if, else if, else

Basic if Example

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}
  • The condition must be a bool.
  • Rust will not automatically convert integers to booleans (unlike C, JS, etc.).

Example of invalid code:

#![allow(unused)]
fn main() {
if number {
    println!("This won't compile");
}
}

will throw:

expected `bool`, found integer

Multiple Conditions (else if)

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("divisible by 4");
    } else if number % 3 == 0 {
        println!("divisible by 3");
    } else if number % 2 == 0 {
        println!("divisible by 2");
    } else {
        println!("not divisible by 4, 3, or 2");
    }
}

Rust will stop at the first true condition and skip the rest.


if as an Expression

Because if returns a value, you can assign it directly:

#![allow(unused)]
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}

Output → The value of number is: 5

⚠️ The two branches must return the same type.

This will not compile:

#![allow(unused)]
fn main() {
let number = if condition { 5 } else { "six" };
}

because one branch returns an integer, the other a string.


Repetition with Loops

Rust has three looping constructs:

  1. loop
  2. while
  3. for

Infinite loop

#![allow(unused)]
fn main() {
loop {
    println!("again!");
}
}

This will print forever until you press Ctrl + C.

You can exit with:

#![allow(unused)]
fn main() {
break;
}

and skip to next iteration using:

#![allow(unused)]
fn main() {
continue;
}

Returning Values from a Loop

Loops can return values with break.

fn main() {
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;
        }
    };
    println!("The result is {result}");
}

Output → The result is 20


Loop Labels

Used when you have nested loops:

fn main() {
    let mut count = 0;

    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }

    println!("End count = {count}");
}

Output:

count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

while Loops

Runs while a condition is true:

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{number}!");
        number -= 1;
    }

    println!("LIFTOFF!!!");
}

Output:

3!
2!
1!
LIFTOFF!!!

for Loops

Used to iterate through collections like arrays:

fn main() {
    let a = [10, 20, 30, 40, 50];
    for element in a {
        println!("the value is: {element}");
    }
}

This is safer than using a manual index (no risk of going out of bounds).

You can also use ranges:

#![allow(unused)]
fn main() {
for number in (1..4).rev() {
    println!("{number}!");
}
println!("LIFTOFF!!!");
}

Output:

3!
2!
1!
LIFTOFF!!!

Summary

ConceptKey Idea
fnDefines a function
StatementsDo not return values
ExpressionsEvaluate to values
Return ValuesThe final expression (no ;)
ifConditional branching (must be bool)
Loopsloop, while, for
breakExit a loop
continueSkip iteration
CommentsUse // for single line

Now, pat your back for coming this far, this will only become more interesting with time!

Chapter 4 (Part 1)

Understanding Ownership

We are going to cover:

Screenshot 2025-10-17 at 9 04 49 PM

Rust’s ownership system is what makes it stand out from almost every other language. It ensures memory safety without garbage collection, which is why Rust can run both fast and safe.

To master ownership, you must understand:

  1. What makes a program safe or unsafe
  2. How memory works (stack vs. heap)
  3. How ownership, moves, and cloning control memory

1. Safety is the Absence of Undefined Behavior

Rust defines safety as:

“A program is safe if it cannot cause undefined behavior.”

Let’s start with a simple, safe program.

fn read(y: bool) {
    if y {
        println!("y is true!");
    }
}

fn main() {
    let x = true;
    read(x);
}

✅ This compiles and runs fine. x is defined before it’s used.


Now see this unsafe version:

fn read(y: bool) {
    if y {
        println!("y is true!");
    }
}

fn main() {
    read(x); // oh no! x isn't defined!
    let x = true;
}

This fails with:

error[E0425]: cannot find value `x` in this scope
 --> src/main.rs:8:10
  |
8 |     read(x);
  |          ^ not found in this scope

Rust prevents compilation because otherwise, x would be used before it’s defined - which is undefined behavior.

In languages like Python or JavaScript, this would raise a runtime exception (NameError, ReferenceError). But Rust checks this at compile time instead, removing the cost of runtime checks.


Why Undefined Behavior Is Dangerous

If Rust allowed that unsafe program, here’s what would happen (in assembly terms):

Safe version:

main:
    mov     edi, 1  ; put 1 (true) into register edi
    call    read

Unsafe version:

main:
    call    read
    mov     edi, 1  ; this happens too late!

Here, read expects the argument to be in edi, but it’s not set yet - it could contain any garbage value. That’s undefined behavior: the CPU might crash, overwrite memory, or worse.


Rust’s Core Promise

Rust guarantees:

  • No reading uninitialized memory
  • No double frees
  • No invalid pointer dereferences

By enforcing these ownership rules at compile time.


2. Ownership as a Discipline for Memory Safety

Ownership prevents unsafe operations on memory.

Memory is where your program stores its data. It can be thought of as two regions:

Memory TypeManaged byLifetimeExample Data
StackRust automaticallyEnds with scopeintegers, small structs
HeapRust + Ownership systemCan live indefinitelystrings, vectors, boxes

Example - Stack Memory

fn main() {
    let a = 5;
    let mut b = a;
    b += 1;
}

Here:

  • Both a and b live on the stack
  • b gets a copy of a
  • Changing b doesn’t affect a

Stack data is small and cheap to copy.


Example - Heap Memory with Box

Now let’s see what happens when we store a large array.

fn main() {
    let a = [0; 1_000_000];
    let b = a;
}

This copies one million elements - 2 million total! That’s wasteful.

To avoid copying large data, Rust uses pointers via heap allocations.

fn main() {
    let a = Box::new([0; 1_000_000]);
    let b = a;
}

Now:

  • The array lives once, in the heap.
  • Both a and b just hold a pointer.
  • But - ownership moves from a to b. a can no longer be used.

3. Rust Does Not Permit Manual Memory Management

In C or C++, you call malloc() and free(). In Rust, you don’t manually free memory - it’s handled automatically when ownership ends.

Imagine Rust let you do this:

fn free<T>(_t: T) {}

fn main() {
    let b = Box::new([0; 100]);
    free(b);             // manually free memory
    assert!(b[0] == 0);  // use freed memory ❌
}

This is undefined behavior - you’re accessing freed memory. Rust prevents this at compile time.

So, Rust never lets you call free() yourself. It frees memory automatically when a variable’s owner goes out of scope.


4. A Box’s Owner Manages Deallocation

Rust’s (almost correct) deallocation rule:

If a variable owns a box, when its frame ends, the heap memory is freed.

Example:

fn main() {
    let a_num = 4;
    make_and_drop();
}

fn make_and_drop() {
    let a_box = Box::new(5);
}

When make_and_drop() ends:

  • Stack frame for a_box ends
  • Its heap memory (5) is freed automatically

✅ Safe and automatic.


The Problem of Double Free

#![allow(unused)]
fn main() {
let a = Box::new([0; 1_000_000]);
let b = a;
}

If both a and b owned the same memory, Rust would free it twice - undefined behavior.

So Rust’s correct rule is:

If a variable owns heap data, when it’s moved, the previous owner is invalidated.

That’s ownership.


5. Collections and Ownership

Types like Vec, String, and HashMap internally use heap memory - but with ownership.

Example:

fn main() {
    let first = String::from("Ferris");
    let full = add_suffix(first);
    println!("{full}");
}

fn add_suffix(mut name: String) -> String {
    name.push_str(" Jr.");
    name
}

Step by step:

  1. first owns “Ferris” on the heap.
  2. Calling add_suffix(first) moves ownership to name.
  3. name.push_str(" Jr.") modifies the string in place.
  4. Returning name moves ownership to full.

No copies, no leaks, no double frees.


If We Try to Use first After Move

fn main() {
    let first = String::from("Ferris");
    let full = add_suffix(first);
    println!("{full}, originally {first}");
}

Rust errors out:

error[E0382]: borrow of moved value: `first`

Explanation:

  • first was moved into add_suffix
  • String does not implement Copy
  • Using it again is invalid - it no longer owns the data

6. Cloning Avoids Moves

If you need to reuse a variable after moving, use .clone().

fn main() {
    let first = String::from("Ferris");
    let first_clone = first.clone();
    let full = add_suffix(first_clone);
    println!("{full}, originally {first}");
}

fn add_suffix(mut name: String) -> String {
    name.push_str(" Jr.");
    name
}

Here:

  • clone() creates a deep copy of the heap data
  • first and first_clone each own separate heap allocations

✅ Both safe to use.


7. Summary: Ownership in Rust

Ownership is Rust’s secret weapon for memory safety.

RuleMeaning
Each heap value has one ownerOnly one variable controls freeing that memory
When the owner goes out of scope, memory is freedNo leaks, no garbage collector needed
Ownership can moveBut the old owner becomes invalid
Use .clone() to copy heap dataCreates a new owned allocation

By enforcing these at compile time, Rust ensures no undefined behavior due to invalid memory access.


References and Borrowing


Why References Exist

We already know ownership and move semantics keep memory safe - but they can make programs annoyingly restrictive.

For example:

fn main() {
    let m1 = String::from("Hello");
    let m2 = String::from("world");
    greet(m1, m2);
    let s = format!("{} {}", m1, m2); // ❌ Error: moved values
}

fn greet(g1: String, g2: String) {
    println!("{} {}!", g1, g2);
}

Here, ownership of m1 and m2 moves into greet. After that, main can’t use them - they’ve been dropped at the end of greet.

Rust will reject this because you’d be trying to use freed memory.


You could fix it like this:

fn main() {
    let m1 = String::from("Hello");
    let m2 = String::from("world");
    let (m1_again, m2_again) = greet(m1, m2);
    let s = format!("{} {}", m1_again, m2_again);
}

fn greet(g1: String, g2: String) -> (String, String) {
    println!("{} {}!", g1, g2);
    (g1, g2)
}

This returns the ownership back - but it’s ugly and verbose. That’s where references come in.


References: Non-Owning Pointers

We can rewrite the program beautifully:

fn main() {
    let m1 = String::from("Hello");
    let m2 = String::from("world");
    greet(&m1, &m2); // borrow them instead of moving
    let s = format!("{} {}", m1, m2);
}

fn greet(g1: &String, g2: &String) {
    println!("{} {}!", g1, g2);
}
  • &m1 creates a reference (borrow).
  • The parameter type &String means “a reference to a String”, not the String itself.
  • g1 doesn’t own the data - it just borrows it.
  • So when greet ends, m1 and m2 are still valid in main.

This is the foundation of Rust memory safety.


Key Idea

  • m1 owns the heap data "Hello".
  • g1 only points to it temporarily.
  • When greet finishes, nothing gets freed - because g1 doesn’t own it.

That’s why references are called non-owning pointers.


Dereferencing - Accessing Data Behind Pointers

You’ve seen & to borrow. The opposite is * to dereference - to actually use the value behind a pointer.

Example:

fn main() {
    let mut x: Box<i32> = Box::new(1);
    let a: i32 = *x;     // read heap value → a = 1
    *x += 1;             // write heap value → x now points to 2

    let r1: &Box<i32> = &x;  
    let b: i32 = **r1;   // two dereferences to reach heap value

    let r2: &i32 = &*x;  // direct reference to heap value
    let c: i32 = *r2;    // one dereference
}
  • r1 points to the box on the stack → needs **r1 to reach heap data.
  • r2 points directly to heap → only *r2 needed.

Rust often automatically inserts these dereferences and references for you.

So, these are all equivalent:

#![allow(unused)]
fn main() {
let x: Box<i32> = Box::new(-1);
let x_abs1 = i32::abs(*x); // explicit
let x_abs2 = x.abs();      // implicit
assert_eq!(x_abs1, x_abs2);

let r: &Box<i32> = &x;
let r_abs1 = i32::abs(**r);
let r_abs2 = r.abs();
assert_eq!(r_abs1, r_abs2);

let s = String::from("Hello");
let s_len1 = str::len(&s);
let s_len2 = s.len();
assert_eq!(s_len1, s_len2);
}

Rust’s dot syntax (.) automatically dereferences as needed - so method calls feel natural.


Rust’s Core Safety Rule

Pointer Safety Principle: Data should never be aliased and mutated at the same time.

Aliasing = multiple access paths to the same data. Mutation = modifying it. Together = danger (use-after-free, data races, etc.).

Rust’s borrow checker enforces this rule.


Example (Undefined Behavior if Allowed)

fn main() {
    let mut v = vec![1, 2, 3];
    let num: &i32 = &v[2];
    v.push(4);
    println!("Third element is {}", *num);
}
  • num points into the vector’s heap.
  • v.push(4) might reallocate the heap.
  • num becomes a dangling pointer. If Rust didn’t stop you, you’d read invalid memory.

Compiler error:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable

That’s the borrow checker saving you.


Permissions Model (Borrow Checker Logic)

Rust internally tracks permissions for every variable:

PermissionMeaning
RRead
WWrite
OOwn (move/drop)

Normally, variables have R+O. mut adds W.

When you create a reference, permissions shift temporarily.

Example (safe version):

fn main() {
    let mut v = vec![1, 2, 3];
    let num: &i32 = &v[2];
    println!("Third element is {}", *num);
    v.push(4);
}
  1. After borrow &v[2]:

    • v loses W and O (cannot mutate or move).
    • num gains R.
  2. After println!:

    • num no longer used → v regains W and O.
  3. Then v.push(4) works fine.


Places vs Variables

Permissions apply to places, not just variables.

A place = anything assignable:

  • a
  • *a
  • a[0]
  • a.field
  • combinations like (*a)[0].1

So you can mutate through a reference without reassigning the reference itself.

Example:

fn main() {
    let x = 0;
    let mut x_ref = &x;
    println!("{x_ref} {x}");
}
  • x_ref can be reassigned (has W).
  • *x_ref cannot be mutated (no W on pointee).

Mutable References

So far, we had shared (&T) references. Now comes unique (&mut T) references - they allow mutation, but no aliasing.

Example:

fn main() {
    let mut v = vec![1, 2, 3];
    let num: &mut i32 = &mut v[2];
    *num += 1;
    println!("Third element is {}", *num);
    println!("Vector is now {:?}", v);
}

Key observations:

  1. When num exists, v loses all permissions - can’t be used or read.
  2. *num gets R + W - it can read and modify v[2].
  3. After num dies (last use), v regains permissions.

That’s how Rust guarantees safety: No aliases if mutation exists.


Downgrading a Mutable Reference

You can create a shared (&T) reference from a mutable (&mut T) one:

fn main() {
    let mut v = vec![1, 2, 3];
    let num: &mut i32 = &mut v[2];
    let num2: &i32 = &*num;
    println!("{} {}", *num, *num2);
}

The borrow &*num removes write permission from *num, but keeps read. So both can safely read at the same time - no mutation while aliasing.


Reference Lifetimes

A reference’s lifetime = from where it’s created to its last use.

Example:

fn main() {
    let mut x = 1;
    let y = &x;
    let z = *y;
    x += z;
}

After z = *y, y’s lifetime ends → x regains W permission.


Control flow can split lifetimes too:

#![allow(unused)]
fn main() {
fn ascii_capitalize(v: &mut Vec<char>) {
    let c = &v[0];
    if c.is_ascii_lowercase() {
        let up = c.to_ascii_uppercase();
        v[0] = up;
    } else {
        println!("Already capitalized: {:?}", v);
    }
}
}
  • In the if branch, c is used → v regains W only after mutation.
  • In the else branch, c isn’t used → v regains W immediately.

Rust tracks this automatically.


Data Must Outlive References

Rust enforces that referenced data must live longer than the reference itself.

Example:

fn main() {
    let s = String::from("Hello world");
    let s_ref = &s;
    drop(s);
    println!("{}", s_ref); // ❌
}
  • drop(s) tries to free s while s_ref still exists.
  • Borrowing removed O from s, but drop requires it → compile error.

Function-Level References (Flow Permission F)

Inside functions, Rust must ensure input/output references are also safe.

Example:

#![allow(unused)]
fn main() {
fn first(strings: &Vec<String>) -> &String {
    let s_ref = &strings[0];
    s_ref
}
}

Perfectly fine - the reference returned (s_ref) points to data inside the input strings, which outlives it.


Now, consider this:

#![allow(unused)]
fn main() {
fn first_or<'a, 'b, 'c>(strings: &'a Vec<String>, default: &'b String) -> &'c String {
    if strings.len() > 0 {
        &strings[0]
    } else {
        default
    }
}
}

Rust can’t compile this. It doesn’t know whether the returned &String comes from strings or default. So it gives:

error[E0106]: missing lifetime specifier

That’s Rust saying: “Tell me which one lives long enough!”


Example of why that matters:

fn main() {
    let strings = vec![];
    let default = String::from("default");
    let s = first_or(&strings, &default);
    drop(default);
    println!("{}", s);
}

If first_or returned default, s would become invalid after drop(default). Hence, Rust prevents it unless lifetimes are declared explicitly.

We’ll fully cover this in Chapter 10: Lifetimes.


Another Example: Returning a Reference to Local Data

#![allow(unused)]
fn main() {
fn return_a_string() -> &String {
    let s = String::from("Hello world");
    let s_ref = &s;
    s_ref
}
}

This is unsafe because s is dropped at the end of the function - so the returned reference would point to freed memory. Rust correctly refuses to compile it.


Summary

  • References let you access data without taking ownership.

  • Created with & or &mut.

  • Dereferenced with * (often implicit).

  • Borrow checker ensures:

    • You can’t mutate and alias at once.
    • Permissions (R/W/O) are tracked and restored after use.
    • Data always outlives its references.

Rust’s reference system looks restrictive, but it’s what allows C-level performance with absolute safety - no garbage collector, no memory leaks, no data races.

You have come very far, now in the next part, we will cover some other important aspects of ownership.

Understanding Ownership(Part 2)

Today, we will complete:

Screenshot 2025-10-19 at 9 23 10 PM

Now you’ll understand how to share data without transferring ownership - safely.


References and Borrowing

Let’s start with the problem Rust solves here.

Problem: Borrowing Data

Suppose you have this code:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

Explanation:

  • &s1 - this creates a reference to s1, not a copy.
  • The function calculate_length takes s: &String, meaning “I’ll look at a string, but I won’t own it.”
  • Ownership of s1 is not transferred, only borrowed temporarily.
  • After the function call, s1 is still valid - you can still use it.

Output:

The length of 'hello' is 5.

This is Rust’s borrowing system in action.


References are Immutable by Default

By default, a reference (&T) cannot modify the value it points to.

Example:

#![allow(unused)]
fn main() {
fn change(some_string: &String) {
    some_string.push_str(", world");
}
}

Compilation error:

cannot borrow `*some_string` as mutable, as it is behind a `&` reference

This happens because &String is an immutable reference - you’re not allowed to change the data.


Mutable References

To modify a borrowed value, you must use a mutable reference (&mut):

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
    println!("{}", s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Output:

hello, world

Rules of Mutable References:

  1. You can have only one mutable reference to a value at a time.
  2. You cannot have a mutable reference while an immutable one exists.

This avoids data races - situations where multiple pointers try to read and write simultaneously.


Compile-Time Safety Example

#![allow(unused)]
fn main() {
let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);
}

Error:

cannot borrow `s` as mutable more than once at a time

Rust prevents this before the program even runs.

If you separate lifetimes, it’s fine:

#![allow(unused)]
fn main() {
let mut s = String::from("hello");

{
    let r1 = &mut s;
    println!("{}", r1);
} // r1 goes out of scope here

let r2 = &mut s;
println!("{}", r2);
}

Works - because r1 is dropped before r2 begins.


Mixing Mutable and Immutable References

#![allow(unused)]
fn main() {
let mut s = String::from("hello");

let r1 = &s; // immutable
let r2 = &s; // immutable
let r3 = &mut s; // mutable
}

Error:

cannot borrow `s` as mutable because it is also borrowed as immutable

You can’t have both immutable and mutable references active at the same time.

But this works:

#![allow(unused)]
fn main() {
let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2); // both used here

let r3 = &mut s; // r1 and r2 no longer used
println!("{}", r3);
}

Works - after immutable refs are no longer used, the mutable one is allowed.


Dangling References

A dangling reference points to data that has been freed - Rust completely forbids this.

Example:

#![allow(unused)]
fn main() {
fn dangle() -> &String {
    let s = String::from("hello");
    &s
}
}

Error:

returns a reference to data owned by the current function

Because s is dropped at the end of dangle, returning &s would make a reference to invalid memory.

Fix:

#![allow(unused)]
fn main() {
fn no_dangle() -> String {
    let s = String::from("hello");
    s
}
}

Now ownership is returned safely.


The Rules of References (Summary)

  1. You can have either one mutable reference or any number of immutable references.
  2. References must always be valid (no dangling refs).

These two rules are what make Rust’s memory model safe without garbage collection.


Next Concept: Slices

Now that you understand ownership and borrowing, slices let you reference a part of a collection (like a string or array) without taking ownership.

Problem Example

You want a function that returns the first word of a string:

#![allow(unused)]
fn main() {
fn first_word(s: &String) -> ?
}

You could find the index of the space, but returning that index isn’t ideal - what if the string changes?

Example:

#![allow(unused)]
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // empties the string
}

Now word’s index points to invalid data.


String Slices (&str)

A string slice is a reference to part of a string.

#![allow(unused)]
fn main() {
let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];
}

hello → "hello" world → "world"

The syntax [start..end] uses byte indices.

Shorthand:

  • &s[..2] means “from 0 to 2”.
  • &s[3..] means “from 3 to end”.
  • &s[..] means “the whole string”.

Slices as Function Parameters

Now fix the earlier problem:

#![allow(unused)]
fn main() {
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}
}

Works for both String and string literals:

#![allow(unused)]
fn main() {
let my_string = String::from("hello world");
let word = first_word(&my_string[..]);

let my_literal = "hello world";
let word = first_word(my_literal);
}

String Slices Prevent Errors

If you use the slice version:

#![allow(unused)]
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // ERROR!
}

Compile-time error - can’t mutate while a slice exists. Rust automatically prevents invalid references!


Other Slices - Arrays

Slices aren’t just for strings:

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);
}

slice type: &[i32] It borrows a section of the array safely.


Key Takeaways

ConceptDescription
Reference (&T)Borrow data immutably
Mutable Reference (&mut T)Borrow data mutably
Borrowing RulesEither multiple immutable refs OR one mutable ref
Dangling RefForbidden - data must live long enough
Slice (&[T] or &str)Borrow part of data without ownership
Compiler EnforcementBorrow checker ensures safety at compile time

Final Summary

Rust prevents memory errors by enforcing ownership, lifetimes, and borrowing rules at compile time, not runtime.

You’ve now learned:

  • Shared vs mutable borrowing
  • Reference rules and lifetimes
  • Slices for partial borrowing
  • How the borrow checker ensures safe memory access

Ownership Recap

This chapter ties together everything we’ve learned about ownership, borrowing, and slices, while also comparing Rust’s memory management model to garbage collection (GC) used in other languages. It also clarifies concepts like the stack vs heap, pointers, and how Rust avoids undefined behavior without using a garbage collector.


Why Ownership Exists

In programming, data in memory must eventually be cleaned up (freed) after it’s no longer needed. Languages handle this in two main ways:

  1. Garbage Collection (GC): Memory is managed automatically by a background process.
  2. Ownership (Rust’s approach): Memory is managed at compile time using strict rules.

Ownership gives you control and safety without a garbage collector, which leads to better performance and predictability.


Ownership vs Garbage Collection

Let’s compare how memory management works in Python (GC) versus Rust (Ownership).


What is Garbage Collection?

In languages like Python, Java, Go, or JavaScript, memory is automatically managed by a garbage collector (GC).

Here’s what happens:

  • The program allocates memory for data while running.
  • The GC periodically scans memory to find objects that are no longer being used.
  • Once it confirms that no variable refers to a piece of data anymore, it frees that memory.

Advantages of GC

  • Easy for developers - no need to manually manage memory.
  • Avoids “use-after-free” bugs (accessing freed memory).
  • Prevents some types of memory leaks automatically.

Drawbacks of GC

  • Performance overhead: The GC has to pause and scan memory. This can slow down execution or cause unpredictable “stop-the-world” moments.
  • Unpredictable behavior: You never know exactly when something will be cleaned up.
  • Hidden data sharing: It’s often unclear which variables point to the same data, causing side effects.

Example: Garbage Collection in Python

Let’s take an example in Python that demonstrates this issue.

class Document:
    def __init__(self, words: list[str]):
        """Create a new document"""
        self.words = words

    def add_word(self, word: str):
        """Add a word to the document"""
        self.words.append(word)

    def get_words(self) -> list[str]:
        """Return the list of words"""
        return self.words

# Create a list and two documents
words = ["Hello"]
d = Document(words)

d2 = Document(d.get_words())  # shares same list
d2.add_word("world")

What’s Happening Here

Let’s answer two important questions:

1. When is the memory freed?

There are three references to the same ["Hello"] list:

  • words
  • d.words
  • d2.words

Python will only deallocate (free) that list after all three references go out of scope.

So, you cannot predict exactly when the data will be freed just by reading the code. This is unpredictable memory cleanup.

2. What are the contents of d?

Both d and d2 point to the same list in memory! So when we do:

d2.add_word("world")

It also modifies d.words. Now both show:

["Hello", "world"]

This is a shared mutable reference - it can easily cause unexpected bugs. You didn’t intend to mutate d, but you did!


Hidden Pointers

Even though Python doesn’t explicitly use the word “pointer,” every object is actually stored on the heap, and variables hold references (pointers) to them.

This makes it hard to see which variable points to what, leading to side effects like the one above.


Rust’s Approach: Ownership Model

Rust makes these ideas explicit and predictable using ownership and borrowing rules. Let’s recreate the same Document idea in Rust:

#![allow(unused)]
fn main() {
type Document = Vec<String>;

fn new_document(words: Vec<String>) -> Document {
    words
}

fn add_word(this: &mut Document, word: String) {
    this.push(word);
}

fn get_words(this: &Document) -> &[String] {
    this.as_slice()
}
}

What’s Different Here?

Let’s look closely at how each function behaves.


new_document

#![allow(unused)]
fn main() {
fn new_document(words: Vec<String>) -> Document {
    words
}
}
  • This function takes ownership of the Vec<String> (words).
  • When we call new_document, the ownership of the vector moves into the function.
  • When the Document goes out of scope, Rust automatically drops (frees) the memory.
  • This is predictable cleanup - at the exact end of the owner’s lifetime.

add_word

#![allow(unused)]
fn main() {
fn add_word(this: &mut Document, word: String) {
    this.push(word);
}
}
  • We pass a mutable reference &mut Document, so we can modify it safely.
  • The function takes ownership of the word, ensuring no other variable can use it after.
  • No other code can mutate this at the same time - Rust enforces this at compile time.

get_words

#![allow(unused)]
fn main() {
fn get_words(this: &Document) -> &[String] {
    this.as_slice()
}
}
  • This function returns an immutable reference to the list of words.
  • You can read the contents, but not modify them.
  • You also cannot accidentally “leak” mutable references that allow unwanted changes.

Example in Rust

Now let’s translate the Python example completely:

fn main() {
    let words = vec!["hello".to_string()];
    let d = new_document(words);

    // Clone (deep copy) the contents of `d`
    let words_copy = get_words(&d).to_vec();
    let mut d2 = new_document(words_copy);

    add_word(&mut d2, "world".to_string());

    // Verify that d was not affected
    assert!(!get_words(&d).contains(&"world".into()));
}

Explanation

  • words is moved into dd now owns the memory.
  • get_words(&d) returns a reference (a “view”) of the data, not ownership.
  • .to_vec() clones each String - now d2 has its own copy.
  • Modifying d2 does not affect d.

No accidental sharing. No undefined behavior. Predictable cleanup and performance.


Summary - Garbage Collection vs Ownership

AspectGarbage Collection (Python/Java)Ownership (Rust)
Memory cleanupDone automatically at runtime by GCDone automatically when owner goes out of scope
PerformanceOverhead due to GC scanningNo GC overhead, compile-time guarantees
PredictabilityCleanup timing unpredictableFully predictable
SafetyAvoids unsafe access at runtimeEnforced safety at compile time
Data sharingImplicit and hiddenExplicit via & and &mut
MutabilityAllowed anywhereControlled via borrowing rules

The Core Concepts of Ownership (Quick Recap)

Let’s recall the three key rules:

  1. Each value in Rust has a single owner.
  2. When the owner goes out of scope, the value is dropped (freed).
  3. There can only be one mutable reference OR any number of immutable references at a time.

These rules guarantee memory safety without runtime overhead.


Borrowing and References

  • Immutable reference (&T): Lets you read data without taking ownership.
  • Mutable reference (&mut T): Lets you modify data, but only one can exist at a time.
  • Dangling references are prevented at compile time.

Stack vs Heap (Recap)

  • Stack: Fast memory for small, fixed-size data (like integers).
  • Heap: Slower memory for dynamic data (like Vec, String, etc).
  • Rust automatically decides where to store data, but ownership ensures heap data is properly freed.

Final Takeaway

Rust doesn’t remove memory management - it just moves it from runtime to compile-time.

If you’ve used other programming languages, you’ve already dealt with memory and pointers - just indirectly. Rust makes them visible and safe, so you can:

  • Predict exactly when cleanup happens,
  • Avoid garbage collection overhead,
  • Prevent unintended data mutations,
  • And write faster, safer, more reliable code.

Finally, we are done with ownership. Will move to stucts in day 6.

Summary Quote

“If Rust is not your first language, then you’ve already worked with memory and pointers - Rust just makes those ideas explicit, predictable, and safe.”

Day 6 - Chapter 5: Structs

This chapter introduces structs, one of Rust’s most important features for defining custom data types. Structs let you group related data together, providing a foundation for organized, idiomatic Rust code. From the book, we will cover:

Screenshot 2025-10-21 at 3 38 45 PM

1. What Are Structs?

Structs (short for structures) group related data together under one name. They’re like named tuples, but each field has a name, making code more readable. Structs are similar to objects in OOP - they hold data, and you can define methods for them.

Example

#![allow(unused)]
fn main() {
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}
}

2. Creating Instances of Structs

Once defined, create instances as follows:

fn main() {
    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };
}

Field order doesn’t matter because fields are explicitly named.

Accessing Fields

#![allow(unused)]
fn main() {
println!("{}", user1.email);
}

If the struct is mutable:

#![allow(unused)]
fn main() {
let mut user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

user1.email = String::from("anotheremail@example.com");
}

The whole struct must be mutable; individual fields cannot be selectively mutable.


3. Returning Structs from Functions

You can return structs from functions:

#![allow(unused)]
fn main() {
fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username: username,
        email: email,
        sign_in_count: 1,
    }
}
}

4. Field Init Shorthand

Rust provides shorthand syntax when field names and variable names are identical:

#![allow(unused)]
fn main() {
fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username,
        email,
        sign_in_count: 1,
    }
}
}

5. Struct Update Syntax

You can reuse fields from another instance using ..:

#![allow(unused)]
fn main() {
let user2 = User {
    email: String::from("another@example.com"),
    ..user1
};
}

This copies or moves fields from user1. Ownership of non-Copy types (like String) moves, making user1 partially invalid afterward.


6. Tuple Structs

Tuple structs are like tuples but have unique types:

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

Even though both contain three i32s, they’re distinct types. You can destructure them:

#![allow(unused)]
fn main() {
let Point(x, y, z) = origin;
}

7. Unit-Like Structs

Structs with no fields:

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

Used when you need to implement traits but store no data.


8. Ownership in Structs

If a struct owns its data (using String), it owns everything inside. If fields are references (&str), you must define lifetimes, which come later in Chapter 10. For now, prefer String.


9. Borrowing Struct Fields

You can borrow individual fields:

struct Point { x: i32, y: i32 }

fn main() {
    let mut p = Point { x: 0, y: 0 };
    let x = &mut p.x;
    *x += 1;
    println!("{}, {}", p.x, p.y);
}

However, you cannot borrow the whole struct immutably while a field is mutably borrowed:

fn print_point(p: &Point) {
    println!("{}, {}", p.x, p.y);
}

fn main() {
    let mut p = Point { x: 0, y: 0 };
    let x = &mut p.x;
    print_point(&p); // Error
}

10. Example: Rectangle Area

(a) Using Separate Variables

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

(b) Using Tuples

fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}

(c) Using Structs

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    println!("The area is {} square pixels.", area(&rect1));
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

This approach gives clear meaning to data and avoids ownership transfer.


11. Printing Structs with Debug

Printing structs directly gives an error because Display isn’t implemented. Use #[derive(Debug)] and {:?} for debug printing:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    println!("rect1 is {rect1:?}");
}

Pretty-print with {:#?}.


12. Using the dbg! Macro

dbg! prints file name, line number, and value to stderr:

#[derive(Debug)]
struct Rectangle { width: u32, height: u32 }

fn main() {
    let scale = 2;
    let rect1 = Rectangle { width: dbg!(30 * scale), height: 50 };
    dbg!(&rect1);
}

It takes ownership of its expression unless you use &.


13. Derive Attributes

#[derive(Debug)] is one of many traits you can auto-implement. Others include Clone, Copy, PartialEq, Eq, Hash, and Default.


Defining Methods on Structs

Instead of external functions, you can define methods inside an impl block:

#![allow(unused)]
fn main() {
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}
}

Call with method syntax:

#![allow(unused)]
fn main() {
rect1.area();
}

Why &self?

  • self - takes ownership
  • &self - immutable borrow (read-only)
  • &mut self - mutable borrow (allows modification)

&self is most common since most methods read data without modifying it.


Methods with Parameters

You can add extra parameters:

#![allow(unused)]
fn main() {
impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}
}

Associated Functions

Functions without self are called associated functions and are called using :::

#![allow(unused)]
fn main() {
impl Rectangle {
    fn square(size: u32) -> Self {
        Self { width: size, height: size }
    }
}

let sq = Rectangle::square(10);
}

Multiple impl Blocks

You can define multiple impl blocks for better organization.


Method Call Sugar

Rust automatically handles references and ownership:

#![allow(unused)]
fn main() {
rect1.area();
}

is equivalent to:

#![allow(unused)]
fn main() {
Rectangle::area(&rect1);
}

Rust automatically adds &, &mut, or moves ownership as needed.


Ownership and Methods

  • &self → reads data
  • &mut self → modifies data
  • self → consumes the instance

Example:

#![allow(unused)]
fn main() {
impl Rectangle {
    fn area(&self) -> u32 { self.width * self.height }
    fn set_width(&mut self, width: u32) { self.width = width; }
    fn max(self, other: Self) -> Self {
        Rectangle {
            width: self.width.max(other.width),
            height: self.height.max(other.height),
        }
    }
}
}

The “Cannot Move Out of *self” Error

If you try to move out of *self in a method that takes &mut self, Rust will error to prevent unsafe behavior. If all fields are Copy, you can derive Copy and Clone:

#![allow(unused)]
fn main() {
#[derive(Copy, Clone)]
struct Rectangle {
    width: u32,
    height: u32,
}
}

Summary

ConceptDescription
StructDefines a custom type grouping related values
Field init shorthandAllows concise initialization when variable and field names match
Struct update syntax (..)Reuses fields from another instance
Tuple structStruct without field names but unique type
Unit-like structStruct with no fields
Owned dataPrefer String for owned fields
BorrowingUse references to avoid ownership transfer
Debug printingUse #[derive(Debug)] and {:?}
dbg!() macroPrints file, line, and value for debugging
MethodFunction defined inside an impl block
self formsself, &self, &mut self for ownership control
Associated functionFunction without self, called with ::
Automatic referencingRust automatically adds & or &mut for methods
Ownership in methods&self reads, &mut self modifies, self consumes
Copy and CloneEnable safe duplication of structs
OrganizationGrouping logic in impl blocks improves readability and modularity

Method Syntax in Rust


What Are Methods?

Methods in Rust are functions associated with a type (like structs, enums, or traits). They are declared with the fn keyword just like normal functions, but with two key differences:

  1. They are defined inside an impl block (short for implementation).
  2. Their first parameter is always self, which represents the instance of the type on which the method is called.

This self lets methods operate on data inside the struct - similar to how methods work in object-oriented languages, but with Rust’s safety and ownership guarantees.


Defining Methods on Structs

Let’s take a simple struct:

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}
}

Earlier, we had a standalone function that took a Rectangle as a parameter to compute its area. Now, we’ll instead make it a method defined directly on the Rectangle struct:

#![allow(unused)]
fn main() {
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}
}

Now we can call it like this:

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

Output:

The area of the rectangle is 1500 square pixels.

How This Works

  • impl Rectangle { ... } begins an implementation block.

  • Inside it, fn area(&self) -> u32 defines a method.

  • The parameter &self means:

    • The method borrows the instance immutably.
    • We don’t take ownership.
    • We can read its fields but not modify them.

Inside the method body, we use self.width and self.height - self refers to the instance (rect1 in our example).

When calling, rect1.area() is method syntax. Rust automatically translates this to:

#![allow(unused)]
fn main() {
Rectangle::area(&rect1);
}

That’s why you don’t need to manually pass rect1 - Rust does it.


Why &self, not self or &mut self?

Rust allows three main forms for the first parameter of methods:

SyntaxOwnershipMeaning
&selfBorrow (immutable)Read data only
&mut selfBorrow (mutable)Modify data
selfTake ownershipConsume the instance

You use:

  • &self → when you just want to read.
  • &mut self → when you want to modify.
  • self → when you want to consume or transform it.

Taking ownership (self) is rare, used mainly when a method returns a completely new instance or moves data out of it.


Why Use Methods?

Using methods instead of plain functions:

  • Improves organization - related behaviors live together.
  • Provides the clean .method() syntax.
  • Avoids repeating the type everywhere.
  • Clearly associates functionality with a type.

Methods and Fields with the Same Name

You can define a method with the same name as a field. Example:

impl Rectangle {
    fn width(&self) -> bool {
        self.width > 0
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    if rect1.width() {
        println!("The rectangle has a nonzero width; it is {}", rect1.width);
    }
}

When you call rect1.width(), Rust knows it’s the method. When you use rect1.width, Rust knows it’s the field.

Methods like this that return a field’s value are called getters. Unlike languages like Java or Python, Rust doesn’t generate getters automatically - you define them yourself. You’ll learn more about making fields private and exposing public getters in Chapter 7.


Methods with More Parameters

Let’s add another method - one that takes another Rectangle as an argument and checks if self can contain it.

#![allow(unused)]
fn main() {
impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}
}

Usage:

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 10, height: 40 };
    let rect3 = Rectangle { width: 60, height: 45 };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Output:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

Here, we pass &rect2 and &rect3 - immutable borrows, since we only read their data.


Associated Functions

Not all functions in an impl block need to be methods. If a function doesn’t take self as its first parameter, it’s called an associated function.

Example - a constructor-like function:

#![allow(unused)]
fn main() {
impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}
}

Here:

  • Self means the same as Rectangle.

  • This function doesn’t take any instance - it creates one.

  • We call it using the :: syntax:

    #![allow(unused)]
    fn main() {
    let sq = Rectangle::square(3);
    }

:: is used for both associated functions and modules in Rust.


Multiple impl Blocks

A struct can have multiple impl blocks.

These two are equivalent:

#![allow(unused)]
fn main() {
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}
}

You may not need to split them, but it’s valid - and sometimes useful when combining traits, generics, or organization.


Method Calls Are Syntactic Sugar

Rust translates method calls into plain function calls.

Example:

#![allow(unused)]
fn main() {
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
    fn set_width(&mut self, width: u32) {
        self.width = width;
    }
}
}

Then, these calls:

#![allow(unused)]
fn main() {
let mut r = Rectangle { width: 1, height: 2 };
let area1 = r.area();
let area2 = Rectangle::area(&r);
assert_eq!(area1, area2);

r.set_width(2);
Rectangle::set_width(&mut r, 2);
}

are identical.

Rust automatically adds:

  • & for immutable self
  • &mut for mutable self

Unlike C or C++, Rust doesn’t need an arrow operator (->). It dereferences automatically when calling with ..


Deeper Dereferencing

Rust even adds as many references/dereferences as needed to match the self type.

#![allow(unused)]
fn main() {
let r = &mut Box::new(Rectangle { width: 1, height: 2 });
let area1 = r.area();
let area2 = Rectangle::area(&**r);
assert_eq!(area1, area2);
}

Here:

  • r is &mut Box<Rectangle>
  • Rust automatically dereferences r and *Box until it gets Rectangle
  • Then it immutably borrows for area()

Rust can “downgrade” a mutable reference to a shared one (&mut&) when it’s safe, but it never upgrades & to &mut.


Methods and Ownership

Let’s see how ownership and borrowing rules apply to methods.

Example: Three Kinds of Methods

#![allow(unused)]
fn main() {
impl Rectangle {    
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn set_width(&mut self, width: u32) {
        self.width = width;
    }

    fn max(self, other: Rectangle) -> Rectangle {
        Rectangle { 
            width: self.width.max(other.width),
            height: self.height.max(other.height),
        }
    }
}
}

Calling These Methods

#![allow(unused)]
fn main() {
let rect = Rectangle { width: 0, height: 0 };
println!("{}", rect.area());

let other_rect = Rectangle { width: 1, height: 1 };
let max_rect = rect.max(other_rect);
}

This is valid - because:

  • rect.area() borrows immutably.
  • rect.max() takes ownership (moves rect).

Mutability with &mut self

If you try to modify something through an immutable variable:

#![allow(unused)]
fn main() {
let rect = Rectangle { width: 0, height: 0 };
rect.set_width(10);
}

Rust errors:

cannot borrow `rect` as mutable, as it is not declared as mutable

Fix: declare it mut.

#![allow(unused)]
fn main() {
let mut rect = Rectangle { width: 0, height: 0 };
rect.set_width(10); // now OK
}

However, if you then do:

#![allow(unused)]
fn main() {
let rect_ref = &rect;
rect_ref.set_width(20);
}

It fails again - because even though the original rect is mutable, rect_ref is an immutable reference. You can’t call a &mut self method through a shared reference.


Moves with self

When a method takes self, it moves the instance.

#![allow(unused)]
fn main() {
let rect = Rectangle { width: 0, height: 0 };
let other = Rectangle { width: 1, height: 1 };
let max_rect = rect.max(other);
println!("{}", rect.area()); // ❌ rect moved
}

Error:

borrow of moved value: `rect`

That’s because max takes ownership of rect, so rect can’t be used again.


Calling self Methods on References

Sometimes, you might want to call a self-taking method (fn consumes(self)) on a reference - for example, inside another method that takes &mut self:

#![allow(unused)]
fn main() {
fn set_to_max(&mut self, other: Rectangle) {
    *self = self.max(other);
}
}

This fails because self.max() tries to move out of *self, but *self is borrowed - Rust prevents that move to avoid double-free errors.


Why Rust Prevents Moves from &mut self

Rust disallows moving fields out of borrowed data unless the type implements Copy. Here’s why:

If we derive Copy, the method becomes valid:

#![allow(unused)]
fn main() {
#[derive(Copy, Clone)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn max(self, other: Self) -> Self {
        Rectangle { 
            width: self.width.max(other.width),
            height: self.height.max(other.height),
        }
    }

    fn set_to_max(&mut self, other: Rectangle) {
        *self = self.max(other);
    }
}
}

Now self.max(other) works because copying doesn’t consume ownership.


If Rectangle owned heap data like a String, automatic copying would lead to double frees (two deallocations of the same heap memory). That’s why Rust doesn’t auto-derive Copy. You must do it manually only when it’s safe.

Example of unsafe move (if Rust allowed it):

#![allow(unused)]
fn main() {
struct Rectangle {
    width: u32,
    height: u32,
    name: String,
}

fn set_to_max(&mut self, other: Rectangle) {
    let max = self.max(other);
    drop(*self);
    *self = max;
}
}

Here, both self.name and other.name would be freed, and then again when *self = max overwrites it - causing undefined behavior. So Rust forbids this move entirely.


Summary

  • Structs let you create custom types grouping related data.

  • Methods define behavior tied to those structs.

  • Associated functions (without self) are functions tied to the type itself, often used as constructors.

  • Ownership and borrowing rules apply the same way for methods:

    • &self - shared access.
    • &mut self - exclusive mutable access.
    • self - ownership transfer.
  • Rust’s method syntax (rect.area()) is syntactic sugar for plain function calls (Rectangle::area(&rect)).

  • Rust’s design ensures memory safety even in tricky cases like moving or copying from self.


In the next file, we will cover enums and pattern matching.

Day 7 - Enums and Pattern Matching

1. Introduction

Enums (short for enumerations) allow you to define a type by enumerating its possible variants - that is, by explicitly listing all the possible forms a value of that type can take. While structs group together related data, enums are about defining a type that can represent exactly one of several possibilities.

In this chapter, we’ll cover:

Screenshot 2025-10-22 at 1 00 42 PM

2. Defining an Enum

An enum defines a type by enumerating its possible variants. Example - representing the kind of IP addresses:

#![allow(unused)]
fn main() {
enum IpAddrKind {
    V4,
    V6,
}
}

Now we can create instances:

#![allow(unused)]
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
}

These variants are namespaced under IpAddrKind, so we use the double colon :: syntax.

We can even define a function that takes any variant:

#![allow(unused)]
fn main() {
fn route(ip_kind: IpAddrKind) {}
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}

3. Storing Data with Enums

Initially, you might combine structs and enums to store both type and data:

#![allow(unused)]
fn main() {
enum IpAddrKind {
    V4,
    V6,
}

struct IpAddr {
    kind: IpAddrKind,
    address: String,
}

let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
};
}

However, you can store data directly inside the enum variant, making it cleaner:

#![allow(unused)]
fn main() {
enum IpAddr {
    V4(String),
    V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
}

Each variant can even hold different types and amounts of data:

#![allow(unused)]
fn main() {
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}
}

So enums are more flexible than structs when variants require different kinds of data.


4. Enum Variants Holding Structs

Enums can even hold other structs or enums:

#![allow(unused)]
fn main() {
struct Ipv4Addr { /* fields omitted */ }
struct Ipv6Addr { /* fields omitted */ }

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}
}

5. Enums with Multiple Data Types

You can create enums with completely different variant forms:

#![allow(unused)]
fn main() {
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}
}

Each variant can store different types of values - unit-like, tuple-like, or struct-like.

You can also define methods on enums:

#![allow(unused)]
fn main() {
impl Message {
    fn call(&self) {
        // method body
    }
}

let m = Message::Write(String::from("hello"));
m.call();
}

6. The Option Enum

Rust doesn’t have null. Instead, it provides Option<T> to represent a value that may or may not exist.

#![allow(unused)]
fn main() {
enum Option<T> {
    None,
    Some(T),
}
}

Examples:

#![allow(unused)]
fn main() {
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
}

Option<T> and T are different types - this means Rust won’t let you use an Option<i8> like an i8 without handling the None case.

For instance, this will not compile:

#![allow(unused)]
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y; // ❌ type mismatch
}

This strictness eliminates null pointer errors.


7. Pattern Matching with match

The match control flow construct lets you handle all possible variants of an enum safely.

Example:

#![allow(unused)]
fn main() {
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}
}

Each arm of the match must handle one pattern. The compiler checks that all cases are covered.

You can add logic inside an arm:

#![allow(unused)]
fn main() {
fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}
}

8. Matching and Binding Values

You can extract data from enum variants inside a match.

#![allow(unused)]
fn main() {
#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Quarter(state) => {
            println!("State quarter from {state:?}!");
            25
        }
        _ => 0,
    }
}
}

If we pass Coin::Quarter(UsState::Alaska), it will print “State quarter from Alaska!”


9. Matching with Option<T>

match works perfectly with Option<T> too.

Example - incrementing an optional integer:

#![allow(unused)]
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}

10. Matches Must Be Exhaustive

Every possible variant must be handled:

#![allow(unused)]
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}
}

This will not compile because None is not covered. Rust ensures safety by requiring exhaustive matches.


11. Catch-All Patterns and the _ Placeholder

You can handle “all other cases” using _:

#![allow(unused)]
fn main() {
let dice_roll = 9;

match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => move_player(dice_roll),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
}

The _ pattern matches any value not previously handled.


12. The if let Construct

if let is a shorthand for handling a single match case when you don’t need full pattern matching:

#![allow(unused)]
fn main() {
let some_u8_value = Some(3);

if let Some(3) = some_u8_value {
    println!("Three");
}
}

This is equivalent to:

#![allow(unused)]
fn main() {
match some_u8_value {
    Some(3) => println!("Three"),
    _ => (),
}
}

if let provides a concise, readable alternative when only one pattern matters.


13. Ownership Inventory #1

This section introduces scenarios inspired by common StackOverflow questions about ownership in Rust. These focus on real-world situations that test your understanding of ownership rules, borrowing, and memory management.

It mentions an experimental in-browser IDE that allows you to hover over functions to get more info about unfamiliar functions or types. Here’s a summary of concepts you should be familiar with based on the description:

  1. Memory Usage & Rust Types:

    • Rust is strict about memory usage and avoids issues like null-pointer dereferencing or undefined behavior (e.g., double-free) by using ownership and borrowing.
    • The IDE functionality mentioned allows you to hover over the function to get detailed type information.
  2. Understanding Borrowing & Ownership:

    • Ensure you understand when data is moved versus when it is borrowed. Rust forces you to acknowledge this behavior to prevent accidental memory corruption.
  3. Function/Method Explanations:

    • The example function make_exciting shows how you can replace characters (. becomes ! and ? becomes ) in a string. You’ll need to inspect or interact with the code to understand its type and how ownership works here.

Summary

  • Structs group multiple related fields.
  • Enums define a type that can have one of several variants.
  • Enums can store different data types in different variants.
  • The Option enum replaces nulls, ensuring safety at compile time.
  • The match construct exhaustively handles all cases.
  • The if let syntax offers a convenient shorthand for simpler matches.

Day 8 - Chapter 7: Managing Growing Projects with Packages, Crates, and Modules

1. Introduction

Today, we’re going to cover one of the most important parts of learning Rust - how to organize your code as your project grows.

Screenshot 2025-10-27 at 4 06 53 PM

When you first start writing Rust, everything might fit nicely in a single main.rs file. But as your program gets bigger, things can quickly get messy. That’s where packages, crates, and modules come in.

2. Packages and Crates

Packages are the containers that hold your Rust projects.

A package can contain:

  • One or more crates (projects or libraries).
  • A Cargo.toml file (this is like the project’s recipe - it lists dependencies and metadata).

There are two main types of crates:

  • Binary crate → makes an executable program (like cargo run).
  • Library crate → provides reusable code (used by other crates).

3. Defining Modules to Control Scope and Privacy

When your Rust project grows, you’ll start having many functions, structs, and enums. To keep everything neat and avoid name clashes, Rust lets you group related code into modules using the mod keyword.

Think of a module as a folder or section of your project - a place to group code that belongs together.

Example: A Restaurant Analogy

Let’s say we’re building a restaurant app. We could have different parts (modules) for the restaurant’s sections, like the front of the house and the back of the house.

The struct would look something like this :

#![allow(unused)]
fn main() {
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {
            println!("Added to waitlist!");
        }

        fn seat_at_table() {
            println!("Seating the customer...");
        }
    }

    mod serving {
        fn take_order() {}
        fn serve_order() {}
        fn take_payment() {}
    }
}
}

Breaking This Down

mod front_of_house - defines a module named front_of_house.

Inside it, we have two submodules:

  • pub mod hosting

  • mod serving

The pub keyword means public - it tells Rust that other parts of the program can use this module or function.If we don’t mark something as pub, Rust keeps it private by default, meaning it can only be used inside the same module.

So in our example:

add_to_waitlist() ✅ is public - other modules can call it.

seat_at_table() ❌ is private - only hosting can use it.

Using the Module from Another Place

If you want to use add_to_waitlist() from outside (like from main.rs), you’d write:

fn main() {
    crate::front_of_house::hosting::add_to_waitlist();
}

Here’s what’s happening:

  • Crate means “start at the root of my current project.”
  • Then we go down the tree:
    • front_of_house
    • hosting
    • add_to_waitlist()

4. Paths for Referring to an Item in the Module Tree

To use a function or struct inside a module, you need to refer to it by its path - kind of like how you use file paths in a computer.

There are two types of paths:

  • Absolute path → starts from the root of your crate.

  • Relative path → starts from where you currently are in the module tree.

💡 Example:

crate::front_of_house::hosting::add_to_waitlist(); // absolute
front_of_house::hosting::add_to_waitlist();        // relative

Think of it like finding a file:

/home/user/docs/file.txtabsolute

../docs/file.txtrelative

5. Bringing Paths Into Scope with the use Keyword

The use keyword is a shortcut to make long paths shorter and cleaner.

Instead of writing:

crate::front_of_house::hosting::add_to_waitlist();

You can bring the path into scope:

use crate::front_of_house::hosting;

hosting::add_to_waitlist();

It’s like creating a desktop shortcut so you don’t have to dig through folders every time you want to open a file.

6. Separating Modules into Different Files

As your code grows, you can split modules into different files to stay organized.

Example project structure:

src/
 ├── main.rs
 ├── front_of_house.rs
 └── front_of_house/
      └── hosting.rs

In your main.rs or lib.rs, you declare:

mod front_of_house;

Rust automatically looks for:

front_of_house.rs, or

A folder named front_of_house containing a mod.rs or nested modules.

This keeps your codebase modular and easy to navigate - just like storing things in labeled boxes instead of one giant drawer.

7. Summary

ConceptThink of it like…What it does
PackageA full projectGroups crates, has Cargo.toml
CrateA houseA binary or library of Rust code
ModuleA room in the houseOrganizes related code
PathThe addressShows how to reach an item
use keywordShortcutBrings names into scope
Separate filesDifferent roomsKeeps your code tidy

Chapter 8 – Common Collections

In this chapter, we will cover:

Screenshot 2025-10-27 at 4 05 58 PM

Rust’s standard library provides several collection types - data structures that store multiple values in a single data type. The most commonly used collections are:

  • Vectors (Vec<T>) – for storing a list of items of the same type in a growable, contiguous array.
  • Strings (String) – for text that is stored as a collection of UTF-8–encoded bytes.
  • Hash maps (HashMap<K, V>) – for storing key-value pairs.

These are the core collections you’ll use in almost every Rust program.


8.1. Storing Lists of Values with Vectors

A vector (Vec<T>) is a growable array type - all elements must be of the same type, and it stores them next to each other in memory.

Creating a Vector

#![allow(unused)]
fn main() {
let v: Vec<i32> = Vec::new();
}
  • Vec::new() creates an empty vector.
  • You can add elements later using push.

A shorthand for initializing with elements:

#![allow(unused)]
fn main() {
let v = vec![1, 2, 3];
}
  • vec![] is a macro that creates a vector and infers its type from the elements.

Updating a Vector

#![allow(unused)]
fn main() {
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
}
  • push appends elements at the end.
  • The vector must be mut since push modifies it.

Dropping a Vector

When a vector goes out of scope, all its elements are also dropped automatically.

#![allow(unused)]
fn main() {
{
    let v = vec![1, 2, 3, 4];
} // v is dropped here, memory freed
}

Rust ensures memory safety - no use-after-free or leaks occur.


8.2. Reading Elements of Vectors

You can access elements in two ways:

#![allow(unused)]
fn main() {
let v = vec![10, 20, 30, 40];

let third: &i32 = &v[2];
println!("The third element is {third}");

match v.get(2) {
    Some(value) => println!("The third element is {value}"),
    None => println!("There is no third element."),
}
}
  • v[2] uses indexing and will panic if out of bounds.
  • v.get(2) returns an Option<&T>, allowing safe error handling.

If you try v[100], it panics at runtime. But v.get(100) gives None.


Borrowing Rules with Vectors

#![allow(unused)]
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
}

This code won’t compile because:

  • You borrowed v immutably (&v[0]).
  • Then tried to mutate it (v.push(6)).

When you push to a vector, Rust may reallocate memory - invalidating existing references. Rust’s borrow checker prevents this to ensure safety.


8.3. Iterating over a Vector

Iterating by reference:

#![allow(unused)]
fn main() {
let v = vec![100, 32, 57];
for i in &v {
    println!("{i}");
}
}

Iterating by mutable reference:

#![allow(unused)]
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
    *i += 50; // must dereference to modify
}
}

8.4. Storing Multiple Types in a Vector

Vectors store elements of one type. But using enums, we can store different variants:

#![allow(unused)]
fn main() {
enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];
}
  • Each variant is a single type (the enum).
  • The compiler knows the size and type of each element at compile time.

8.5. Strings

A String is a collection of UTF-8 encoded bytes, built on top of Vec<u8>.

Rust has two main string types:

  • String – an owned, growable, heap-allocated string.
  • &str – a string slice; a reference to a string (often a literal).

Creating a New String

#![allow(unused)]
fn main() {
let mut s = String::new();
}

Or create from a string literal:

#![allow(unused)]
fn main() {
let s = "initial contents".to_string();
}

Or using String::from():

#![allow(unused)]
fn main() {
let s = String::from("hello");
}

All three create a String.


Updating a String

Appending with push_str and push

#![allow(unused)]
fn main() {
let mut s = String::from("foo");
s.push_str("bar"); // foo + bar = foobar
}

push_str takes a string slice (&str) because it doesn’t take ownership.

#![allow(unused)]
fn main() {
let mut s = String::from("lo");
s.push('l'); // adds a single character
}

Concatenation with +

#![allow(unused)]
fn main() {
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;
}
  • s1 is moved and can’t be used after this.
  • &s2 is borrowed.
  • + actually calls add(self, s: &str).

Using format!

format! works like println! but returns a string:

#![allow(unused)]
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = format!("{s1}-{s2}-{s3}");
}
  • Doesn’t take ownership.
  • More flexible than +.

Indexing Strings

You can’t index strings directly like s[0].

Why?

Because Rust strings are UTF-8 encoded, and one character may take multiple bytes. For example, “नमस्ते” takes 18 bytes but only 6 characters.

#![allow(unused)]
fn main() {
let s = String::from("नमस्ते");
let h = s[0]; // ❌ compile-time error
}

Slicing Strings

You can slice a valid range:

#![allow(unused)]
fn main() {
let hello = "Здравствуйте";
let s = &hello[0..4]; // takes first 4 bytes, not characters
}

0..4 works only if it cuts cleanly between character boundaries.


Iterating over Strings

#![allow(unused)]
fn main() {
for c in "नमस्ते".chars() {
    println!("{c}");
}
}
  • .chars() iterates over Unicode scalar values.

To get raw bytes:

#![allow(unused)]
fn main() {
for b in "नमस्ते".bytes() {
    println!("{b}");
}
}

8.6. Hash Maps

A hash map (HashMap<K, V>) stores key-value pairs.

To use it:

#![allow(unused)]
fn main() {
use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
}

Accessing Values

#![allow(unused)]
fn main() {
let team_name = String::from("Blue");
let score = scores.get(&team_name);
}

get returns an Option<&V>.

To iterate:

#![allow(unused)]
fn main() {
for (key, value) in &scores {
    println!("{key}: {value}");
}
}

Ownership Rules with Hash Maps

For keys and values that implement Copy (like integers), values are copied in. For owned types like String, ownership is moved:

#![allow(unused)]
fn main() {
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
}

After this, both field_name and field_value are invalid - ownership moved.


Updating Hash Maps

Overwriting a Value

#![allow(unused)]
fn main() {
scores.insert(String::from("Blue"), 25);
}

This replaces the old value.


Inserting Only If Key Doesn’t Exist

#![allow(unused)]
fn main() {
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
}
  • entry() returns an enum Entry.
  • or_insert() inserts only if the key is missing.

Updating Based on Old Value

#![allow(unused)]
fn main() {
let text = "hello world wonderful world";
let mut map = HashMap::new();

for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1;
}
println!("{:?}", map);
}

This is a word counter using HashMap.


Chapter Summary

  • Vectors: Store lists of elements of the same type.
  • Strings: Collections of UTF-8 bytes - safe, explicit, and owned.
  • HashMaps: Store key-value pairs with full control over ownership and updates.

Each of these collections interacts with ownership and borrowing rules differently, but consistently. Rust’s rules guarantee that you can never have dangling references or memory leaks.

Day 10: Chapter 9 - Error Handling

Errors are inevitable in software, and Rust ensures you handle them before your program compiles. This makes your code more robust and less prone to runtime crashes. We will cover:

Screenshot 2025-10-27 at 4 33 21 PM

Rust divides errors into two categories:

  • Recoverable errors – issues like “file not found,” which can be fixed or retried.
  • Unrecoverable errors – bugs such as accessing memory out of bounds, where the program must stop.

Unlike languages with exceptions, Rust uses:

  • Result<T, E> for recoverable errors
  • panic! macro for unrecoverable errors

Unrecoverable Errors with panic!

When something goes fundamentally wrong, panic! stops program execution and prints an error message.

Example:

fn main() {
    panic!("crash and burn");
}

Output:

thread 'main' panicked at src/main.rs:2:5: crash and burn
note: run with `RUST_BACKTRACE=1` to display a backtrace

You can see where the error occurred and even get a full backtrace using:

RUST_BACKTRACE=1 cargo run

Rust unwinds the stack (cleans up memory) when panicking, but you can also make your program abort immediately by setting this in Cargo.toml:

[profile.release]
panic = 'abort'

Example of a runtime panic:

fn main() {
    let v = vec![1, 2, 3];
    v[99];
}

Rust prevents you from accessing invalid memory, unlike C, where this would be undefined behavior.


Recoverable Errors with Result<T, E>

When something might fail (like opening a file), use Result.

Example:

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");
}

Here, File::open returns:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

You can match on the result:

use std::fs::File;

fn main() {
    let greeting_file = match File::open("hello.txt") {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {error:?}"),
    };
}

If the file doesn’t exist, this will panic.


Handling Different Kinds of Errors

Sometimes you want to handle errors differently:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = match File::open("hello.txt") {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => File::create("hello.txt").unwrap(),
            _ => panic!("Problem opening the file: {error:?}"),
        },
    };
}

Simplifying with Closures

Instead of multiple match expressions:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {error:?}");
            })
        } else {
            panic!("Problem opening the file: {error:?}");
        }
    });
}

Cleaner and easier to read.


Shortcuts for Panic on Error: unwrap and expect

Both are shortcuts for match:

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

Or with a custom error message:

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

expect is preferred because it gives context when debugging.


Propagating Errors

Instead of handling errors immediately, you can pass them up the call stack.

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = match File::open("hello.txt") {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}
}

This allows the calling code to decide how to handle the error.


The ? Operator

A cleaner, shorter way to propagate errors.

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}
}

You can even chain:

#![allow(unused)]
fn main() {
fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();
    File::open("hello.txt")?.read_to_string(&mut username)?;
    Ok(username)
}
}

Or make it a one-liner:

#![allow(unused)]
fn main() {
use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}
}

Key Takeaways:

  • Use panic! for unrecoverable errors.
  • Use Result<T, E> for recoverable errors.
  • unwrap() and expect() are quick but risky; prefer proper error handling.
  • Use ? to propagate errors efficiently.
  • Rust enforces handling errors at compile time, ensuring safer code.

Chapter 10

We will cover:

Screenshot 2025-10-27 at 7 51 53 PM

Part 1: Generics, Traits

Rust provides powerful abstraction tools that let you write flexible and reusable code without sacrificing performance. The three main concepts we’ll study are:

  1. Generics - placeholders for types.
  2. Traits - shared behavior definitions for types.
  3. Lifetimes - constraints that ensure references remain valid.

Removing Duplication by Extracting a Function

Let’s begin with a simple example before we dive into generics.

Suppose you have a list of integers and want to find the largest number.

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let mut largest = &number_list[0];

    for number in &number_list {
        if number > largest {
            largest = number;
        }
    }

    println!("The largest number is {largest}");
}

This works fine for one list, but if you need to repeat this for multiple lists, you’d duplicate the code. Instead, you can extract the logic into a function:

fn largest(list: &[i32]) -> &i32 {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];
    let result = largest(&number_list);
    println!("The largest number is {result}");
}

Now your logic is reusable. You’ve abstracted the behavior (finding the largest number) into a function that works on any list of integers.


Generic Data Types

Now imagine you want to use the same function for characters (char) or floating-point numbers (f64).

If you tried to write separate functions:

#![allow(unused)]
fn main() {
fn largest_i32(list: &[i32]) -> &i32 { ... }
fn largest_char(list: &[char]) -> &char { ... }
}

You’d again have duplicated logic. Generics solve this problem.


Defining a Function with Generics

You can define a function that works for any comparable type:

#![allow(unused)]
fn main() {
fn largest<T>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}
}

Here, T is a generic type parameter - a placeholder for any type. However, this code will not compile yet because the compiler doesn’t know that T supports the > comparison operator.

To fix this, we add a trait bound (we’ll explain traits shortly):

#![allow(unused)]
fn main() {
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}
}

Now, Rust knows that T must be a type that implements the PartialOrd trait - meaning it supports comparisons like >, <, >=, etc.


Using Generics in Structs

Generics can also be used inside structs.

For example:

#![allow(unused)]
fn main() {
struct Point<T> {
    x: T,
    y: T,
}
}

This means x and y are of the same type T.

Example:

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

However, this won’t compile:

#![allow(unused)]
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}

because x and y must have the same type.

If you want to allow different types, use multiple generic parameters:

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}

Using Generics in Enums

Enums can also be generic.

You’ve already used them:

#![allow(unused)]
fn main() {
enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

These allow Option and Result to store any type of value - making them extremely versatile.

For instance:

#![allow(unused)]
fn main() {
let some_number = Some(5);
let some_string = Some("hello");
}

Option<T> becomes Option<i32> and Option<&str> here.


Using Generics in Methods

You can also define methods on structs or enums with generics.

For example:

#![allow(unused)]
fn main() {
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}
}

Here’s how you use it:

fn main() {
    let p = Point { x: 5, y: 10 };
    println!("p.x = {}", p.x());
}

Notice the syntax: impl<T> Point<T> - this tells Rust that we’re implementing methods for all Point<T> types, regardless of the specific T.

You can also restrict implementations to specific types:

#![allow(unused)]
fn main() {
impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}
}

Now, only Point<f32> instances have this method.


Generic Methods with Different Type Parameters

You can define methods that use different generic parameters from the struct.

struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };

    let p3 = p1.mixup(p2);

    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

Output:

p3.x = 5, p3.y = c

Here, we combined two different Point instances into one - reusing type parameters flexibly.


Performance of Generics

Rust’s generics have zero runtime cost due to a process called monomorphization.

Monomorphization means that during compilation, Rust replaces each instance of a generic function or type with a specific version for each concrete type it’s used with.

For example, this code:

#![allow(unused)]
fn main() {
let integer = Some(5);
let float = Some(5.0);
}

is turned into:

#![allow(unused)]
fn main() {
enum Option_i32 {
    Some(i32),
    None,
}

enum Option_f64 {
    Some(f64),
    None,
}
}

So at runtime, your program is as efficient as if you’d written all those versions by hand. Generics cost nothing at runtime - they’re purely a compile-time abstraction.


Traits: Defining Shared Behavior

A trait defines shared behavior - similar to interfaces in other languages.

For example, suppose you have two data types - NewsArticle and SocialPost - and you want both to provide a summary.

You define a trait:

#![allow(unused)]
fn main() {
pub trait Summary {
    fn summarize(&self) -> String;
}
}

This defines the signature of the summarize method. Any type that implements this trait must define its own version of this method.


Implementing a Trait for a Type

You can implement Summary for multiple types:

#![allow(unused)]
fn main() {
pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
}

Now both NewsArticle and SocialPost implement the Summary trait, each with its own behavior.

You can call the trait method just like any other method:

use aggregator::{SocialPost, Summary};

fn main() {
    let post = SocialPost {
        username: String::from("horse_ebooks"),
        content: String::from("of course, as you probably already know, people"),
        reply: false,
        repost: false,
    };

    println!("1 new post: {}", post.summarize());
}

Part 2: Lifetimes in Rust

1. Understanding Lifetimes

Lifetimes are a form of generic parameter in Rust that describe how long a reference is valid. While type generics (like <T>) describe what kind of data a function can handle, lifetime generics describe how long references to that data remain valid.

Rust uses lifetimes to ensure memory safety without garbage collection, by checking that no reference outlives the data it points to.

Every reference in Rust has an associated lifetime - the scope during which that reference is valid. In most cases, lifetimes are inferred automatically by the compiler, but sometimes we must explicitly annotate them to help the borrow checker understand how references relate to each other.


2. Preventing Dangling References with Lifetimes

The main goal of lifetimes is to prevent dangling references - situations where a reference points to memory that no longer exists.

Example (which does not compile):

fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    } // x goes out of scope here
    println!("r: {}", r);
}

Explanation

  • x is declared in the inner scope and destroyed when the inner scope ends.
  • r is declared in the outer scope, but it references x.
  • After the inner scope ends, x is gone, so r would point to invalid memory.

Rust’s borrow checker prevents this by rejecting the code with an error like:

error[E0597]: `x` does not live long enough

3. The Borrow Checker

The borrow checker analyzes lifetimes of all references to ensure:

"No reference outlives the data it points to."

Consider lifetimes 'a and 'b:

fn main() {
    let r; // ---------+-- 'a
    {      //          |
        let x = 5; // -+-- 'b
        r = &x;    // |    |
    }      // ------+    |
    println!("r: {r}"); // |
} // ---------------------+

Here 'b (the lifetime of x) is shorter than 'a (the lifetime of r). Rust compares these and rejects the code because r refers to data that does not live long enough.


4. Fixing Dangling References

The correct version ensures that the data (x) outlives the reference (r):

fn main() {
    let x = 5;
    let r = &x;
    println!("r: {r}");
}

Now, both the data and reference exist in the same scope - safe and valid.


5. Generic Lifetimes in Functions

Consider a function to find the longer of two string slices:

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

If you try this implementation:

#![allow(unused)]
fn main() {
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}
}

It fails with an error:

error[E0106]: missing lifetime specifier

Why?

The compiler doesn’t know whether the returned reference comes from x or y. Since each reference could have a different lifetime, Rust requires you to explicitly define their relationship.


6. Lifetime Annotation Syntax

Syntax examples:

#![allow(unused)]
fn main() {
&i32         // reference without lifetime
&'a i32      // reference with lifetime 'a
&'a mut i32  // mutable reference with lifetime 'a
}

Lifetime annotations don’t change how long something lives. They only describe how multiple references relate in terms of validity.


7. Lifetime Annotations in Function Signatures

We use angle brackets to declare lifetimes, just like type parameters.

Example of fixing longest:

#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
}

Explanation:

  • 'a is a generic lifetime parameter.
  • Both parameters x and y, and the return value, share the same lifetime 'a.
  • It tells Rust: “The returned reference will be valid as long as both x and y are valid - i.e., the smaller of their lifetimes.”

Now, Rust can verify this and the function compiles safely.


8. How Lifetimes Relate During Function Calls

Example where both inputs live long enough:

fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {result}");
    }
}

✅ Works fine because string2 lives long enough for the println!.

Now, an invalid case:

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    } // string2 dropped here
    println!("The longest string is {result}");
}

❌ Compilation fails because result might refer to string2, which has gone out of scope. Rust enforces this strictly, even if we know it would refer to string1 in this case.


9. Thinking in Terms of Lifetimes

If a function only depends on one reference’s lifetime, you can limit annotations accordingly:

#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}
}

Here, 'a applies only to x and the return value. y is independent.


10. Returning References to Local Data (Dangling References)

Invalid example:

#![allow(unused)]
fn main() {
fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}
}

This fails because result is created inside the function and destroyed when the function ends, leaving a dangling reference. Lifetime annotations can’t fix this; the correct solution is to return owned data instead:

#![allow(unused)]
fn main() {
fn longest(x: &str, y: &str) -> String {
    String::from("really long string")
}
}

11. Lifetimes in Struct Definitions

When a struct holds references, each reference needs a lifetime annotation:

#![allow(unused)]
fn main() {
struct ImportantExcerpt<'a> {
    part: &'a str,
}
}

Example usage:

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

This ensures that ImportantExcerpt cannot outlive the String it borrows from.


12. Lifetime Elision (Automatic Inference Rules)

Rust has lifetime elision rules that allow omitting explicit lifetimes in common cases. These are compiler heuristics based on frequent patterns.

The Three Lifetime Elision Rules

  1. Each input reference gets its own lifetime.

    #![allow(unused)]
    fn main() {
    fn foo(x: &i32) → fn foo<'a>(x: &'a i32)
    fn foo(x: &i32, y: &i32) → fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
    }
  2. If there’s exactly one input lifetime, it’s assigned to all output lifetimes.

    #![allow(unused)]
    fn main() {
    fn foo<'a>(x: &'a i32) -> &'a i32
    }
  3. If there are multiple input lifetimes and one of them is &self (a method), the lifetime of self is assigned to all output lifetimes.


Applying to an Example

The function:

#![allow(unused)]
fn main() {
fn first_word(s: &str) -> &str {
    // ...
}
}

Follows these rules:

  1. Input &str gets 'a.
  2. Output lifetime matches input lifetime 'a.

So effectively:

#![allow(unused)]
fn main() {
fn first_word<'a>(s: &'a str) -> &'a str
}

No explicit annotation is needed because it fits Rust’s rules.


13. Lifetime Annotations in Method Definitions

If a struct has lifetimes, you must declare them after impl:

#![allow(unused)]
fn main() {
impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}
}

Here, no explicit lifetime is needed for self - elision handles it.

Another example:

#![allow(unused)]
fn main() {
impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}
}

Explanation:

  • Both inputs (&self and announcement) get their own lifetimes.
  • Because one input is &self, the return type takes on self’s lifetime (rule 3).
  • So no explicit lifetime annotation is required.

14. The 'static Lifetime

The 'static lifetime indicates that a reference lives for the entire duration of the program.

Example:

#![allow(unused)]
fn main() {
let s: &'static str = "I have a static lifetime.";
}

String literals are stored in the binary, not on the stack, so they exist for the program’s whole runtime.

However, you should not use 'static to “force” a reference to compile unless it genuinely has a static lifetime. It’s often better to fix the underlying ownership or scope issue.


Final Summary

  • Every reference has a lifetime (its valid scope).
  • Rust’s borrow checker ensures no reference outlives its data.
  • Lifetime annotations describe relationships between references when the compiler cannot infer them automatically.
  • Lifetime elision rules handle common patterns automatically.
  • Lifetimes apply to functions, structs, and methods to ensure valid borrowing.
  • The 'static lifetime means data lives for the whole program duration.

Together, Generics, Traits, and Lifetimes form the foundation of safe, reusable, and performant code in Rust - giving developers the power of abstraction without sacrificing memory safety or performance.

Day 12 - I/O Project

Coming soon.

Day 13 - Iterators & Closures

Coming soon.