Rust is a systems programming language known for its focus on memory safety, zero-cost abstractions, and concurrency without data races. In this guide, we’ll cover the core building blocks of Rust programming—from managing memory with ownership and borrowing to leveraging smart pointers, traits, strings, concurrency, metaprogramming, iterators, closures, and the standard library collections.

Table of Contents

  1. Ownership, Borrowing, and Lifetimes
  2. Smart Pointers
  3. Traits and Trait Objects
  4. Demystifying Strings
  5. Concurrency with std::thread
  6. Metaprogramming and Macros
  7. Iterators and Closures
  8. Standard Library Collections

Ownership, Borrowing, and Lifetimes

Rust’s unique approach to memory safety is built on three interrelated concepts:

  • Ownership: Each piece of data has a single owner. When the owner goes out of scope, the data is dropped.
  • Borrowing: Instead of transferring ownership, you can lend references to data. There are immutable borrows (&T) that allow multiple readers, and mutable borrows (&mut T) that allow one writer at a time.
  • Lifetimes: Lifetimes ensure that references remain valid as long as needed and no longer.

Example: Ownership and Moving

Imagine a treasure chest that only one pirate can own at a time.

fn main() {
    let treasure = String::from("Gold Coins"); // treasure owns the data
    let captain_treasure = treasure; // Ownership moves to captain_treasure
    // println!("{}", treasure); // Error! The original owner no longer has access.
    println!("Captain's treasure: {}", captain_treasure);
}

Example: Borrowing and Lifetimes

A pirate can lend a map without giving up the treasure.

fn main() {
    let treasure = String::from("Gold Coins");
    {
        let map = &treasure; // Borrowing the treasure as an immutable reference
        println!("Reading the treasure map: {}", map);
        // `map` is valid only within this block.
    }
    // Now, treasure is free of borrows.
    println!("Treasure is still safe: {}", treasure);
}

Example: Mutable Borrowing

A single pirate can modify the treasure, but only one mutable reference is allowed at a time.

fn main() {
    let mut treasure = String::from("Gold Coins");
    
    // Borrow immutably for a read
    println!("Initial treasure: {}", &treasure);
    
    {
        let mut mutable_ref = &mut treasure; // Unique mutable borrow
        mutable_ref.push_str(" and Silver Coins");
        println!("Modified treasure: {}", mutable_ref);
    } // mutable_ref goes out of scope here
    
    // Now we can borrow again
    println!("Final treasure: {}", treasure);
}

Smart Pointers

Smart pointers extend regular pointers with extra capabilities like heap allocation, shared ownership, and interior mutability.

Example: Box – Heap Allocation

A Box<T> moves data to the heap. Think of it as storing a heavy treasure off the ship’s deck.

fn main() {
    let boxed_treasure = Box::new("Ancient Artifact");
    println!("The treasure stored on the heap: {}", boxed_treasure);
}

Example: Rc – Shared Ownership

Rc<T> allows multiple owners. Imagine several pirates sharing the same treasure map without making copies.

use std::rc::Rc;

fn main() {
    let shared_map = Rc::new("X marks the spot");
    let pirate1 = Rc::clone(&shared_map);
    let pirate2 = Rc::clone(&shared_map);

    println!("Pirate 1 sees: {}", pirate1);
    println!("Pirate 2 sees: {}", pirate2);
    println!("Reference count: {}", Rc::strong_count(&shared_map));
}

Example: RefCell – Interior Mutability

RefCell<T> enables mutation even when data is borrowed immutably at compile time, with runtime checks. It’s like a locked chest that you can open (at runtime) to adjust its contents.

use std::cell::RefCell;

fn main() {
    let treasure_map = RefCell::new("X marks the spot");
    
    // Borrow mutably at runtime to update the map.
    *treasure_map.borrow_mut() = "X marks the spot near the old oak tree";
    
    println!("Updated treasure map: {}", treasure_map.borrow());
}

Example: Combining Rc and RefCell

When multiple owners need to mutate shared data, combine Rc and RefCell.

use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let shared_treasure = Rc::new(RefCell::new("Ancient Coins"));

    let pirate1 = Rc::clone(&shared_treasure);
    let pirate2 = Rc::clone(&shared_treasure);

    *pirate1.borrow_mut() = "Ancient Coins with a Secret Mark";
    println!("Pirate 2 sees: {}", pirate2.borrow());
}

Traits and Trait Objects

Traits define shared behavior, similar to interfaces in other languages. They enable polymorphism and code reuse.

Example: A Treasure Trait

Imagine different kinds of treasures that all can describe themselves and reveal their value.

trait Treasure {
    fn description(&self) -> String;
    fn value(&self) -> u32;
}

// Implementing the Treasure trait for a String.
impl Treasure for String {
    fn description(&self) -> String {
        format!("A shiny treasure: {}", self)
    }
    fn value(&self) -> u32 {
        100 // Fixed value for this example.
    }
}

// A struct representing a treasure map.
struct Map {
    location: String,
    multiplier: u32,
}

impl Treasure for Map {
    fn description(&self) -> String {
        format!("A map leading to: {}", self.location)
    }
    fn value(&self) -> u32 {
        self.multiplier * 10
    }
}

fn main() {
    let treasures: Vec<Box<dyn Treasure>> = vec![
        Box::new(String::from("Gold Coins")),
        Box::new(Map { location: String::from("Hidden Cave"), multiplier: 5 }),
    ];
    
    for treasure in treasures.iter() {
        println!("Description: {}", treasure.description());
        println!("Value: {}", treasure.value());
        println!("---");
    }
}

In this example, we use a trait object (Box) to store different types that share the same behavior.

Working with Strings

Rust uses two primary string types:

  • String: An owned, mutable, heap-allocated string.
  • &str: A borrowed, immutable string slice.

Example: Creating and Modifying a String

fn main() {
    let mut treasure = String::from("Gold Coins");
    treasure.push_str(" and Diamonds");
    println!("The treasure chest contains: {}", treasure);
}

Example: Borrowing a String Slice (&str)

fn main() {
    let treasure_map: &str = "Ancient Map";
    println!("The treasure map reads: {}", treasure_map);
}

Example: Converting Between String and &str

fn main() {
    let owned_treasure = String::from("Emerald");
    // Borrowing as a slice
    print_treasure(&owned_treasure);
    
    // Converting a borrowed string to an owned string
    let borrowed_map = "Ruby";
    let owned_map = borrowed_map.to_string();
    println!("Owned map: {}", owned_map);
}

fn print_treasure(treasure: &str) {
    println!("Treasure: {}", treasure);
}

These examples emphasize that you own data with a String while &str lets you borrow data safely.

Concurrency with std::thread

Rust makes concurrency safe by enforcing ownership rules even across threads.

Example: Spawning a Thread

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        println!("Thread: Searching for treasure in the forest!");
    });
    println!("Main: Searching for treasure in the cave!");
    handle.join().unwrap();
}

Example: Moving Data into a Thread

use std::thread;

fn main() {
    let treasure_map = String::from("X marks the spot");
    
    // Use `move` to transfer ownership of `treasure_map` into the closure.
    let handle = thread::spawn(move || {
        println!("Thread: Using the treasure map: {}", treasure_map);
    });
    handle.join().unwrap();
}

Example: Sharing Data with Arc and Mutex

For sharing mutable data between threads, use Arc (Atomic Reference Counted pointer) with a Mutex.

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let treasure_chest = Arc::new(Mutex::new(vec!["Gold Coins"]));
    let mut handles = vec![];

    for i in 0..4 {
        let chest = Arc::clone(&treasure_chest);
        let handle = thread::spawn(move || {
            let mut chest_guard = chest.lock().unwrap();
            chest_guard.push(format!("Treasure from pirate {}", i));
            println!("Pirate {} added treasure!", i);
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Final treasure chest: {:?}", *treasure_chest.lock().unwrap());
}

In this example, each thread safely modifies the shared treasure chest.

Metaprogramming with Macros

Macros generate code at compile time, reducing boilerplate and allowing flexible syntax.

Example: A Declarative Macro with macro_rules!

macro_rules! filter_treasures {
    ($list:expr, $threshold:expr) => {
        $list.iter()
             .cloned()
             .filter(|&value| value > $threshold)
             .collect::<Vec<_>>()
    };
}

fn main() {
    let treasures = vec![100, 200, 300, 50];
    let valuable = filter_treasures!(treasures, 150);
    println!("Valuable treasures: {:?}", valuable);
}

Example: Using Built-In Derive Macros

Rust provides built-in procedural macros to automatically implement common traits.

#[derive(Debug, Clone, PartialEq, Eq)]
struct Treasure {
    name: String,
    value: u32,
}

fn main() {
    let t1 = Treasure { name: "Golden Crown".to_string(), value: 500 };
    let t2 = t1.clone();
    println!("Treasure: {:?}\nAre they equal? {}", t1, t1 == t2);
}

Iterators and Closures

Rust’s iterators and closures offer a powerful and concise way to work with collections.

Example: Using Iterators

Instead of manually looping, you can use iterator chains:

fn main() {
    let treasures = vec![100, 200, 300, 50];
    let doubled: Vec<_> = treasures.iter()
        .filter(|&&t| t > 100)
        .map(|&t| t * 2)
        .collect();
    println!("Doubled valuable treasures: {:?}", doubled);
}

Example: Closures Capturing Environment

Closures can capture surrounding variables, making them flexible for filtering and transformations.

fn main() {
    let threshold = 150;
    let treasures = vec![100, 200, 300, 50];
    let filtered: Vec<_> = treasures.iter()
        .filter(|&&t| t > threshold)
        .cloned()
        .collect();
    println!("Treasures over {}: {:?}", threshold, filtered);
}

Example: Destructuring in Closures

Closures can destructure complex data types like tuples:

fn main() {
    let treasure_ranks = vec![(1, "gold"), (2, "silver"), (3, "bronze")];
    let descriptions: Vec<_> = treasure_ranks.iter()
        .map(|(rank, kind)| format!("Rank {}: {}", rank, kind))
        .collect();
    println!("Treasure descriptions: {:?}", descriptions);
}

Standard Library Collections

Rust’s collections let you organize data efficiently. Each collection type has characteristics suited for different tasks.

Vec – Dynamic Array

fn main() {
    let mut treasure_vault = vec!["gold coin", "silver coin", "diamond"];
    treasure_vault.push("emerald");
    for treasure in &treasure_vault {
        println!("Found: {}", treasure);
    }
}

VecDeque – Double-Ended Queue

use std::collections::VecDeque;

fn main() {
    let mut queue = VecDeque::new();
    queue.push_back("first treasure");
    queue.push_front("urgent treasure");
    while let Some(treasure) = queue.pop_front() {
        println!("Processing: {}", treasure);
    }
}

HashMap – Key-Value Store

use std::collections::HashMap;

fn main() {
    let mut inventory = HashMap::new();
    inventory.insert("gold coin", 10);
    inventory.insert("diamond", 2);
    
    for (item, count) in &inventory {
        println!("{}: {} found", item, count);
    }
}

HashSet – Unique Items

use std::collections::HashSet;

fn main() {
    let mut unique_treasures = HashSet::new();
    unique_treasures.insert("gold coin");
    unique_treasures.insert("gold coin"); // Duplicate is ignored.
    for treasure in &unique_treasures {
        println!("Unique treasure: {}", treasure);
    }
}

BinaryHeap – Priority Queue

use std::collections::BinaryHeap;

fn main() {
    let mut heap = BinaryHeap::new();
    heap.push(10);
    heap.push(50);
    heap.push(30);
    if let Some(top) = heap.peek() {
        println!("Most valuable: {}", top);
    }
}

BTreeMap and BTreeSet – Sorted Collections

use std::collections::{BTreeMap, BTreeSet};

fn main() {
    // BTreeMap: keys are stored in sorted order.
    let mut sorted_inventory = BTreeMap::new();
    sorted_inventory.insert("diamond", 150);
    sorted_inventory.insert("gold coin", 100);
    sorted_inventory.insert("silver coin", 20);
    println!("Sorted Inventory: {:?}", sorted_inventory);
    
    // BTreeSet: a sorted set of unique items.
    let mut sorted_treasures = BTreeSet::new();
    sorted_treasures.insert("emerald");
    sorted_treasures.insert("ruby");
    sorted_treasures.insert("sapphire");
    println!("Sorted Treasures: {:?}", sorted_treasures);
}

Key Takeaways

  • Ownership, Borrowing, and Lifetimes: These rules ensure that each piece of data has a single owner, borrowed data never outlives its owner, and references remain valid.
  • Smart Pointers: Use Box, Rc, and RefCell to manage heap data, share ownership, and allow controlled mutation.
  • Traits and Trait Objects: Define shared behavior with traits and use dynamic dispatch with trait objects when needed.
  • Strings: Understand the difference between an owned String and a borrowed &str; converting between them is straightforward.
  • Concurrency: Use threads with safe ownership transfer (move), and share data safely using Arc and Mutex.
  • Macros: Write macros to eliminate boilerplate and generate code at compile time.
  • Iterators and Closures: Use iterator chains and closures to work with collections in a clear, functional style.
  • Standard Collections: Choose from various collections (Vec, HashMap, BinaryHeap, etc.) based on your data organization and performance needs.