Rust is static types language Rustc compiler convert rust to executable cargo rust build system and pkg manager

cargo new projectname (like npm in js)

  • cargo.tomo (similar to pkg.json file in js)
fn main(){
 println!("hello world")
 println!("{}",1) // we cannot directly print number we need to format
}

cargo run filename compile to bin and run rustc filename convert to bin and we need to run

Varaibles

  • are imutable by default
  • const may need to define the vairable types
 
let x =1
x=5  // will error
let x= 5; //no error we can redeclare to bypasss
 
//we also cannot chnage the type 
let a =1
a="hai"  ///error
let a ="hai" //allowed
 
//const must need to define the type
//- `const` has no fixed memory address; it’s inlined wherever used.
const A : u32 = 50;

Shadowing

  • Shadowing re‑binds the name we can change type:
let x = 5;
let x = x + 1;   // new variable shadows the old one
let x = x * 2;
println!("{}", x); // 12

Types

Data type of every expression must be known at compile time.we don’t need to explicitly specify the type of every variable, but the type system will infer the types based on how the variables are used. This is known as type inference.

Scalar Types

  1. Integers:
    • i32: 32-bit signed integer (default)
    • i64: 64-bit signed integer
    • u32: 32-bit unsigned integer
    • u64: 64-bit unsigned integer
    • isize: Pointer-sized signed integer
    • usize: Pointer-sized unsigned integer
  2. Floats:
    • f32: 32-bit floating-point number
    • f64: 64-bit floating-point number (default)
  3. Characters:
    • char: Unicode scalar value (e.g., ‘a’, ’😊’)

Example: let x: char = 'a';

  1. Boolean:
    • bool: true or false

Example: let x: bool = true;

Compound Types

fn main() {
 
	//Array
 
    // 1. Creating an array
    let numbers = [1, 2, 3, 4, 5];
    println!("Array: {:?}", numbers);
    // 2. Accessing an array element
    println!("First element: {}", numbers[0]);
    // 3. Array length
    println!("Array length: {}", numbers.len());
    // 4. Array iteration
    for num in &numbers {
        println!("Number: {}", num);
    }
    // 5. Array mutation
    let mut numbers_mut = [1, 2, 3, 4, 5];
    numbers_mut[0] = 10;
    println!("Mutated array: {:?}", numbers_mut);
    // 6. Creating an array with a default value
    let mut bools = [false; 5];
    println!("Array of booleans: {:?}", bools);
    // 7. Array slicing
    let slice = &numbers[1..4];
    println!("Slice: {:?}", slice);
 
    // 8. Multidimensional arrays
    let matrix = [
        [1, 2, 3],
        [4, 5, 6],
        [7, 8, 9],
    ];
    println!("Matrix: {:?}", matrix);
 
 
    // 1. Creating a tuple
    let person = ("John", 30, "Developer");
    println!("Name: {}, Age: {}, Occupation: {}", person.0, person.1, person.2);
 
    // 2. Pattern matching with tuples
    let (name, age, occupation) = person;
    println!("Name: {}, Age: {}, Occupation: {}", name, age, occupation);
 
    // 3. Tuple indexing
    println!("Name: {}", person.0);
    println!("Age: {}", person.1);
    println!("Occupation: {}", person.2);
 
    // 4. Tuple destructuring
known at compile time. There’s     let (_, age, _) = person;
    println!("Age: {}", age);
 
    // 5. Creating a tuple with different types
    let mixed_tuple = (1, "hello", true, 3.14);
    println!("{:?}", mixed_tuple);
 
    // 6. Tuple as function return value
    let result = calculate_area(10, 20);
    println!("Area: {}", result.0);
    println!("Perimeter: {}", result.1);
}
 
fn calculate_area(length: i32, width: i32) -> (i32, i32) {
    let area = length * width;
    let perimeter = 2 * (length + width);
    (area, perimeter)
}
  1. Structs:
    • A custom data type with named fields Example
struct Person {
    name: String,
    age: u32,
}
 
let x: Person = Person {
    name: "Alice".to_string(),
    age: 30,
};
  1. String : have two imutable fixed length and growable heap allocated data structure
 
fn main() {
 
	let s = "hello"
	//growable string
    let mut s = "   Hello, World!   ".to_string();
 
    // Trim whitespace from the beginning and end of the string
    s = s.trim().to_string();
 
    // Convert the string to uppercase
    s = s.to_uppercase();
 
    // Replace commas with dashes
    s = s.replace(",", "-");
 
    // Insert a substring at a specific position
    s.insert_str(7, " Beautiful ");
 
    // Remove a range of characters from the string
    s.remove(15..20);
 
    println!("{}", s); // Output: "HELLO- Beautiful WORLD!"
}

1. Numeric Types Rust has fixed-width numeric types that map closely to modern hardware.

  • Integer Types: Rust offers signed (i8, i16, i32, i64, i128) and unsigned (u8, u16, u32, u64, u128) integers. The i32 type is the default if multiple possibilities exist.
  • Architecture-Dependent Types: The isize and usize types match the machine’s address space (32-bit or 64-bit). usize is required for array indices and representing collection sizes.
  • Literals: You can write integers with type suffixes (e.g., 42u8), prefixes for hexadecimal (0x), octal (0o), and binary (0b), or with underscores for readability (e.g., 4_294_967_295).
  • Casting & Overflows: Rust performs almost no implicit numeric conversions; you must explicitly use the as operator (e.g., 10_i8 as u16). Integer arithmetic overflow causes a panic in debug builds, but wraps around in release builds. If you need specific behavior, you can use checked, wrapping, saturating, or overflowing operations.
  • Floating-Point Types: Rust provides IEEE single-precision f32 and double-precision f64. If a type is not specified, Rust defaults to f64.

2. Booleans and Characters

  • Boolean (bool): Represents true or false. Rust is extremely strict about conditionals: constructs like if and while require a strict bool expression, meaning you cannot substitute an integer like 0 or 1 for false or true.
  • Character (char): Represents a single 32-bit Unicode character. Character literals use single quotes (e.g., '*'). Because they are 32 bits, they can represent everything from ASCII letters to Japanese kanji.

3. Tuples and the Unit Type

  • Tuples: Fixed-size sequences of elements that can be of different types. You define them with parentheses, like ("Brazil", 1985) which has the type (&str, i32). Elements are accessed using dot notation (e.g., t.0, t.1). They are often used to return multiple values from a function.
  • The Unit Type (): A zero-tuple called the “unit type,” which is used when there is no meaningful value to return, similar to void in other languages.

4. Arrays, Vectors, and Slices (Sequences) Rust separates sequences based on how they are allocated and sized:

  • Arrays ([T; N]): Have a fixed size determined at compile time, and all elements must be the same type. They are defined with brackets, like “ or [true; 10000] for repeated values.
  • Vectors (Vec<T>): Dynamically allocated on the heap, resizable sequences. You can create them using the vec! macro or Vec::new(). They track a pointer to the heap, their current length, and their capacity.
  • Slices (&[T] and &mut [T]): “Fat pointers” consisting of a memory address and a length. They allow you to reference a portion of an array or vector without owning it, which is ideal for writing functions that can accept both types.

5. Pointer Types To keep memory allocations to a minimum, Rust values nest by default rather than allocating implicitly on the heap. When you need pointers, Rust provides:

  • References: Non-owning pointers that are guaranteed never to be null or dangling. There are shared references (&T) which are read-only, and mutable references (&mut T) which grant exclusive read/write access. Rust strictly enforces a “single writer or multiple readers” rule.
  • Boxes (Box<T>): The simplest way to allocate a value on the heap. When the Box goes out of scope, the heap memory is freed immediately.
  • Raw Pointers (*const T and *mut T): Similar to C/C++ pointers, but they are unsafe and you can only dereference them inside an unsafe block.

6. String Types Rust strings are always valid UTF-8 sequences, not raw arrays of characters.

  • String Slices (&str): A “fat pointer” reference to UTF-8 text owned by something else. String literals (e.g., "hello") are &str types pointing to preallocated, read-only memory.
  • Strings (String): Analogous to Vec<T>, a String is a heap-allocated, resizable UTF-8 buffer. You can create one from a slice using methods like .to_string() or the format!() macro.

7. Type Aliases We can create custom shorthand names for existing types using the type keyword (e.g., type Bytes = Vec<u8>;).

Loops

fn main() {
    // 1. For loop
    let numbers = [1, 2, 3, 4, 5];
    for num in numbers {
        println!("For loop: {}", num);
    }
	for item in &numbers {   // borrow array
	    println!("{}", item);
	}
 
    // 2. While loop
    let mut i = 0;
    while i < 5 {
        println!("While loop: {}", i);
        i += 1;
    }
 
    // 3. Loop (infinite loop)
    let mut j = 0;
    loop {
        println!("Loop: {}", j);
        j += 1;
        if j == 5 {
            break;
        }
    }
 
    // 4. For loop with iterator
    let fruits = vec!["apple", "banana", "cherry"];
    for fruit in fruits {
        println!("For loop with iterator: {}", fruit);
    }
 
    // 5. For loop with range
    for k in 1..6 {
        println!("For loop with range: {}", k);
    }
}
 
let mut counter = 0;
let result = loop {
    counter += 1;
    if counter == 10 {
        break counter * 2;   // break can return a value
    }
};
println!("{}", result); // 20
 

Statements vs Expressions

  • Statement: an instruction that does not return a value (ends with ;).
  • Expression: returns a value (no ; at the end).

In Rust, almost everything is an expression. The block { ... } is an expression that returns the value of its last expression.

let y = {
    let x = 3;
    x + 1   // no semicolon -> value of block is 4
};
println!("{}", y); // 4

match

match is exhaustive and can destructure. Far

let x = 1;
match x {
    1 => println!("one"),
    2 => println!("two"),
    _ => println!("other"),   // underscore catches all other values
}
 
let number = 7;
match number {
    1 => println!("one"),
    2..=5 => println!("two to five"),
    6 | 7 => println!("six or seven"),
    _ => println!("other"),
}
 
 
//Destructuring tuples, structs, enums
 
let pair = (2, -2);
match pair {
    (0, y) => println!("x is 0, y is {}", y),
    (x, 0) => println!("y is 0, x is {}", x),
    (x, y) => println!("x={}, y={}", x, y),
}
 
enum Coin { Penny, Nickel, Dime, Quarter }
fn value(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

some and none are builtin enums will look like below

enum Option<T> {
    Some(T),
    None,
}
fn find_even(nums: Vec<i32>) -> Option<i32> {
    for n in nums {
        if n % 2 == 0 {
            return Some(n);
        }
    }
    None
}
 
let result = find_even(vec![1, 3, 5, 8]);
 
match result {
    Some(n) => println!("found {}", n),
    None => println!("no even number"),
}
 
 
fn main() {
    let mut input = String::new();
    println!("Enter a number between 1 and 10:");
    std::io::stdin().read_line(&mut input).unwrap();
    let num: i32 = input.trim().parse().unwrap_or(0);
 
    let description = match num {
        1 => "one",
        2..=5 => "two to five",
        6..=10 => "six to ten",
        _ => "out of range",
    };
    println!("You entered: {}", description);
}
 

Option enum

In JavaScript, null and undefined represent absence of a value. They are the source of countless runtime errors: Cannot read property 'x' of null. Rust eliminates this class of bugs by encoding the possibility of absence in the type system using Option<T>

Option<T> is a generic enum defined in the standard library:

enum Option<T> {
    Some(T),  // contains a value of type T
    None,     // represents absence
}
 
//in memory it will be stored as
 
//optional = { tag: Some, value: 7 } {tag:None,value:''}
 
// Rust – Option forces you to check
let name: Option<String> = get_user_name();
match name {
    Some(n) => println!("{}", n.to_uppercase()),
    None => println!("no name"),
}
 
//in some(n) rust do n = name.value only if Is name.tag == Some 
 
 
let some_number = Some(5);        // type: Option<i32>
let some_string = Some("hello");  // Option<&str>
let absent: Option<i32> = None;   // type annotation required for None
 

The difference: In Rust, the type Option<String> is not a String. we cannot call to_uppercase() on it directly. The compiler forces us to handle both cases.

option have some builtin function like

  • map
  • filter
// The definition itself
enum Option<T> {
    None,
    Some(T),
}
 
impl<T> Option<T> {
    // map: transform Some(T) -> Some(U), None stays None
    pub fn map<U, F>(self, f: F) -> Option<U>
    where
        F: FnOnce(T) -> U,
    {
        match self {
            Some(x) => Some(f(x)),
            None => None,
        }
    }
 
    // and_then: flat‑map – the function already returns Option
    pub fn and_then<U, F>(self, f: F) -> Option<U>
    where
        F: FnOnce(T) -> Option<U>,
    {
        match self {
            Some(x) => f(x),
            None => None,
        }
    }
 
    // filter: keep Some only if predicate true
    pub fn filter<P>(self, predicate: P) -> Self
    where
        P: FnOnce(&T) -> bool,
    {
        match self {
            Some(x) => {
                if predicate(&x) {
                    Some(x)
                } else {
                    None
                }
            }
            None => None,
        }
    }
 
    // unwrap_or: return value or fallback
    pub fn unwrap_or(self, default: T) -> T {
        match self {
            Some(x) => x,
            None => default,
        }
    }
 
    // unwrap_or_else: lazy fallback (closure called only if None)
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T,
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
 
    // as_ref: borrow inner value without moving
    pub fn as_ref(&self) -> Option<&T> {
        match self {
            Some(x) => Some(x),
            None => None,
        }
    }
 
    // as_mut: mutable borrow of inner value
    pub fn as_mut(&mut self) -> Option<&mut T> {
        match self {
            Some(x) => Some(x),
            None => None,
        }
    }
 
    // take: replace with None and return the old value
    pub fn take(&mut self) -> Option<T> {
        // std::mem::take is equivalent to mem::replace(self, None)
        match std::mem::replace(self, None) {
            Some(x) => Some(x),
            None => None,
        }
    }
 
    // replace: set to Some(value) and return the old Option
    pub fn replace(&mut self, value: T) -> Option<T> {
        std::mem::replace(self, Some(value))
    }
 
    // ok_or: convert Option to Result (None becomes Err)
    pub fn ok_or<E>(self, err: E) -> Result<T, E> {
        match self {
            Some(x) => Ok(x),
            None => Err(err),
        }
    }
 
    // ok_or_else: lazy error creation
    pub fn ok_or_else<E, F>(self, err: F) -> Result<T, E>
    where
        F: FnOnce() -> E,
    {
        match self {
            Some(x) => Ok(x),
            None => Err(err()),
        }
    }
}

Macro

In Rust, a macro is a way to extend the language itself. Macros are essentially functions that generate code at compile-time, allowing you to create new syntax, abstractions, and domain-specific languages (DSLs) within Rust.

Macros are defined using the macro keyword, followed by the name of the macro and a set of rules that define how the macro should be expanded. When the macro is invoked, the Rust compiler will replace the macro invocation with the expanded code.

Macros need to be call with !

macro_rules! greet {
    ($name:expr) => {
        println!("Hello, {}!", $name);
    };
}
 
fn main() {
    greet!("Alice");
    greet!("Bob");
}

Types of Macros in Rust:

  1. Declarative Macros: These macros are defined using the macro_rules! syntax and are used to generate code at compile-time.
  2. Procedural Macros: These macros are defined using the proc_macro attribute and are used to generate code at compile-time using a procedural interface.
  3. Attribute Macros: These macros are used to attach attributes to items, such as functions or structs.

Builtin Macro

1. println!: Prints its arguments to the standard output, followed by a newline character.

2. vec!: Creates a new vector with the specified elements.

3. format!: Formats its arguments into a string.

4. assert!: Asserts that a condition is true, and panics if it’s false.

5. assert_eq!: Asserts that two values are equal, and panics if they’re not.

6. assert_ne!: Asserts that two values are not equal, and panics if they are.

7. panic!: Panics with a custom message.

8. unimplemented!: Panics with a message indicating that a function or method is not implemented.

9. todo!: Panics with a message indicating that a function or method is not implemented, and suggests that it should be implemented.

10. include!: Includes the contents of a file into the current module.

11. include_str!: Includes the contents of a file as a string.

12. module!: Defines a new module.

13. concat!: Concatenates its arguments into a single string.

14. stringify!: Converts its argument into a string.

15. debug_assert!: Asserts that a condition is true, and panics if it’s false, but only in debug builds.

Function

fn main() {
    // 1. Defining a function
    fn greet(name: &str) {
        println!("Hello, {}!", name);
    }
    greet("Alice");
 
    // 2. Defining a function with return value
    fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    let result = add(2, 3);
    println!("Result: {}", result);
 
    // 3. Defining a closure
    let names = vec!["Alice", "Bob", "Charlie"];
    let greet = |name: &str| println!("Hello, {}!", name);
    for name in names {
        greet(name);
    }
 
    // 4. Defining a closure with capture
    let message = "Hello, ".to_string();
    let greet = move |name: &str| println!("{}", format!("{}{}", message, name));
    greet("Alice");
 
    // 5. Defining a closure as a function parameter
    fn process<F>(data: &str, func: F)
    where
        F: Fn(&str),
    {
        func(data);
    }
    process("Hello, World!", |s| println!("{}", s));
 
    // 6. Defining a closure as a function return value
    fn create_greeter(name: &str) -> impl Fn() {
        move || println!("Hello, {}!", name)
    }
    let greeter = create_greeter("Alice");
    greeter();
}

Expression

Rust is what is called an expression language. This means it follows an older tradition, dating back to Lisp, where expressions do all the work. In C, if and switch are statements. They don’t produce a value, and they can’t be used in the middle of an expression. In Rust, if and match can produce values. We already saw a match expression that produces a numeric value

Memory Management

 Rust has no GC –yet it’s memory safe without runtime overhead. This is achieved through ownership, borrowing, lifetimes, and the borrow checker (all compile‑time checks).

Rust enforces RAII (Resource Acquisition Is Initialization), so whenever an object goes out of scope, its destructor is called and its owned resources are freed.

JavaScript analogy:

  • Stack: primitives (numberboolean) and references to objects.
  • Heap: all objects, arrays, closures.

we explicitly choose stack vs heap. By default, everything is stack‑allocated unless we use heap‑allocated types like Box<T>VecString.

let x = 5;            // on stack (i32 has fixed size)
let y = Box::new(5);  // on heap (Box points to heap, pointer on stack)
  • Ownership = each value has one owner; owner decides when memory is freed.
  • Borrowing = references that don’t take ownership, with strict aliasing rules.
  • Lifetimes = annotations that tell the compiler how long references are valid.
  • Borrow checker = compile‑time police enforcing these rules.

Ownership

  • Each value in Rust has a single owner (a variable).
  • When the owner goes out of scope, the value is dropped (its memory is freed immediately).
{
    let s = String::from("hello"); // s owns the heap string
} // scope ends -> s is dropped -> heap memory freed
 
let a = String::from("Rust");
let b = a;            // ownership of the string moves from a to b
// println!("{}", a); //  compile error: a is no longer valid
 

This is deterministic  no GC pause, you know exactly when memory is freed.

some types are so cheap to copy that the compiler copies them instead of moving. These implement the Copy trait:

  • Integers (i32u64, etc.)
  • Booleans (bool)
  • Characters (char)
  • Tuples of Copy types
  • Arrays (if elements are Copy)
let x = 5;
let y = x;   // x is copied, not moved – both valid
println!("{}", x); //  works

we can’t implement Copy for types that manage heap memory (like StringVec)

Borrowing

Often we want to use a value without taking ownership. That’s borrowing via references (&).

fn calculate_length(s: &String) -> usize {
    s.len()
} // s goes out of scope, but nothing is dropped (no ownership)
 
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("{}", s1); //  s1 still valid

Rules of borrowing (enforced at compile time):

  1. At any given time, you can have either one mutable reference OR any number of immutable references.
  2. References must always be valid (no dangling references).
Mutable Borrowing

Allow us to edit the varaible

let mut s = String::from("hello");
let r1 = &mut s;
r1.push_str(" world");
// let r2 = &mut s; //  cannot borrow as mutable more than once
println!("{}", r1);
 
//Mixing immutable and mutable borrows
 
let mut s = String::from("hello");
let r1 = &s;      // immutable borrow
let r2 = &s;      // another immutable borrow
// let r3 = &mut s; //  cannot borrow as mutable because r1,r2 exist
println!("{} {}", r1, r2);
// r1 and r2 go out of scope here – now mutable borrow allowed
let r3 = &mut s; // 

Lifetimes

The borrow checker needs to know how long a reference lives  that’s a lifetime. Most of the time we don’t write lifetimes explicitly (lifetime elision), but sometimes we must.

Dangling Reference

fn dangle() -> &String {
    let s = String::from("hello");
    &s
} // s is dropped, but we return a reference to it –  compile error

Rust catches this because the reference’s lifetime is tied to s, which ends at the function return.

When a function returns a reference, Rust must answer:

“Which input does this output reference come from?”

we write lifetimes with an apostrophe: 'a. They connect the lifetimes of parameters and return values.

 
let s1 = String::from("hello");
let result;
 
{
    let s2 = String::from("world");
    result = longest(&s1, &s2);
    print("{}",result) // will works b
}
println!("{}", result); // will not work becuase a take shortest
 
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

'a = the minimum lifetime shared by both inputs

s1 → lives for whole main         ('long)
2 → lives only inside block      ('short)

When we call: result = longest(&s1, &s2);

Rust resolves 'a as: 'a = min('long, 'short) = 'short

But below code will work

fn main() {
    let s = "Ss";
    let k;
    {
        let b = "sss";
        k = longest(s,b);
    }
    println!("{}", k);
}
 
fn longest<'a,'b>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
 
//because b and s are static **string literals**.
 
//'a = min('static, 'static) = 'static

String literals are not stack variables.

They are stored in: binary / read-only memory (static memory)

s → &‘static str
b → &‘static str

Even though b is declared inside the block:

  • the reference variable b dies
  • but the actual string data lives forever

NOTE: LIFETME ARE JUST COMPILE TIME CHECK THERE IS NO RELATION ON RUNTIME

Lifetime Elision Rules (what the compiler infers automatically)

  1. Each parameter that is a reference gets its own lifetime.
  2. If there is exactly one input lifetime, it is assigned to all output lifetimes.
  3. If there is a &self or &mut self (method), the lifetime of self is assigned to all output lifetime

The Borrow Checker

The borrow checker is a static analyzer that runs at compile time. It enforces the ownership and borrowing rules by tracking:

  • Liveness of variables (when they go out of scope)
  • Aliasing (multiple references)
  • Mutation vs immutability

It uses regions (lifetime intervals) and ensures that:

  • No reference outlives its referent.
  • No mutable reference aliases with any other reference to the same data.

Mental model:

Think of references as tickets to access data. we can have many read‑only tickets, or one exclusive write ticket. Once you return the ticket, someone else can take a write ticket.

Enum

 
enum Direction {
    Up,
    Down,
    Left,
    Right,
}
 
enum WebEvent {
    PageLoad,                     // no data
    KeyPress(char),               // single value
    Click { x: i32, y: i32 },     // named fields anonymous struct
    Paste(String),                // single value (String)
}
 
let load = WebEvent::PageLoad;
let press = WebEvent::KeyPress('x');
let click = WebEvent::Click { x: 10, y: 20 };
let paste = WebEvent::Paste(String::from("text"));
 
// Methods on enums
impl WebEvent {
    fn is_page_load(&self) -> bool {
        matches!(self, WebEvent::PageLoad)
    }
}
 
let event = WebEvent::KeyPress('a');
match event {
    WebEvent::PageLoad => println!("page loaded"),
    WebEvent::KeyPress(c) => println!("pressed '{}'", c),
    WebEvent::Click { x, y } => println!("click at ({},{})", x, y),
    WebEvent::Paste(s) => println!("pasted '{}'", s),
}
 
let res: Result<u32, &str> = Ok(42);
match res {
    Ok(val) => println!("value = {}", val),
    Err(e) => println!("error: {}", e),
}

Struct

In JavaScript, objects are dynamic collections of key‑value pairs. In Rust, structs are fixed, typed, and known at compile time. There’s no prototype chain, no dynamic property addition, and no this binding surprises

struct User {
    name: String,    // note: not &str; we'll explain
    age: u32,
    active: bool,
}
 
let user = User {
    name: String::from("Alice"),
    age: 30,
    active: true,
};
 
let mut user = User { ... };
user.age = 31;               // OK because user is mut
println!("{}", user.name);   // reading always allowed
 
let name = String::from("Alice");
let age = 30;
let user = User { name, age, active: true };
 
//update
let user2 = User {
    name: String::from("Bob"),
    ..user   // remaining fields from user
};
 
This moves or copies fields. If a field is non‑Copy (like `String`), it’s moved from `user` into `user2`, so `user` becomes invalid afterwards.
  • Type of each field must be declared.
  • Fields are fixed – we cannot add or remove fields after definition.
  • All fields must be present when creating an instance.
  • must provide values for all fields. Order doesn’t matter (unlike tuple structs).

implementation

struct Rectangle {
    width: u32,
    height: u32,
}
 
impl Rectangle {
    // Method (takes &self)
    fn area(&self) -> u32 {
        self.width * self.height
    }
    
    // Method with mutable self
    fn scale(&mut self, factor: u32) {
        self.width *= factor;
        self.height *= factor;
    }
    
    // Associated function (no self) – like static method
    fn square(side: u32) -> Rectangle {
        Rectangle { width: side, height: side }
    }
}
 
let mut rect = Rectangle { width: 10, height: 20 };
println!("{}", rect.area());   // calls method
rect.scale(2);
let sq = Rectangle::square(5); // calls associated function
  • &self borrows immutably, &mut self mutably, self consumes (moves) the struct.
  • can have multiple impl blocks for the same struct (useful for trait implementations)

Struct holding references (requires lifetimes)

struct UserRef<'a> {
    name: &'a str,   // borrowed, not owned
    age: u32,
}
 
let name = String::from("Alice");
let user = UserRef { name: &name, age: 30 }; // name must outlive user

Pattern Matching

Pattern matching  the safe, exhaustive way to deconstruct structs and enums.

// A few types to work with
#[derive(Debug)]
struct User {
    name: String,
    age: u32,
    active: bool,
}
 
#[derive(Debug)]
enum WebEvent {
    PageLoad,
    KeyPress(char),
    Click { x: i32, y: i32 },
    Paste(String),
}
 
fn main() {
    // ----------------------------------------------------------------
    // 1. Basic match on enums
    // ----------------------------------------------------------------
    let event = WebEvent::KeyPress('a');
    match event {
        WebEvent::PageLoad => println!("Page loaded"),
        WebEvent::KeyPress(c) => println!("Key pressed: {}", c),
        WebEvent::Click { x, y } => println!("Click at ({}, {})", x, y),
        WebEvent::Paste(s) => println!("Pasted: {}", s),
    }
 
    // ----------------------------------------------------------------
    // 2. match on Option<T>
    // ----------------------------------------------------------------
    let maybe_value: Option<i32> = Some(42);
    match maybe_value {
        Some(v) => println!("Value is {}", v),
        None => println!("No value"),
    }
 
    // ----------------------------------------------------------------
    // 3. match on Result<T, E>
    // ----------------------------------------------------------------
    let result: Result<i32, &str> = Ok(100);
    match result {
        Ok(val) => println!("Success: {}", val),
        Err(e) => println!("Error: {}", e),
    }
 
    // ----------------------------------------------------------------
    // 4. Destructuring structs
    // ----------------------------------------------------------------
    let user = User {
        name: "Alice".to_string(),
        age: 30,
        active: true,
    };
    match user {
        User { name, age: 30, active } => {
            println!("{} is 30 years old, active: {}", name, active);
        }
        User { name, age, .. } => {
            println!("{} is {} years old (not 30)", name, age);
        }
    }
 
    // ----------------------------------------------------------------
    // 5. Destructuring tuples
    // ----------------------------------------------------------------
    let point = (3, 5);
    match point {
        (0, 0) => println!("origin"),
        (x, 0) => println!("on x-axis at {}", x),
        (0, y) => println!("on y-axis at {}", y),
        (x, y) => println!("point at ({}, {})", x, y),
    }
 
    // ----------------------------------------------------------------
    // 6. Destructuring arrays / slices
    // ----------------------------------------------------------------
    let arr = [1, 2, 3];
    match arr {
        [a, b, c] => println!("array: {}, {}, {}", a, b, c),
        [a, b] => println!("two elements: {}, {}", a, b), // not used here
        _ => println!("other length"),
    }
 
    // ----------------------------------------------------------------
    // 7. Ranges and multiple patterns with `|`
    // ----------------------------------------------------------------
    let x = 7;
    match x {
        1 | 2 => println!("one or two"),
        3..=5 => println!("three to five"),
        6..=10 => println!("six to ten"),
        _ => println!("other"),
    }
 
    // ----------------------------------------------------------------
    // 8. Match guards (extra `if` condition)
    // ----------------------------------------------------------------
    let number = Some(15);
    match number {
        Some(n) if n < 10 => println!("less than ten"),
        Some(n) => println!("ten or more: {}", n),
        None => println!("none"),
    }
 
    // ----------------------------------------------------------------
    // 9. @ bindings (capture value while matching)
    // ----------------------------------------------------------------
    let value = 12;
    match value {
        n @ 1..=5 => println!("small: {}", n),
        n @ 10..=20 => println!("medium: {}", n),
        _ => println!("other"),
    }
 
    // Also works with Option:
    let opt = Some(5);
    match opt {
        Some(n @ 1..=5) => println!("small option: {}", n),
        Some(n) => println!("other option: {}", n),
        None => (),
    }
 
    // ----------------------------------------------------------------
    // 10. Ignoring values with `_` and `..`
    // ----------------------------------------------------------------
    let (a, _, c) = (1, 2, 3);
    println!("a={}, c={}", a, c); // second value ignored
 
    let user2 = User { name: "Bob".into(), age: 25, active: false };
    match user2 {
        User { name, .. } => println!("Name is {}", name), // ignore age and active
    }
 
    // ----------------------------------------------------------------
    // 11. `if let` – match a single pattern
    // ----------------------------------------------------------------
    let optional = Some(7);
    if let Some(x) = optional {
        println!("if let got: {}", x);
    } else {
        println!("none");
    }
 
    // With guard:
    let value = Some(12);
    if let Some(x) = value {
        if x > 10 {
            println!("big if let: {}", x);
        }
    }
 
    // ----------------------------------------------------------------
    // 12. `let else` – require match, else diverge (Rust 1.65+)
    // ----------------------------------------------------------------
    let get_first_char = |s: String| -> char {
        let Some(c) = s.chars().next() else {
            panic!("string was empty");
        };
        c
    };
    println!("first char: {}", get_first_char("hello".to_string()));
 
    // ----------------------------------------------------------------
    // 13. `matches!` macro – quick boolean test
    // ----------------------------------------------------------------
    let ev = WebEvent::PageLoad;
    if matches!(ev, WebEvent::PageLoad) {
        println!("matches! says it's PageLoad");
    }
 
    // Can also match with guards in matches!
    let num = Some(5);
    if matches!(num, Some(x) if x > 3) {
        println!("matches! with guard: number > 3");
    }
 
    // ----------------------------------------------------------------
    // 14. Destructuring enums with data (already shown above, but extra)
    // ----------------------------------------------------------------
    let click = WebEvent::Click { x: 100, y: 200 };
    match click {
        WebEvent::Click { x, y } => println!("Click destructured: {}, {}", x, y),
        _ => (),
    }
 
    // ----------------------------------------------------------------
    // 15. Reference patterns (`ref` and `ref mut`)
    // ----------------------------------------------------------------
    let user3 = User { name: "Carol".to_string(), age: 28, active: true };
    match user3 {
        // `ref` borrows instead of moving
        User { name: ref n, age, active } => {
            println!("Borrowed name: {}", n);
            // `user3` is still usable because we only borrowed `name`
        }
    }
    // user3 can still be used because we didn't move `name` (we used `ref`)
    println!("user3.name is still owned: {}", user3.name);
 
    // Mutable reference pattern
    let mut user4 = User { name: "Dave".into(), age: 35, active: false };
    match &mut user4 {
        User { age: ref mut a, .. } => {
            *a += 1;
        }
    }
    println!("user4 age after mutation: {}", user4.age);
}

Module System

crate is a compilation unit essentially our package. There are two types:

  • Binary crate – produces an executable (has a main function)
  • Library crate – produces a library (has a lib.rs, no main)

cargo package can contain one library crate and any number of binary crates. The crate root is:

  • src/main.rs for a binary crate
  • src/bin/any.rs if we want more then one bin
  • src/lib.rs for a library crate

mod

//inlinle in single file
mod math {
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }
}
 
//Create `src/math.rs`:
 
// math.rs
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}
 
 
// main.rs
mod math;  // looks for math.rs or math/mod.rs
 
fn main() {
    let sum = math::add(2, 3);
    println!("{}", sum);
}

 Module Hierarchy

Modules form a tree. The crate root (main.rs or lib.rs) is the root of the tree.

Paths can be:

  • Absolute – start with crate:: (like require('my-package') but for our crate)
  • Relative – self::super::
crate
├── math
│   ├── add
│   └── subtract
├── utils
│   └── logging
└── main

Think of Rust’s module system like a file cabinet:

  • Package = the whole cabinet (what cargo new creates)
  • Crate = a drawer (either a library or an executable)
  • Module = folders inside the drawer
  • Items = the actual papers (functions, structs, enums, etc.)

The key insight: Rust separates “where code lives” from “how code is organized.” The file system gives you hints, but modules are a logical structure we declare explicitly.

When we run cargo new my_project, we get a package. A package contains one or more crates.

my_project/
├── Cargo.toml          // Package manifest
└── src/
    └── main.rs         // Binary crate root

Two crate types:

  • Binary crate: has fn main(), compiles to an executable. Root is src/main.rs.
  • Library crate: reusable code, no main(). Root is src/lib.rs.

A package can have at most one library crate, but many binary crates (put extras in src/bin/).

Modules can live inside a single file using the mod keyword:

// src/main.rs
mod greetings {
    pub fn hello() {
        println!("Hello!");
    }
    
    fn private_helper() {
        // Not accessible outside this module
    }
}
 
fn main() {
    greetings::hello();        // Works — it's pub
    // greetings::private_helper();  // ERROR — private
}

Key rule: Everything in Rust is private by default. we must write pub to expose it.

pub isn’t just on/off. There are gradations:

pub fn anyone_can_use()         // Fully public
pub(crate) fn crate_only()      // Public within this crate
pub(super) fn parent_only()     // Public to parent module
pub(in path::to::mod) fn scoped // Public to a specific path
fn totally_private()            // Default — module only

For struct fields, pub on the struct doesn’t expose fields — you need pub on each:

pub struct User {
    pub name: String,    // Accessible
    age: u32,            // Private even though User is pub
}

For enum, variants inherit the enum’s visibility pub enum means all variants are public.

When we write mod greetings; (with a semicolon, no body), Rust looks for the module in one of two places:

src/
├── main.rs
├── greetings.rs              // Option A: flat file
└── greetings/                // Option B: folder
    └── mod.rs                // (old style, still works)

Modern Rust (2018+ edition) prefers a hybrid:

src/
├── main.rs
├── greetings.rs              // The module's code
└── greetings/                // Its submodules
    └── formal.rs

Example:

// src/main.rs
mod greetings;  // "Load greetings from greetings.rs or greetings/mod.rs"
 
fn main() {
    greetings::hello();
    greetings::formal::good_evening();
}
// src/greetings.rs
pub mod formal;  // Load submodule from greetings/formal.rs
 
pub fn hello() {
    println!("Hi!");
}
// src/greetings/formal.rs
pub fn good_evening() {
    println!("Good evening.");
}

To reference items, we use paths with ::

// Absolute path (starts from crate root)
crate::greetings::formal::good_evening();
 
// Relative path (from current module)
greetings::formal::good_evening();
 
// From a sibling module, using super
super::other_module::something();
 
// From the current module
self::helper();

Typing crate::greetings::formal::good_evening() everywhere is painful. use brings items into scope:

use crate::greetings::formal::good_evening;
 
fn main() {
    good_evening();  // Now just works
}

Common patterns:

use std::collections::HashMap;           // Import one thing
use std::collections::{HashMap, HashSet}; // Import multiple
use std::io::{self, Read, Write};        // self = io itself, plus Read/Write
use std::fmt::Result as FmtResult;       // Rename to avoid conflicts
use std::net::*;                         // Glob import (use sparingly!)

Idiom: For functions, use the parent module, not the function itself. For structs/enums/traits, use them directly.

use std::collections::HashMap;  // Good — HashMap::new()
use std::io;                    // Good — io::Result<T>
 
// Avoid:
use std::io::Result;  // Ambiguous with std::fmt::Result

Re-exports with pub use

This is the magic trick for designing nice APIs. pub use brings something into scope and re-exports it:

// src/lib.rs
mod internal {
    pub mod deeply {
        pub mod nested {
            pub struct Important;
        }
    }
}
 
pub use internal::deeply::nested::Important;
// Users can now write: my_crate::Important
// Instead of: my_crate::internal::deeply::nested::Important

This lets we reorganize internals without breaking users. Most polished crates use a prelude module:

External Crates

In Cargo.toml:

[dependencies]
serde = "1.0"
tokio = { version = "1", features = ["full"] }

In code, just use them — cargo handles the rest:

use serde::Serialize;
use tokio::time::sleep;

In Rust 2015 we needed extern crate serde; in 2018+ editions this is automatic.

Under the hood

Here’s something that surprises many people: modules don’t exist at runtime. They’re purely a compile-time organizational tool.

In the compiled binary:

Functions become symbols (e.g., _ZN8my_crate9greetings5hello17h1a2b3c4d5e6f7890E — that’s a mangled name) The module path is encoded in the symbol name for uniqueness Private functions may be inlined away entirely The module structure is erased; only the items remain

This is why Rust can have zero-cost modules they don’t cost anything at runtime because they don’t exist at runtime. Compare to Python, where modules are actual objects with attribute lookups happening at runtime.

The rlib format (Rust’s library format) does preserve module structure in metadata so that downstream crates can find items. But for your final binary, it’s all flat.

COLLECTIONS

collections are data structures stored on the heap, meaning their size can grow or shrink at runtime. The three core ones you we know are Vec<T>, HashMap<K, V>, and VecDeque<T>.

Vectors (Vec<T>)

A vector is a growable array. All elements must be the same type, and they’re stored contiguously in memory (great for cache performance).

Beginner:

fn main() {
    // Creating vectors
    let mut v1: Vec<i32> = Vec::new();
    let mut v2 = vec![1, 2, 3, 4, 5];  // macro with initial values
    
    // Adding elements
    v1.push(10);
    v1.push(20);
    v1.push(30);
    
    // Reading elements — two ways
    let third: &i32 = &v2[2];           // panics if out of bounds
    println!("Third: {}", third);
    
    match v2.get(100) {                  // returns Option<&T>, safe
        Some(val) => println!("Got: {}", val),
        None => println!("Out of bounds"),
    }
    
    // Iterating
    for i in &v2 {
        println!("{}", i);
    }
    
    // Mutable iteration
    for i in &mut v2 {
        *i += 10;  // dereference to modify
    }
}
 
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];      // immutable borrow
// v.push(6);           // ERROR: cannot borrow as mutable Pushing might reallocate the vector to new memory, invalidating `first`. Rust prevents this at compile time.
println!("{}", first);  // first used here
v.push(6);              // OK now, first is no longer used
 
 
 
// Pre-allocate when you know the size — avoids reallocations
let mut v: Vec<i32> = Vec::with_capacity(1000);
for i in 0..1000 {
    v.push(i);  // no reallocation happens
}
 
// Useful methods
let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6];
v.sort();                               // [1, 1, 2, 3, 4, 5, 6, 9]
v.dedup();                              // [1, 2, 3, 4, 5, 6, 9]
let sum: i32 = v.iter().sum();
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
let evens: Vec<&i32> = v.iter().filter(|&&x| x % 2 == 0).collect();
 
// Draining (removes and yields elements)
let mut v = vec![1, 2, 3, 4, 5];
let drained: Vec<i32> = v.drain(1..4).collect();  // drained = [2,3,4], v = [1,5]

NOTE:

Below code get error because we try to own the vector value which make the vector invalid

let mut v = Vec::new();  
  
for i in 101 .. 106 {  
v.push(i.to_string());   //A `String` in Rust is **heap-allocated owned data**.
}  
  
let third = v[2];
 
//which wil conver to 
third = &v[2]
 
//but the assingment operator do would require ownership transfer
 
let third: String = *(&v[2]);
 
//so we get error can move 

when we v[2] Index::index(&v, 2)

The indexing operator returns a reference, not ownership.

under the hood The Index is a trait

pub trait Index<Idx> {
    type Output: ?Sized;
    fn index(&self, index: Idx) -> &Self::Output;
}
 
So `v[2]` is actually sugar for:
 
*Index::index(&v, 2)

The * at the end is where the move attempt happens. we are dereferencing a &String to get a String, which means moving the String out from behind the reference and we can’t move out from behind a shared reference.

 
let mut v = vec![1973, 1968];
v.sort();
// this what internally do implicitly borrows a mutable reference to v
(&mut v).sort();
// equivalent, but more verbose

HashMaps (HashMap<K, V>)

Beginner:

use std::collections::HashMap;
 
fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
    
    // Access
    let team = String::from("Blue");
    let score = scores.get(&team).copied().unwrap_or(0);
    println!("Score: {}", score);
    
    // Iterate
    for (key, value) in &scores {
        println!("{}: {}", key, value);
    }
}
 
let text = "the quick brown fox jumps over the lazy dog the end";
let mut word_count: HashMap<&str, i32> = HashMap::new();
 
for word in text.split_whitespace() {
    // If key exists, increment; otherwise insert 0 and then increment
    *word_count.entry(word).or_insert(0) += 1;
}
// word_count now: {"the": 3, "quick": 1, "brown": 1, ...}
// The `entry` API avoids doing two lookups (one to check, one to insert).
 
// Inserting owned strings — the map takes ownership
let key = String::from("name");
let value = String::from("Alice");
let mut map = HashMap::new();
map.insert(key, value);
// println!("{}", key);  // ERROR: key was moved
 
// Custom struct as key — must implement Hash + Eq
#[derive(Hash, Eq, PartialEq, Debug)]
struct UserId(u64);
 
let mut users: HashMap<UserId, String> = HashMap::new();
users.insert(UserId(1), "Alice".into());
users.insert(UserId(2), "Bob".into());
 

Traits

A trait is a list of methods that a type promises to provide. it similar to interface in java or js with more power

trait Animal {
    fn make_sound(&self) -> String;
	
	// default implementation — everyone gets this for free fn
	hello(&self) -> String { 
		format!("Hello, I'm {}", self.name())
	}
}
 
fn make_it_speak(animal: &impl Animal) {
    println!("{}", animal.make_sound());
}
 
struct Dog;
struct Cat;
struct Rock;
 
impl Animal for Dog {
    fn make_sound(&self) -> String { String::from("Woof!") }
}
 
impl Animal for Cat {
    fn make_sound(&self) -> String { String::from("Meow!") }
}
// Note: Rock does NOT implement Animal
 
fn main() {
    make_it_speak(&Dog);    // "Woof!"
    make_it_speak(&Cat);    // "Meow!"
    // make_it_speak(&Rock); //  COMPILER ERROR, not runtime!
}
 
 
 
// Rust — data is in struct, methods are in impl blocks
struct Dog {
    name: String,  // just data
}
 
// Methods go in a separate impl block
impl Dog {
    fn new(name: String) -> Self {
        Dog { name }
    }
    
    fn fetch(&self) -> String {
        format!("{} fetches the ball", self.name)
    }
}
 
// Trait implementations go in ANOTHER impl block
impl Animal for Dog {
    fn make_sound(&self) -> String {
        format!("{} says Woof!", self.name)
    }
}
  • struct = the shape of your data (like a plain JS object’s properties)
  • impl Dog = methods specific to Dog (inherent methods)
  • impl Animal for Dog = implementing a contract/interface

safely add methods to existing types via traits, and it’s the idiomatic way:

The trait is only active where it’s imported. No global pollution. This is called the “extension method” pattern

// Rust — this is good practice and safe
trait Summable {
    fn sum_all(&self) -> i32;
}
 
impl Summable for Vec<i32> {
    fn sum_all(&self) -> i32 {
        self.iter().sum()
    }
}
 
fn main() {
    let v = vec![1, 2, 3];
    println!("{}", v.sum_all());  // 6
}

Built in trait

  • debug
  • Clone
  • PartialEq
  • Iterator
  • hash
pub trait Debug {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}
 
#[derive(Debug)]
struct User {
    name: String,
    age: u32,
}
 
fn main() {
    let u = User { name: "Alice".into(), age: 30 };
    println!("{:?}", u);    // User { name: "Alice", age: 30 }
    println!("{:#?}", u);   // pretty-printed with newlines
}
 
use std::fmt;
 
struct User { name: String, age: u32 }
 
impl fmt::Debug for User {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "User[{}, age={}]", self.name, self.age)
    }
}
 
//Clone
 
pub trait Clone {
    fn clone(&self) -> Self;
    
    // default method
    fn clone_from(&mut self, source: &Self) {
        *self = source.clone();
    }
}
 
#[derive(Clone)]
struct User {
    name: String,
    age: u32,
}
 
fn main() {
    let a = User { name: "Alice".into(), age: 30 };
    let b = a.clone();  // deep copy
    // a is still valid
}
 
 
#[derive(PartialEq, Eq)]
struct UserId(u64);
 
fn main() {
    let a = UserId(1);
    let b = UserId(1);
    let c = UserId(2);
    
    println!("{}", a == b);  // true
    println!("{}", a == c);  // false
}
 
 
pub trait Iterator {
    type Item;  // the type of thing being yielded
    
    // the ONLY required method
    fn next(&mut self) -> Option<Self::Item>;
    
    // ~75 default methods built on top of next()
    fn map<B, F>(self, f: F) -> Map<Self, F> where ... { ... }
    fn filter<P>(self, predicate: P) -> Filter<Self, P> where ... { ... 
    fn collect<B: FromIterator<Self::Item>>(self) -> B { ... }
    fn fold<B, F>(self, init: B, f: F) -> B where ... { ... }
    // ... and many more
}
 
struct Counter {
    count: u32,
    max: u32,
}
 
impl Iterator for Counter {
    type Item = u32;
    
    fn next(&mut self) -> Option<u32> {
        if self.count < self.max {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}
 
fn main() {
    let counter = Counter { count: 0, max: 5 };
    let sum: u32 = counter.map(|x| x * 2).sum();  // 2+4+6+8+10 = 30
    println!("{}", sum);
}

Iterator

  • iter() borrows elements from the collection, allowing you to traverse them without taking ownership.
  • into_iter(), on the other hand, consumes the collection, taking ownership of its elements. Once a collection is consumed, it can no longer be used.
  • iter_mut(): This method creates an iterator that mutably borrows each element of the collection.

An important feature of iterators in Rust is that they are lazy by default. This means methods like .map() and .filter() don’t immediately execute their operations. Instead, they create a chain of iterator transformations that only run when the iterator is consumed typically through methods like .collect().sum(), or .count().

 
let squares = vec![1, 2, 3, 4].iter().map(|x| x * x);
 
let squares: Vec<_> = squares.collect();
 
 
let mut numbers = vec![1, 2, 3];
for num in numbers.iter_mut() {
    *num += 1; // Mutates each element by adding 1
    println!("{}", num);
}
 
println!("{:?}", numbers); // Outputs: [2, 3, 4]

Methods

  • map(|x| f(x))
  • filter(|x| condition(x))
  • filter_map(|x| f(x))
  • find(|x| condition(x))
  • max()
  • min()

Closures

A closure is an anonymous function that can capture variables from the surrounding scope where it’s defined. Think of it as a function you can define inline and pass around like a value.

syntax

|params| action
let add = |a, b| a + b;
println!("{}", add(2, 3)); // 5
 
//how we write if we not use closurs
fn add(a: i32, b: i32) -> i32 { a + b }

ERROR HANDLING

Rust has no exceptions. It uses two mechanisms: panic! for unrecoverable errors and Result<T, E> for recoverable ones.

fn main() {
    panic!("something went terribly wrong");
    
    let v = vec![1, 2, 3];
    v[99];  // panics: index out of bounds
}
 
use std::fs::File;
use std::io::ErrorKind;
 
fn main() {
    let f = File::open("hello.txt");
    
    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Couldn't create: {:?}", e),
            },
            other => panic!("Other error: {:?}", other),
        },
    };
}

Shortcuts: unwrap, expect, and ?

unwrap I am 100% sure this contains a value. If I’m wrong, crash. to tell compiler dont want error handling

expect Same as unwrap, but with a custom panic message.

? “If it fails, return the error to the caller

fn read_file() -> Result<String, std::io::Error> {
    let content = std::fs::read_to_string("file.txt")?;
    Ok(content)
}
//is similar as below
match std::fs::read_to_string("file.txt") {
    Ok(v) => v,
    Err(e) => return Err(e),
}
use std::fs::File;
 
// unwrap — panics on Err with generic message
let f = File::open("hello.txt").unwrap();
 
// expect — panics with your custom message (better for debugging)
let f = File::open("hello.txt").expect("hello.txt should exist");
 
// The ? operator — propagates errors
fn read_username() -> Result<String, std::io::Error> {
    use std::io::Read;
    let mut f = File::open("user.txt")?;  // return Err if fails
    let mut s = String::new();
    f.read_to_string(&mut s)?;             // return Err if fails
    Ok(s)
}

unwrap_or and unwrap_or_else

let x = result.unwrap_or(10);
 
let x = result.unwrap_or_else(|e| {
    println!("error: {:?}", e);
    10
});

Smart Pointers

In Rust, a regular reference (&T) is just a pointer with borrow-checker rules. A smart pointer is a struct that acts like a pointer but adds extra capabilities: ownership semantics, reference counting, runtime borrow checking, or thread-safe sharing.

String is a smart pointer around Vec<u8>, and Vec<T> itself is a smart pointer managing heap memory. Smart pointers implement two key traits:

  • Deref — lets *ptr work, and enables auto-dereferencing (so we can call methods directly)
  • Drop — runs cleanup code when the pointer goes out of scope

Box<T> is the simplest smart pointer. It puts a value on the heap instead of the stack.

This is essential for recursive types (like linked lists) where the size might not be known at compile time.

fn main() {
    let x = Box::new(5);      // 5 lives on the heap
    println!("{}", *x);        // dereference to get 5
    println!("{}", x + 1);     // auto-deref: prints 6
}  // x drops here, heap memory freed
 
 
struct Linkednodes {
 value: i32;
 next: Option<Box<Linkednodes>>
}

Rc<T> (Reference Counted) — Allows multiple owners of the same data in single-threaded contexts. Keeps a count of references and drops the data when the count hits zero.

use std::rc::Rc;
let a = Rc::new(String::from("hello"));
let b = Rc::clone(&a);      // both a and b own the string

Arc<T> (Atomic Reference Counted) — Like Rc<T> but thread-safe, for sharing data across threads.

Mutex  Provides mutual exclusion for safe data mutation across threads. It uses a lock to ensure only one thread can access the data at a time, preventing data races.

Resources