Home / Rust / Advanced Rust Concepts: Building on Your Foundation

Advanced Rust Concepts: Building on Your Foundation

Advanced Rust Concepts: Building on Your Foundation

Great job getting started with Rust basics! Now that you understand variables, functions, control flow, and basic data types, let's dive deeper into more advanced concepts that will make you a much more capable Rust programmer.

Understanding Ownership in Depth

The ownership system is Rust's biggest strength. Let me explain how to work with it effectively:

fn main() {
    // Moving values between functions
    let s1 = String::from("Hello");
    let s2 = take_ownership(s1);  // s1 is moved into the function
    
    println!("{}", s2);  // This works!
    // println!("{}", s1);  // This would cause an error - s1 was moved
    
    // Borrowing instead of moving
    let s3 = String::from("World");
    let len = calculate_length(&s3);  // Pass reference (borrow)
    
    println!("Length of '{}' is {}", s3, len);  // s3 still works!
}

fn take_ownership(s: String) -> String {
    println!("{}", s);
    s  // Return the string back to caller
}

fn calculate_length(s: &String) -> usize {
    s.len()  // We can read from reference but not modify
}

Mutable References

fn main() {
    let mut s = String::from("Hello");
    
    change(&mut s);  // Pass mutable reference
    println!("{}", s);
}

fn change(s: &mut String) {
    s.push_str(", world!");  // Can modify through mutable reference
}

Working with Structs and Methods

Structs let you create custom data types:

// Define a struct
struct Person {
    name: String,
    age: u32,
}

impl Person {
    // Associated function (like static method)
    fn new(name: &str, age: u32) -> Person {
        Person {
            name: name.to_string(),
            age,
        }
    }
    
    // Method that takes self
    fn get_info(&self) -> String {
        format!("{} is {} years old", self.name, self.age)
    }
    
    // Method that modifies self
    fn have_birthday(&mut self) {
        self.age += 1;
    }
}

fn main() {
    let mut person = Person::new("Alice", 30);
    
    println!("{}", person.get_info());
    person.have_birthday();
    println!("After birthday: {}", person.get_info());
}

Enums for Flexible Data

Enums represent data that can be one of several variants:

enum Direction {
    North,
    South,
    East,
    West,
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u8, u8, u8),
}

fn main() {
    let direction = Direction::North;
    
    match direction {
        Direction::North => println!("Heading north"),
        Direction::South => println!("Heading south"),
        Direction::East => println!("Heading east"),
        Direction::West => println!("Heading west"),
    }
    
    // Using enums with data
    let msg = Message::Write("Hello".to_string());
    
    match msg {
        Message::Quit => println!("Quitting..."),
        Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
        Message::Write(text) => println!("Writing: {}", text),
        Message::ChangeColor(r, g, b) => println!("Changing color to RGB({}, {}, {})", r, g, b),
    }
}

Working with Options and Results

These are essential for handling potentially missing or error-prone values:

fn main() {
    // Option<T> - either Some(T) or None
    let maybe_number: Option<i32> = find_number("42");
    
    match maybe_number {
        Some(n) => println!("Found number: {}", n),
        None => println!("No number found"),
    }
    
    // Using unwrap_or for simpler handling
    let result = find_number("abc").unwrap_or(0);
    println!("Result is: {}", result);
}

fn find_number(input: &str) -> Option<i32> {
    input.parse::<i32>().ok()  // Convert string to number, return None if fails
}

Working with Vectors and Iterators

fn main() {
    let mut numbers = vec![1, 2, 3, 4, 5];
    
    // Iterator methods
    let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
    println!("Doubled: {:?}", doubled);
    
    // Filter and collect
    let even_numbers: Vec<i32> = numbers.iter().filter(|&x| x % 2 == 0).collect();
    println!("Even numbers: {:?}", even_numbers);
    
    // Using for loop with enumerate
    for (index, value) in numbers.iter().enumerate() {
        println!("Index {} has value {}", index, value);
    }
}

Error Handling Patterns

// Custom error type using Result
#[derive(Debug)]
enum CalculatorError {
    DivisionByZero,
    InvalidInput,
}

fn divide(a: f64, b: f64) -> Result<f64, CalculatorError> {
    if b == 0.0 {
        Err(CalculatorError::DivisionByZero)
    } else {
        Ok(a / b)
    }
}

fn main() {
    let result = divide(10.0, 2.0);
    
    match result {
        Ok(value) => println!("Result: {}", value),
        Err(error) => println!("Error: {:?}", error),
    }
}

Practical Example: A Simple Todo App

Here's how we can put these concepts together in a practical example:

#[derive(Debug, Clone)]
struct Task {
    id: u32,
    description: String,
    completed: bool,
}

impl Task {
    fn new(id: u32, description: &str) -> Task {
        Task {
            id,
            description: description.to_string(),
            completed: false,
        }
    }
    
    fn complete(&mut self) {
        self.completed = true;
    }
}

struct TodoApp {
    tasks: Vec<Task>,
    next_id: u32,
}

impl TodoApp {
    fn new() -> TodoApp {
        TodoApp {
            tasks: Vec::new(),
            next_id: 1,
        }
    }
    
    fn add_task(&mut self, description: &str) -> u32 {
        let task = Task::new(self.next_id, description);
        self.tasks.push(task);
        self.next_id += 1;
        self.next_id - 1
    }
    
    fn complete_task(&mut self, id: u32) -> bool {
        for task in &mut self.tasks {
            if task.id == id {
                task.complete();
                return true;
            }
        }
        false
    }
    
    fn list_tasks(&self) {
        println!("Tasks:");
        for task in &self.tasks {
            let status = if task.completed { "✓" } else { "○" };
            println!("{} [{}] {}", status, task.id, task.description);
        }
    }
}

fn main() {
    let mut app = TodoApp::new();
    
    app.add_task("Learn Rust basics");
    app.add_task("Build a simple project");
    app.complete_task(1);
    
    app.list_tasks();
}

Memory Management Concepts

Rust's ownership system means you never have to worry about memory leaks or dangling pointers:

fn main() {
    // Stack allocation (fast, automatic cleanup)
    let x = 42;
    let y = x;  // Copy happens automatically
    
    println!("x: {}, y: {}", x, y);  // Both work fine
    
    // Heap allocation with String
    let s1 = String::from("Hello");
    let s2 = s1.clone();  // Explicit clone for heap data
    
    println!("s1: {}, s2: {}", s1, s2);  // Both still work!
    
    // Function calls and ownership transfer
    let s3 = String::from("World");
    process_string(s3);  // s3 is moved into function
    // println!("{}", s3);  // This would error - s3 was moved
    
    let s4 = String::from("Rust");
    use_string(&s4);     // Borrow instead of move
    println!("Still works: {}", s4);  // s4 still available!
}

fn process_string(s: String) {
    println!("Processing: {}", s);
    // s is dropped here when function ends
}

fn use_string(s: &String) {
    println!("Using: {}", s);
    // s is borrowed, so caller can still use it
}

Key Takeaways

  1. Ownership: Values are moved by default; use references (&) to borrow
  2. Methods and Traits: Extend structs with functionality using impl blocks
  3. Enums: Handle multiple possible values elegantly
  4. Options and Results: Build robust error handling into your programs
  5. Iterators: Work with collections in a functional style

What's Next?

You're now ready to tackle more complex Rust concepts like:

  • Working with traits (interfaces) for code reuse
  • Creating more sophisticated data structures
  • Understanding lifetimes (how long references live)
  • Using crates and the Rust ecosystem
  • Writing tests for your Rust programs

Remember: Rust is a language that rewards careful thinking about memory management. The initial learning curve pays off in the form of extremely reliable, fast code without garbage collection overhead.

Keep practicing these concepts with small projects, and you'll be amazed at how much more powerful and flexible your Rust programming skills become!