Getting Started

To install Rust, please follow the official documentation. You should, at least, have the Rust compiler (rustc) installed.


Managing Rust Projects with Cargo

cargo is Rust's official build tool and package manager. It simplifies project management, dependency handling, and building.

Common Cargo Commands

cargo new my_project      # Create a new project
cargo build               # Build the project
cargo build --release     # Build an optimized release binary
cargo run                 # Build and run the project
cargo test                # Run tests

Benefits of Cargo

  • Dependency Management: Automatically fetches and manages project dependencies from crates.io.
  • Build Automation: Handles compilation, linking, and building release artifacts.
  • Conventional Project Structure: Enforces a consistent project structure, making Rust projects familiar and easy to navigate.

Control Flow

Rust provides flexible control flow constructs, including if statements, while loops, and for loops.

If-Statement

The if statement syntax is straightforward:

let x = 5;
if x > 0 {
    println!("Positive!");
} else {
    println!("Non-positive!");
}

Loops

While Loop

let mut n = 3;
while n > 0 {
    println!("Countdown: {}", n);
    n -= 1;
}

For Loop

The for loop is used to iterate over a collection.

let numbers = vec![1, 2, 3, 4, 5];
for n in numbers {
    println!("Number: {}", n);
}

Ranges and Iterators

Rust has a powerful syntax for creating ranges and using iterators, inspired by functional programming.

Ranges

  • 0..n: Creates a range from 0 up to (but not including) n.
  • 0..=n: Creates a range from 0 up to and including n.
for n in 1..5 {
    // Iterates from 1 to 4
}
for n in 1..=5 {
    // Iterates from 1 to 5
}

Iterators

Iterators allow you to perform operations on a sequence of items. Common methods include iter(), map(), sum(), and collect().

let numbers = vec![1, 2, 3, 4, 5];

// Get the sum of all numbers
let sum: i32 = numbers.iter().sum();

// Get a new vector with each number squared
let squares: Vec<i32> = numbers.iter().map(|x| x * x).collect();

Closures

A closure (also known as a lambda) is an anonymous function you can save in a variable or pass as an argument to other functions.

  • Closures are defined using the |param| body syntax.
  • In the example above, |x| x * x is a closure that takes x and returns its square.

Pattern Matching

Rust's match construct is a powerful control flow operator that allows you to compare a value against a series of patterns and execute code based on which pattern matches.

let x = 5;
let result = match x {
    1 => "One",
    2 | 3 => "Two or Three",
    n if n < 0 => "Negative",
    _ => "Other",
};

Handling Absence: Option and Result

Rust provides two special enums, Option<T> and Result<T, E>, to handle cases where a value might be missing or an operation might fail.

Option<T>

Represents a value that can either be something (Some(T)) or nothing (None).

Result<T, E>

Represents a value that can either be a success (Ok(T)) or an error (Err(E)).

pub enum Option<T> {
    None,
    Some(T),
}

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

These types force you to handle potential failures, leading to more robust and reliable code.

let maybe_value: Option<i32> = Some(42);
let value = maybe_value.unwrap_or(0); // Returns 42, or 0 if it was None

let result: Result<i32, &str> = Ok(100);
let next_result = result.map(|x| x * 2); // Produces Ok(200)

Parsing Input from Stdin

Here’s a brief overview of reading and parsing integers from standard input.

Reading a Line

Use std::io::stdin().read_line() to read a line of text.

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

Parsing a String

Use the .parse() method to convert a string to another type.

let parsed_num: Result<i32, _> = input.trim().parse();
match parsed_num {
    Ok(number) => println!("You entered: {}", number),
    Err(_) => println!("That's not a valid integer!"),
}

Ownership, Borrowing, and Heap Allocation

Ownership

In Rust, every value has a single owner. When the owner goes out of scope, the value is dropped.

  • Ownership can be moved from one variable to another.
  • Once moved, the original variable can no longer be used.
let s1 = String::from("hello");
let s2 = s1; // Ownership of the string is moved to s2
// println!("{}", s1); // This would cause a compile-time error!

Borrowing

You can create references to a value without taking ownership. This is called borrowing.

  • Immutable Borrow: &T lets you read the data but not modify it. You can have multiple immutable borrows at once.
  • Mutable Borrow: &mut T lets you read and modify the data. You can only have one mutable borrow at a time.
fn calculate_length(s: &String) -> usize { // s is an immutable borrow
    s.len()
}

fn change(s: &mut String) { // s is a mutable borrow
    s.push_str(", world");
}

Heap Allocation with Box<T>

The Box<T> smart pointer allows you to allocate memory on the heap. When a Box<T> goes out of scope, its destructor is called, and the memory is deallocated.

fn heap_allocation() -> Box<Vec<i32>> {
    // Allocates a vector on the heap
    Box::new(vec![1, 2, 3])
}
// When `data` goes out of scope, the memory is freed.
let data = heap_allocation();

Practical Example: Parsing a Graph

Step 1: Define the Data Structure

struct Graph {
    num_vertices: usize,
    adj: Vec<Vec<usize>>,
    rev_adj: Vec<Vec<usize>>,
}

Step 2: Parse from Stdin

fn parse_graph() -> Result<Box<Graph>, std::io::Error> {
    let mut input = String::new();
    std::io::stdin().read_line(&mut input)?;
    let mut parts = input.trim().split_whitespace();
    let v: usize = parts.next().unwrap().parse().unwrap();
    let mut e: usize = parts.next().unwrap().parse().unwrap();

    let mut adj = vec![vec![]; v];
    let mut rev_adj = vec![vec![]; v];

    while e > 0 {
        input.clear();
        std::io::stdin().read_line(&mut input)?;
        let mut edge_parts = input.trim().split_whitespace();
        let u: usize = edge_parts.next().unwrap().parse().unwrap();
        let w: usize = edge_parts.next().unwrap().parse().unwrap();

        adj[u - 1].push(w - 1);
        rev_adj[w - 1].push(u - 1);
        e -= 1;
    }

    Ok(Box::new(Graph { num_vertices: v, adj, rev_adj }))
}

Step 3: Entry Point

fn main() -> Result<(), std::io::Error> {
    // The graph is allocated on the heap, and its ownership is
    // moved to the `graph` variable.
    let graph = parse_graph()?;

    // ... perform operations with the graph ...

    // When `main` ends, `graph` goes out of scope, and the memory
    // is automatically freed.
    Ok(())
}

Tips and Tricks

Smart Referencing & Dereferencing Rust automatically adds or removes &, &mut, and * in certain contexts, making code cleaner. For example, you can call methods on a Box<T> as if it were a stack-allocated object.

let boxed_graph: Box<Graph> = parse_graph().unwrap();
// `process_graph` expects `&Graph`, but we can pass `&boxed_graph`.
// Rust automatically coerces `&Box<Graph>` to `&Graph`.
process_graph(&boxed_graph);

Optimize with Asserts Using assert! can help the compiler optimize away bounds checks, leading to faster code when you can guarantee invariants.

fn process(data: &Vec<i32>, size: usize) {
    assert!(data.len() >= size);
    for i in 0..size {
        // Because of the assert, the compiler can remove
        // the bounds check for `data[i]`.
        let _ = data[i];
    }
}

Error Propagation with ? The ? operator is a clean way to propagate errors. If a function returns a Result, ? will unwrap Ok values or cause an early return of Err values.

Useful Resources