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 | shThe Rustlings course provides interactive exercises:
cargo install rustlings
rustlings initBasic 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 = #
let num2 = # // Valid: both are immutable
println!("{}, {}", num1, num2); // num1, num2 cleared after last use
let num3 = &mut num;
// let num4 = # // 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.