Lecture 1 / 12
Lecture 01 Β· Fundamentals

Introduction to Rust & Setup

Beginner ~50 min

What is Rust?

Rust is a modern systems programming language focused on performance, memory safety, and thread safety without a garbage collector. It is designed to provide the control of a low-level language (like C or C++) while ensuring that the programmer cannot make common mistakes that lead to crashes or security vulnerabilities.

Created by Mozilla and now governed by the Rust Foundation, Rust introduces a unique concept called the Ownership System. This system allows the compiler to manage memory automatically and safely at compile-time, meaning your program doesn't need a "garbage collector" to pause execution and clean up memory.

Why Rust?

  • Zero-cost Abstractions: You can use high-level programming features (like iterators and closures) without any performance penalty compared to writing it manually in a low-level way.
  • Memory Safety: Rust prevents "Null Pointer" errors and "Buffer Overflows" by checking your code during compilation. If it compiles, it is memory-safe.
  • Fearless Concurrency: Rust's type system prevents "Data Races" (where two threads try to change the same piece of data at once), making multi-threaded programming much safer.
  • Growing Ecosystem: From high-performance web servers (Actix, Axum) to WebAssembly (WASM) and embedded systems, Rust is versatile.

Rust consistently ranks as one of the most loved programming languages in the Stack Overflow Developer Survey because it gives developers confidence that their code will run safely and efficiently.

Installation & Toolchain

The best way to install Rust is via rustupβ€”the official toolchain installer. It not only installs the compiler but also manages different versions of Rust and its essential tools.

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

After installation, restart your terminal. You can update Rust anytime using rustup update.

Windows users: The installer will guide you through setting up the Visual C++ Build Tools if needed. This is required because Rust relies on these tools to link your code into an executable.

Recommended IDE Setup

While you can use any text editor, the industry standard for Rust is Visual Studio Code with the following extension:

  • rust-analyzer: This is essential. It provides autocomplete, type hints, and real-time error checking as you type.

Understanding Cargo

In Rust, you rarely call the compiler (rustc) directly. Instead, you use Cargo, Rust's all-in-one build system and package manager.

Cargo handles three main tasks:

  • Building your code: Compiling your project and managing dependencies.
  • Downloading Crates: "Crates" are Rust libraries. Cargo downloads them from crates.io automatically.
  • Managing Projects: It organizes your folder structure and handles versioning.

Your First Rust Program

Every Rust program starts with a main() function β€” this is the entry point of your application.

main.rs
fn main() {
    println!("Hello, Rust Mastery!");
    println!("Welcome to safe systems programming!");
}
Output
Hello, Rust Mastery!
Welcome to safe systems programming!

Breaking Down the Code

  • fn keyword: Used to declare a function. In Rust, functions are the primary way to group logic.
  • println!: Notice the !. This tells Rust that println! is a Macro, not a regular function. Macros are used when a function needs to do complex things, like varying the number of arguments it can take.
  • Semicolons (;): Every statement in Rust must end with a semicolon. If you omit it, Rust might treat the line as a return value.
  • Static Typing: Rust knows exactly what every piece of data is at compile time, which is why it's so fast.

πŸ’» Try It Yourself - Multi-Language Compiler

Practice Rust and many other programming languages right here in your browser! Switch between languages, modify the code, and click "Run" to see results instantly.

πŸ’‘ Practice Tips:

  • Switch to Rust in the language selector and try the ownership examples
  • Experiment with Rust's memory safety features and borrowing
  • Try other systems languages like C++, Go, or C to compare safety features
  • Use the "Load Example" button to see Rust-specific code samples
  • Use Ctrl+Enter to quickly run your code
🎯 Exercise 1.1

Create your first project using Cargo. Run cargo new hello_rust in your terminal, then modify src/main.rs to print your name and your goal for learning Rust. Run it using cargo run.

Experiment with build modes. Try using cargo run --release. Notice that it takes longer to compile, but the resulting binary is highly optimized for speed.

Pro Tip: Use cargo check frequently. It checks if your code will compile without actually building the binary, which is much faster and helps you catch errors quickly.

Lecture 02 Β· Fundamentals

Variables & Ownership

Beginner ~45 min

Introduction to Variables

Variables are used to store data in memory. In Rust, variables are safe, predictable, and designed to prevent many common programming errors.

Rust focuses heavily on memory safety and performance, which is why variables and ownership are such important concepts in the language.

Immutability by Default

In Rust, variables are immutable by default. This means their values cannot be changed after they are created.

Immutability helps make programs safer and easier to understand because values remain predictable.

let x = 5;
// x = 6; // Error!

let mut y = 10;
y = 11; // OK

In the example above:

  • x is immutable and cannot be changed
  • y is mutable because it uses the mut keyword

Why Rust Uses Immutability

Rust encourages immutability because it reduces bugs and makes concurrent programming safer.

Immutable variables cannot accidentally change during program execution, making debugging easier.

Variable Declaration Syntax

Variables in Rust are declared using the let keyword.

let name = "Rust";
let age = 5;
let is_fast = true;

Rust automatically infers the data type in many situations.

Explicit Type Annotation

You can explicitly define variable types when needed.

let score: i32 = 100;
let price: f64 = 19.99;

Type annotations are useful when Rust cannot infer the type automatically.

Common Rust Data Types

Type Description Example
i32 32-bit integer 10
f64 64-bit floating point 3.14
bool Boolean value true
char Single Unicode character 'A'
&str String slice "Hello"

Mutable Variables

Mutable variables allow values to change during program execution.

let mut counter = 0;

counter = counter + 1;

println!("{}", counter);

Use mutable variables only when necessary to maintain code safety and clarity.

Shadowing Variables

Rust allows variables to be redefined using the same name. This feature is called shadowing.

let number = 5;

let number = number + 1;

println!("{}", number);

Unlike mutation, shadowing creates a completely new variable.

Difference Between Mutation and Shadowing

Mutation Shadowing
Uses mut Uses another let
Changes the same variable Creates a new variable
Cannot change type Can change type

Introduction to Ownership

Ownership is one of Rust’s most important concepts. It allows Rust to manage memory safely without needing a garbage collector.

Each value in Rust has a variable that's called its owner. When the owner goes out of scope, the value is dropped.

The Three Ownership Rules

  • Each value in Rust has one owner
  • There can only be one owner at a time
  • When the owner goes out of scope, the value is automatically dropped

Ownership and Scope

Scope defines where a variable is valid.

{
    let language = "Rust";

    println!("{}", language);
}

// language no longer exists here

Once the block ends, Rust automatically cleans up the variable from memory.

Ownership Transfer

When assigning certain types to another variable, ownership moves to the new variable.

let s1 = String::from("hello");
let s2 = s1;

// println!("{}", s1); // Error

After the move, s1 is no longer valid because ownership transferred to s2.

Cloning Data

If you want both variables to remain valid, use clone().

let s1 = String::from("Rust");
let s2 = s1.clone();

println!("{}", s1);
println!("{}", s2);

The clone() method creates a deep copy of the data.

Copy Types

Simple types such as integers are copied automatically instead of moved.

let x = 5;
let y = x;

println!("{}", x);
println!("{}", y);

Both variables remain valid because integers implement the Copy trait.

References and Borrowing

Rust allows borrowing data using references so ownership does not transfer.

fn print_text(text: &String) {
    println!("{}", text);
}

let message = String::from("Hello");

print_text(&message);

println!("{}", message);

The ampersand & creates a reference to the value.

Benefits of Ownership

  • Prevents memory leaks
  • Prevents dangling pointers
  • Improves memory safety
  • Eliminates the need for garbage collection

Common Beginner Mistakes

  • Trying to use variables after ownership moves
  • Forgetting to use mut for mutable variables
  • Confusing references with copies
  • Using values outside their scope

Practice Exercise

Create a mutable variable called count and increase its value.

Then create a String variable and experiment with ownership transfer and cloning.

let mut count = 1;

count = count + 1;

println!("{}", count);

let name = String::from("Rust");
let copied_name = name.clone();

println!("{}", copied_name);

Summary

In this lecture, you learned:

  • How Rust variables work
  • The difference between mutable and immutable variables
  • How ownership manages memory safely
  • How values move and clone in Rust
  • How borrowing and references work
Lecture 03 Β· Fundamentals

Data Types & Functions

Beginner ~50 min

Basic (Scalar) Data Types

Go is a statically typed language, meaning every variable must have a defined type. Here are the most common basic types:

  • Integers: Used for whole numbers. The most common is int, but you can use specific sizes like int8, int16, int32, int64 (and their unsigned counterparts uint).
  • Floating-point: Used for decimals. Go provides float32 and float64 (the default for decimals).
  • Boolean: The bool type represents either true or false.
  • String: The string type represents a sequence of characters, wrapped in double quotes "like this".

Type Inference

You don't always have to explicitly declare the type. Using the short declaration operator :=, Go infers the type automatically based on the value assigned.

main.go
var age int = 25 // Explicit declaration
name := "Alice"       // Inferred as string
isReady := true        // Inferred as bool

Compound Types

Compound types allow you to group multiple values together.

  • Arrays: Fixed-size sequences of the same type.
    var arr [5]int
  • Slices: Dynamic-size sequences. Slices are the most used "list" type in Go.
    numbers := []int{1, 2, 3}
  • Maps: Key-value pairs (dictionaries).
    emails := map[string]string{"John": "john@mail.com"}
  • Structs: Custom types that group different types of data together.
    type User struct { Name string; Age int }

Functions

Functions are the building blocks of Go. They are declared using the func keyword.

Basic Function Syntax

main.go
func add(a int, b int) int {
    return a + b
}

// Usage:
result := add(10, 20) // result is 30

Multiple Return Values

One of Go's most powerful features is the ability for a function to return more than one value. This is commonly used to return a result and an error simultaneously.

main.go
func divide(a, b float64) (float64, string) {
    if b == 0 {
        return 0, "Division by zero error!"
    }
    return a / b, "Success"
}

// Usage:
val, msg := divide(10, 0)
fmt.Println(msg) // Prints: Division by zero error!

Variadic Functions

A variadic function can take an unlimited number of arguments of a specific type using the ... syntax.

main.go
func sumAll(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

// Usage:
fmt.Println(sumAll(1, 2, 3, 4, 5)) // result: 15
🎯 Exercise 3.2

Create a program that:

  1. Defines a struct called Student with fields for Name (string) and Grade (float64).
  2. Writes a function called checkPass that takes a Student struct and returns a bool (true if grade >= 50, otherwise false).
  3. Create a slice of 3 Students and use a loop to print whether each student passed or failed.

Lecture 04 Β· Fundamentals

Control Flow

Beginner ~40 min

Introduction to Control Flow

Control flow determines how a program makes decisions and repeats actions. In Rust, control flow structures allow programs to react dynamically to different conditions.

The most common control flow tools are:

  • if expressions
  • else statements
  • Loops
  • match expressions

If Expressions

Rust uses if expressions to execute code only when a condition is true.

let condition = true;

let number = if condition { 5 } else { 6 };

In this example:

  • If condition is true, number becomes 5
  • Otherwise, number becomes 6

If Expressions Return Values

Unlike many programming languages, Rust’s if statements are expressions. This means they can return values.

let age = 18;

let status = if age >= 18 {
    "Adult"
} else {
    "Minor"
};

println!("{}", status);

Both branches must return the same type.

Boolean Conditions

Conditions inside an if statement must evaluate to a boolean value.

let number = 10;

if number > 5 {
    println!("Number is greater than 5");
}

Rust does not automatically convert integers into booleans.

Using Else Statements

The else keyword defines code that executes when the condition is false.

let logged_in = false;

if logged_in {
    println!("Welcome back!");
} else {
    println!("Please log in");
}

Else If Conditions

Use else if when checking multiple conditions.

let score = 85;

if score >= 90 {
    println!("Grade A");
} else if score >= 75 {
    println!("Grade B");
} else {
    println!("Grade C");
}

Conditions are checked from top to bottom.

Infinite Loops

The loop keyword creates an infinite loop that continues until manually stopped.

loop {
    println!("Running forever");
}

Infinite loops are useful for game engines, servers, and continuous processes.

Breaking Out of Loops

Use the break keyword to stop a loop.

let mut counter = 0;

loop {
    counter += 1;

    if counter == 5 {
        break;
    }
}

println!("Loop ended");

Returning Values from Loops

Rust loops can return values using break.

let mut counter = 0;

let result = loop {
    counter += 1;

    if counter == 10 {
        break counter * 2;
    }
};

println!("{}", result);

Output:

20

While Loops

The while loop repeats while a condition remains true.

let mut number = 3;

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

    number -= 1;
}

println!("LIFTOFF!");

For Loops

The for loop is commonly used to iterate through collections or ranges.

for number in 1..5 {
    println!("{}", number);
}

The range 1..5 includes 1 up to 4.

Inclusive Ranges

Use ..= to include the final value in the range.

for number in 1..=5 {
    println!("{}", number);
}

This prints numbers from 1 to 5.

Looping Through Arrays

You can iterate through array elements using a for loop.

let fruits = ["Apple", "Banana", "Orange"];

for fruit in fruits {
    println!("{}", fruit);
}

The Match Expression

The match expression is Rust’s powerful pattern matching system.

let number = 2;

match number {
    1 => println!("One"),
    2 => println!("Two"),
    3 => println!("Three"),
    _ => println!("Other"),
}

The underscore _ acts as a default case.

Why Match is Powerful

The match statement is safer and more expressive than many traditional switch statements.

  • Checks all possible cases
  • Supports pattern matching
  • Improves readability
  • Works with enums and complex data types

Nested Control Flow

Control flow structures can be nested inside one another.

let age = 20;
let has_ticket = true;

if age >= 18 {
    if has_ticket {
        println!("Entry allowed");
    }
}

Common Beginner Mistakes

  • Using non-boolean values as conditions
  • Creating infinite loops accidentally
  • Forgetting to update loop counters
  • Using mismatched return types in if expressions
  • Missing the default case in match

Practice Exercise

Create a program that:

  • Checks whether a number is even or odd
  • Prints numbers from 1 to 10 using a loop
  • Uses match to display the name of a weekday
let number = 8;

if number % 2 == 0 {
    println!("Even");
} else {
    println!("Odd");
}

Summary

In this lecture, you learned:

  • How Rust control flow works
  • How to use if expressions
  • How loops operate in Rust
  • How to use while and for loops
  • How powerful match expressions are
Lecture 05 Β· Fundamentals

Loops & Iterators

Beginner ~45 min

Looping with 'for'

The for loop is the most idiomatic way to iterate in Rust. Instead of using a counter and indexing into a collection, Rust uses the IntoIterator trait, which allows the loop to safely traverse elements without risk of "out of bounds" errors.

main.rs
for i in 1..=5 {
    println!("{}", i);
}

Range Syntax: Rust uses ranges to create sequences of numbers efficiently.

  • 1..5 β†’ 1, 2, 3, 4 (Exclusive: stops before 5)
  • 1..=5 β†’ 1, 2, 3, 4, 5 (Inclusive: includes 5)
  • 0..10 β†’ Common for indexing arrays from 0 to 9
main.rs
// Advanced range operations
for i in 0..10 { ... }           // Standard 0 to 9
for i in (1..=10).step_by(2) { ... } // 1, 3, 5, 7, 9
for i in (1..100).rev() { ... }     // Countdown from 99 to 1

Iterating over Arrays, Vectors & Strings

When iterating over collections, you must decide how you want to handle Ownership. There are three main ways to create an iterator:

  • iter(): Borrows each element immutably (&T). The original collection stays intact.
  • iter_mut(): Borrows each element mutably (&mut T). Allows you to change values inside the loop.
  • into_iter(): Consumes the collection and takes ownership of the elements (T). The original collection is destroyed.
main.rs
let fruits = ["Apple", "Banana", "Mango"];

// 1. Basic iteration (borrowing)
for fruit in fruits {
    println!("I like {}", fruit);
}

// 2. Iterating with the index using .enumerate()
for (index, fruit) in fruits.iter().enumerate() {
    println!("Fruit # {}: {}", index, fruit);
}

while Loop

The while loop is used when the number of iterations is not known in advance, and you want to continue as long as a specific condition remains true.

main.rs
let mut number = 1;

while number <= 5 {
    println!("Number: {}", number);
    number += 1; // Must manually increment to avoid infinite loop
}

The Powerful 'loop' Keyword

loop creates an infinite loop. Unlike while, it doesn't check a condition at the start. It is often used for server listening loops or retry logic where you want the program to run until a specific break condition is met.

main.rs
let mut counter = 0;

loop {
    counter += 1;
    
    if counter > 10 {
        break; // Exit the loop immediately
    }
    
    if counter % 2 == 0 {
        continue;     // Skip the rest of this iteration
    }
    
    println!("Counter: {}", counter);
}

Returning Values from Loops

One of Rust's most unique features is that loop is an expression. This means you can return a value from a loop by passing it to the break keyword.

main.rs
let mut counter = 0;

let result = loop {
    counter += 1;
    
    if counter == 10 {
        break counter * 2;   // This value is assigned to 'result'
    }
};
println!("Final Result: {}", result); // Result: 20

Iterators in Rust (Functional Approach)

Rust’s real power lies in its iterator system. Iterators are lazy, meaning they do nothing until you call a "consuming" method like collect() or a for loop.

Commonly used adapter methods:

  • map(): Transforms each element.
  • filter(): Keeps only elements that match a condition.
  • sum(): Adds all elements together.
  • collect(): Transforms the iterator back into a collection (like a Vec).
main.rs
let numbers = vec![1, 2, 3, 4, 5];

// 1. Summing elements
let sum: i32 = numbers.iter().sum();

// 2. Chaining methods: Filter and then Map
let squared_evens: Vec = numbers.iter()
    .filter(|&x| x % 2 == 0) // Keep only even numbers
    .map(|x| x * x)            // Square them
    .collect();                // Turn back into a Vector

println!("Sum: {}", sum);
println!("Squared Evens: {:?}", squared_evens);

Pro Tips:

  • Prefer for loops over while when possible β€” they are more idiomatic and prevent infinite loops caused by forgetting to increment a counter.
  • When in doubt, use iter(). Only use into_iter() if you specifically need to destroy the original collection to move its data.
  • Remember that map and filter are lazy. If you don't call collect() or a loop, the code inside them will never even run!
Lecture 06 Β· Core Concepts

Ownership & Borrowing

Intermediate ~65 min

Introduction to Ownership

Ownership is one of the most unique and important concepts in Rust. It is the system that allows Rust to manage memory safely without using a garbage collector.

Instead of automatically cleaning unused memory at random times, Rust follows strict ownership rules at compile time. These rules prevent memory leaks, dangling pointers, and data races.

Understanding ownership is essential because almost every Rust program depends on it.

The Three Ownership Rules

Rust ownership is based on three core rules:

  • Each value in Rust has a variable called its owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value is dropped.

These rules make memory management automatic and safe.

Variable Scope

A variable is only valid inside the scope where it is declared.

fn main() {

    {
        let name = "Rust";
        println!("{}", name);
    }

    // name is no longer valid here
}

When the scope ends, Rust automatically frees the memory associated with the variable.

Ownership Transfer

Some values are moved instead of copied. After a move occurs, the original variable can no longer be used.

fn main() {

    let s1 = String::from("hello");

    let s2 = s1;

    // println!("{}", s1);
}

In this example, ownership of the string moves from s1 to s2. The variable s1 becomes invalid after the move.

Why Rust Uses Moves

Rust uses move semantics to prevent multiple variables from trying to free the same memory location.

This eliminates common bugs such as:

  • Double free errors
  • Dangling pointers
  • Memory corruption

Copy Types

Simple primitive values are copied instead of moved because they are stored directly on the stack.

fn main() {

    let x = 10;

    let y = x;

    println!("{}", x);
    println!("{}", y);
}

Integers, booleans, characters, and floating-point numbers usually implement the Copy trait.

Cloning Data

If you want to create a deep copy of heap data, you can use the clone() method.

fn main() {

    let s1 = String::from("Rust");

    let s2 = s1.clone();

    println!("{}", s1);
    println!("{}", s2);
}

Unlike moves, cloning creates a completely separate copy of the data.

Ownership and Functions

Passing a variable to a function may transfer ownership depending on the type.

fn print_string(text: String) {
    println!("{}", text);
}

fn main() {

    let msg = String::from("Hello");

    print_string(msg);

    // msg is no longer valid here
}

The function takes ownership of the string parameter.

Returning Ownership

Functions can also return ownership back to the caller.

fn give_back(s: String) -> String {
    s
}

This allows ownership to move safely between functions.

References & Borrowing

Instead of transferring ownership, you can "borrow" a value using references (&).

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

Borrowing allows a function to use a value without taking ownership of it.

This means the original variable remains valid after the function call.

fn main() {

    let text = String::from("Rust");

    let length = calculate_length(&text);

    println!("Length: {}", length);

    println!("{}", text);
}

Mutable References

By default, references are immutable. To modify borrowed data, you must use mutable references.

fn change(text: &mut String) {
    text.push_str(" language");
}

Mutable references allow safe modification while still preventing dangerous memory access.

fn main() {

    let mut name = String::from("Rust");

    change(&mut name);

    println!("{}", name);
}

Borrowing Rules

Rust enforces strict borrowing rules to guarantee memory safety.

  • You can have multiple immutable references at the same time.
  • You can only have one mutable reference at a time.
  • You cannot combine mutable and immutable references simultaneously.

Multiple Immutable References

let s = String::from("hello");

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

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

This is allowed because immutable references only read data.

Single Mutable Reference

let mut s = String::from("hello");

let r1 = &mut s;

Rust prevents multiple mutable references because simultaneous modification could cause data races.

Dangling References

Rust prevents dangling references at compile time.

// This code will not compile

fn dangle() -> &String {

    let s = String::from("hello");

    &s
}

The compiler stops this because the local variable would be destroyed after the function ends.

Slices and Borrowing

Slices are references to parts of collections without taking ownership.

let word = &text[0..4];

Slices are heavily used with strings and arrays.

Benefits of Ownership

  • Memory safety without garbage collection.
  • Prevention of data races.
  • Better performance.
  • Automatic resource cleanup.
  • Safer concurrent programming.

Common Beginner Mistakes

  • Trying to use variables after ownership has moved.
  • Creating multiple mutable references.
  • Confusing cloning with borrowing.
  • Forgetting to use mut for mutable references.
  • Returning references to local variables.

Summary

Ownership and borrowing are the foundation of Rust’s memory safety model. Instead of relying on garbage collection, Rust uses compile-time rules to manage memory safely and efficiently.

Although ownership may feel difficult at first, mastering it allows developers to write high-performance and reliable systems software with confidence.

Lecture 07 Β· Core Concepts

Structs & Enums

Intermediate ~60 min

Introduction to Structs

Structs are custom data types that allow you to group related values together. They are similar to objects or classes in other programming languages, but Rust structs only store data.

Structs help organize code and make programs easier to understand and maintain.

Defining a Struct

A struct is created using the struct keyword.

struct User {
    username: String,
    email: String,
    active: bool,
}

This struct stores information related to a user account.

Creating Struct Instances

You can create instances of a struct by providing values for each field.

let user1 = User {
    username: String::from("rustacean"),
    email: String::from("rust@example.com"),
    active: true,
};

Each field must receive a value of the correct type.

Accessing Struct Fields

Struct fields are accessed using dot notation.

println!("{}", user1.username);

This prints the value stored inside the username field.

Mutable Structs

To modify struct fields, the struct instance must be mutable.

let mut user1 = User {
    username: String::from("rustacean"),
    email: String::from("rust@example.com"),
    active: true,
};

user1.active = false;

Without the mut keyword, Rust will prevent modifications.

Struct Update Syntax

Rust provides a convenient way to create new struct instances using existing values.

let user2 = User {
    email: String::from("new@example.com"),
    ..user1
};

The ..user1 syntax copies remaining fields from another struct instance.

Tuple Structs

Tuple structs are structs without named fields.

struct Color(u8, u8, u8);

let black = Color(0, 0, 0);

Tuple structs are useful when field names are unnecessary.

Unit-Like Structs

Rust also supports structs without fields.

struct Logger;

These are commonly used for traits or marker types.

Methods on Structs

Methods are defined using impl blocks.

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

impl Rectangle {

    fn area(&self) -> u32 {
        self.width * self.height
    }
}

The &self parameter refers to the current struct instance.

Associated Functions

Associated functions belong to a struct but do not use self.

impl Rectangle {

    fn square(size: u32) -> Rectangle {

        Rectangle {
            width: size,
            height: size,
        }
    }
}

These functions are often used as constructors.

Introduction to Enums

Enums allow a value to be one of several possible variants. They are extremely powerful and widely used in Rust.

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

The variable can only hold one variant at a time.

Enums With Data

Enum variants can also store additional data.

enum Message {
    Text(String),
    Number(i32),
    Quit,
}

This makes enums much more flexible than traditional enums in many other languages.

The Option Enum

Rust uses the Option enum to represent optional values safely.

let some_number = Some(5);

let no_value: Option<i32> = None;

The Option type helps prevent null pointer errors.

Pattern Matching

Pattern matching allows you to check enum variants and execute different code depending on the result.

match some_option {

    Some(i) => println!("{}", i),

    None => println!("None"),
}

The match statement is one of Rust’s most powerful control flow features.

Match Expressions

Every possible pattern must be handled in a match expression.

let number = 3;

match number {

    1 => println!("One"),

    2 => println!("Two"),

    3 => println!("Three"),

    _ => println!("Other"),
}

The underscore _ acts as a default case.

The if let Syntax

When you only care about one pattern, if let provides a shorter alternative.

if let Some(x) = some_option {
    println!("{}", x);
}

This is cleaner than writing a full match expression for simple cases.

The Result Enum

Rust also uses enums for error handling through the Result type.

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

This allows programs to safely handle operations that may fail.

Benefits of Structs and Enums

  • Improve code organization.
  • Create custom data models.
  • Provide safer error handling.
  • Reduce bugs caused by invalid states.
  • Support powerful pattern matching.

Common Beginner Mistakes

  • Forgetting to make structs mutable.
  • Confusing tuple structs with tuples.
  • Ignoring all enum variants in match statements.
  • Trying to use null instead of Option.
  • Moving ownership accidentally when updating structs.

Practice

Practice

Create a struct called Book with fields for title, author, and pages.

Then create an enum called Status with variants:

  • Available
  • Borrowed
  • Reserved

Use a match statement to print different messages for each status.

Summary

Structs and enums are fundamental building blocks in Rust. Structs organize related data, while enums represent multiple possible states safely and efficiently.

Combined with pattern matching, these features allow Rust developers to write expressive, safe, and maintainable programs.

Lecture 08 Β· Core Concepts

Collections & Strings

Intermediate ~50 min

Introduction to Collections

Collections are data structures used to store multiple values together. Rust provides several powerful collection types that help developers manage and organize data efficiently.

Unlike arrays, many collections in Rust are dynamic, meaning they can grow or shrink during program execution.

The most commonly used collections include:

  • Vectors
  • Strings
  • Hash Maps

These collections are stored on the heap, allowing flexible memory usage.

Vectors (Dynamic Arrays)

Vectors are dynamic arrays that can store multiple values of the same type. They are one of the most commonly used collections in Rust.

let mut v = vec![1, 2, 3];

v.push(4);

The vec! macro creates a vector containing initial values.

The push() method adds new elements to the end of the vector.

Creating Empty Vectors

You can also create an empty vector and add elements later.

let mut numbers: Vec<i32> = Vec::new();

numbers.push(10);
numbers.push(20);

Because the vector starts empty, Rust requires an explicit type declaration.

Accessing Vector Elements

Vector elements can be accessed using indexing or the safer get() method.

let first = v[0];

match v.get(1) {
    Some(value) => println!("{}", value),
    None => println!("No value found"),
}

Using get() is safer because it prevents runtime crashes caused by invalid indexes.

Iterating Through Vectors

You can loop through vector elements using a for loop.

for value in &v {
    println!("{}", value);
}

The &v creates an immutable reference so ownership is not moved.

Modifying Vector Elements

Mutable references allow vector elements to be modified during iteration.

for value in &mut v {
    *value += 1;
}

The * operator dereferences the mutable reference to access the actual value.

Vector Memory and Ownership

Vectors follow Rust’s ownership rules. When a vector is assigned to another variable, ownership moves unless the vector is cloned.

let v1 = vec![1, 2, 3];

let v2 = v1;

// v1 is no longer valid

To create a full copy, use the clone() method.

let v2 = v1.clone();

Strings in Rust

Rust provides two main string types:

  • String
  • &str (string slice)

A String is growable and heap-allocated, while &str is an immutable reference to string data.

Creating Strings

let name = String::from("Rust");

let language = "Programming";

The first variable creates an owned string, while the second is a string slice.

Appending to Strings

Strings can grow dynamically using methods such as push_str() and push().

let mut text = String::from("Hello");

text.push_str(" World");

text.push('!');

The final string becomes Hello World!.

String Concatenation

Strings can be combined using the + operator or the format! macro.

let s1 = String::from("Hello");

let s2 = String::from(" Rust");

let s3 = s1 + &s2;

The + operator moves ownership of the first string.

The format! macro is often cleaner and safer.

let message = format!("{} {}", s1, s2);

String Slices

String slices allow references to parts of a string without taking ownership.

let text = String::from("Rustacean");

let slice = &text[0..4];

println!("{}", slice);

This slice contains the value Rust.

UTF-8 and Strings

Rust strings are stored as UTF-8 encoded text. This allows support for international languages and Unicode characters.

Because UTF-8 characters may use multiple bytes, direct indexing into strings is not allowed.

// This is invalid in Rust

// let ch = text[0];

Instead, Rust encourages safer approaches such as iterators.

Iterating Over Characters

for ch in text.chars() {
    println!("{}", ch);
}

The chars() method iterates through Unicode characters safely.

Hash Maps

Hash maps store data as key-value pairs.

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert("Alice", 90);

scores.insert("Bob", 85);

Hash maps are useful for fast lookups and storing related data.

Accessing Hash Map Values

match scores.get("Alice") {

    Some(score) => println!("{}", score),

    None => println!("No score found"),
}

The get() method returns an Option because the key may not exist.

Ownership and Collections

Collections often take ownership of inserted values.

let name = String::from("Rust");

scores.insert(name, 100);

// name is no longer valid

This behavior prevents invalid memory access and ensures safety.

Benefits of Rust Collections

  • Memory safety
  • High performance
  • Flexible dynamic storage
  • Powerful ownership tracking
  • Efficient data management

Common Beginner Mistakes

  • Trying to index strings directly.
  • Forgetting ownership transfer in collections.
  • Using indexing without checking bounds.
  • Confusing String and &str.
  • Mutating immutable vectors.

Practice

Practice

Create a vector that stores five numbers and write a loop that prints only even numbers.

Then create a string and append additional text using push_str().

Summary

Collections and strings are essential building blocks in Rust. Vectors provide dynamic storage, strings allow safe UTF-8 text handling, and hash maps organize key-value data efficiently.

Mastering these collections is important for building real-world Rust applications, APIs, command-line tools, and systems software.

Lecture 09 Β· Advanced

Error Handling

Advanced ~60 min

Introduction to Error Handling

Error handling is an essential part of writing reliable and safe programs. Rust provides a powerful system that helps developers handle errors explicitly instead of ignoring them.

Unlike many languages that rely heavily on exceptions, Rust encourages developers to handle failures using types such as Result<T, E> and Option<T>.

  • Improves application stability
  • Prevents unexpected crashes
  • Encourages safer programming practices
  • Makes failures more predictable

Panic vs Result

Rust groups errors into two categories:

  • Recoverable errors using Result<T, E>
  • Unrecoverable errors using panic!

Recoverable errors are situations the program can handle gracefully, while unrecoverable errors indicate serious bugs or invalid states.

Understanding panic!

The panic! macro immediately stops program execution when something goes critically wrong.

fn main() {
    panic!("Something went wrong!");
}

When a panic occurs, Rust prints an error message and stack trace.

Common Causes of Panic

  • Accessing invalid array indexes
  • Calling unwrap() on an error value
  • Invalid assumptions in the program
  • Unexpected runtime states

Recoverable Errors with Result

The Result enum represents operations that may succeed or fail.

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

Ok contains a successful value, while Err contains error information.

Reading Files with Result

Many standard library functions return a Result.

use std::fs::File;

fn main() {

    let file = File::open("data.txt");

    match file {

        Ok(f) => println!("File opened!"),

        Err(error) => println!("Error: {:?}", error),
    }
}

Pattern Matching with Result

Rust commonly uses match expressions to handle different outcomes.

let result: Result<int, string> = Ok(10);

match result {

    Ok(value) => println!("Value: {}", value),

    Err(msg) => println!("Error: {}", msg),
}

The unwrap() Method

The unwrap() method extracts the successful value from a Result.

let file = File::open("notes.txt").unwrap();

If the operation fails, unwrap() causes a panic.

The expect() Method

The expect() method works like unwrap() but allows a custom error message.

let file = File::open("config.txt")
    .expect("Failed to open config file");

Propagating Errors

Instead of handling errors immediately, functions can return them to the caller.

use std::fs::File;
use std::io::Error;

fn open_file() -> Result<File, Error> {

    let file = File::open("data.txt")?;

    Ok(file)
}

The ? Operator

The ? operator simplifies error propagation.

If the operation succeeds, the value is returned. If it fails, the error is automatically returned from the function.

fn divide(a: f64, b: f64) -> Result<f64, string> {

    if b == 0.0 {

        return Err("Cannot divide by zero".to_string());
    }

    Ok(a / b)
}

Option vs Result

Rust also provides the Option<T> enum for values that may or may not exist.

Type Purpose
Option<T> Represents missing values
Result<T, E> Represents recoverable errors

Using Option

fn find_number(numbers: Vec<i32>, target: i32) -> Option<usize> {

    for (index, value) in numbers.iter().enumerate() {

        if *value == target {

            return Some(index);
        }
    }

    None
}

Handling Multiple Error Types

Large applications may encounter different kinds of errors.

use std::num::ParseIntError;

fn parse_number(input: &str) -> Result<i32, ParseIntError> {

    input.parse::<i32>()
}

Creating Custom Errors

Developers can define custom error types using enums.

enum AppError {
    NotFound,
    InvalidInput,
}

Error Handling Best Practices

  • Use Result for recoverable errors
  • Avoid unnecessary panics
  • Use descriptive error messages
  • Prefer the ? operator for cleaner code
  • Handle errors close to where they occur

Common Mistakes

  • Overusing unwrap()
  • Ignoring error values
  • Using panic for recoverable situations
  • Returning unclear error messages
  • Not propagating errors properly

Mini Practice

Try creating the following:

  • A function returning Result<T, E>
  • A file-reading example with match
  • A custom error enum
  • A function using the ? operator
  • An Option example returning Some or None
Lecture 10 Β· Advanced

Modules & Crates

Intermediate ~45 min

Cargo is Rust's build system and package manager. Use Cargo.toml to manage dependencies.

Lecture 11 Β· Advanced

Traits & Generics

Advanced ~65 min

Defining Shared Behavior

pub trait Summary {
    fn summarize(&self) -> String;
}
Lecture 12 Β· Capstone

Capstone Project: Secure CLI

Advanced ~180 min

Create a Command Line Interface (CLI) tool that securely manages passwords or encrypts files using crates like clap and rust-crypto.

// Project goals:
// 1. Parse CLI arguments
// 2. Handle file I/O safely
// 3. Implement robust error handling
// 4. Use traits for polymorphic output
Lecture 13 Β· Advanced

Lifetimes

Intermediate ~55 min Requires: Lecture 12

Content coming soon...

Lecture 14 Β· Advanced

Smart Pointers (Box, Rc, RefCell)

Intermediate ~50 min Requires: Lecture 13

Content coming soon...

Lecture 15 Β· Advanced

Concurrency (Threads & Channels)

Advanced ~55 min Requires: Lecture 14

Content coming soon...

Lecture 16 Β· Advanced

Closures & Advanced Iterators

Advanced ~50 min Requires: Lecture 15

Content coming soon...

Lecture 17 Β· Professional

Async Rust (Tokio)

Advanced ~55 min Requires: Lecture 16

Content coming soon...

Lecture 18 Β· Professional

Final Project β€” Rust Web Service

Advanced ~90 min Requires: All Previous

Content coming soon...