Learning Rust Basics

Rust is a systems programming language focused on memory safety, concurrency, and performance without a garbage collector. These notes cover the fundamentals from the Rustlings course.

Installation

Rust is installed using the official rustup toolchain manager:

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

The Rustlings course provides interactive exercises:

cargo install rustlings
rustlings init

Basic Syntax

Hello World

The main function is the program entry point. Output is printed with println!():

fn main() {
    println!("Hello World!");
}

Variables

Variables are declared with let. Types can be inferred or explicitly annotated with :. Mutable variables require the mut keyword:

fn main() {
    let num = "THREE";
    println!("The number is {num}");
    let mut num: i32 = 10;
    println!("The number is {num}");
    num = 100;
    println!("The number is {num}");
}

A variable can be redeclared with a different type in the same scope (shadowing).

Functions

Functions are defined with fn. Return values use return or the implicit last expression (no semicolon):

fn call_me(num: i32) -> i32 {
    return num * num;
}
 
fn main() {
    let square = call_me(32);
    println!("The square of 32 is {square}");
}

If-Else

Conditionals do not require parentheses around the condition. The last expression in each branch is implicitly returned:

fn check_str(fruit: String) -> i32 {
    if fruit == "apple" {
        1
    } else if fruit == "orange" {
        2
    } else {
        3
    }
}

Vectors

Vectors are heap-allocated dynamic arrays. Unlike C++ std::vector, Rust vectors do not implement the Copy trait, so they are moved rather than copied when assigned to another variable:

fn main() {
    let mut v: Vec<i32> = vec![10, 20, 30, 40];
    for i in 0..=10 {
        v.push(i);
    }
    let mut v1 = Vec::<i32>::new();
    v1.push(5);
    let mut v2 = v; // v is moved, no longer usable
    v2.push(11);
}

Mapping over Vectors

Vectors can be transformed using iter() and map(), collected back into a new vector:

fn double_element(vector: Vec<i32>) -> Vec<i32> {
    vector.iter().map(|v| v * 2).collect()
}

Move Semantics and Ownership

Rust uses an ownership system to manage memory without a garbage collector. Every value has exactly one owner at any time. When a variable is assigned to another or passed to a function, ownership is moved:

fn main() {
    let s: String = String::from("Hello");
    take_ownership(s);
    // s is no longer usable here
 
    let mut s1: String = give_ownership("Hello");
    println!("Main received {}", s1);
 
    let s2: String = take_and_give_ownership(&mut s1);
    println!("Main received {}", s2);
}
 
fn take_ownership(s: String) {
    println!("Took ownership of {}", s);
}
 
fn give_ownership(s: &str) -> String {
    s.to_string()
}
 
fn take_and_give_ownership(s: &mut String) -> String {
    s.push_str(", World!");
    s.to_string()
}

References and Borrowing

To avoid moving ownership, pass a reference with &. References are cleared after their last use at compile time:

  • Any number of immutable references (&T) can coexist.
  • Only one mutable reference (&mut T) can exist at a time, and no immutable references can coexist with it.
fn main() {
    let mut num: i32 = 5;
    let num1 = &num;
    let num2 = &num; // Valid: both are immutable
    println!("{}, {}", num1, num2); // num1, num2 cleared after last use
 
    let num3 = &mut num;
    // let num4 = &num; // Invalid: mutable ref exists
    println!("{}", num3); // num3 cleared after last use
 
    let num5 = &mut num; // Valid: num3 no longer used
    *num5 += 1;
}

This system prevents data races at compile time by ensuring either one mutable writer or multiple immutable readers at any point.

References