Table of Contents
- Installing Rust
- Rust Basics: Variables, Data Types, and Control Flow
- Functions and Return Values
- Ownership: Rust’s Secret Sauce
- Structs and Enums: Organizing Data
- Error Handling: Graceful Failure
- Hands-On Project: Build a “Guess the Number” Game
- Advanced Topics to Explore
- References and Further Learning
1. Installing Rust
Rust’s official toolchain manager, rustup, simplifies installation across Windows, macOS, and Linux. Follow these steps:
Step 1: Install rustup
-
Linux/macOS: Open a terminal and run:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shFollow the on-screen prompts (default options work for most users).
-
Windows: Download the rustup-init.exe installer and run it. Enable “Add Rust to PATH” during setup.
Step 2: Verify Installation
Close and reopen your terminal, then check the installed versions:
rustc --version # Rust compiler version (e.g., rustc 1.75.0)
cargo --version # Rust package manager version (e.g., cargo 1.75.0)
cargo is Rust’s build tool and package manager—it handles compiling code, managing dependencies, and creating projects.
Updating Rust
To update to the latest version:
rustup update
2. Rust Basics: Variables, Data Types, and Control Flow
Let’s start with Rust’s fundamental building blocks.
Variables: Mutable vs. Immutable
By default, variables in Rust are immutable (cannot be changed after declaration). Use mut to make them mutable:
// Immutable variable (default)
let x = 5;
// x = 6; ❌ Error: cannot assign twice to immutable variable
// Mutable variable
let mut y = 10;
y = 20; ✅ OK
Shadowing
You can “shadow” a variable by redeclaring it with the same name. This is useful for transforming values without mutability:
let spaces = " ";
let spaces = spaces.len(); // Now `spaces` is a number (3), not a string
Data Types
Rust is statically typed: the compiler checks types at compile time. Types fall into two categories:
Scalar Types (Single values)
-
Integers: Signed (
i8,i16,i32,i64,i128,isize) and unsigned (u8,u16, …,usize). Default:i32(fastest on most systems).let age: u8 = 30; // Unsigned 8-bit integer (0-255) let temperature: i32 = -5; // Signed 32-bit integer -
Floats:
f32(32-bit) andf64(64-bit, default).let pi: f64 = 3.14159; -
Booleans:
boolwith valuestrueorfalse.let is_active = true; -
Characters:
char(Unicode scalar values, e.g.,'a','😀').let heart: char = '❤️';
Compound Types (Groups of values)
-
Tuples: Fixed-size collections of mixed types.
let person: (&str, u8) = ("Alice", 30); // (name, age) let (name, age) = person; // Destructure the tuple println!("Name: {}, Age: {}", name, age); // Output: Name: Alice, Age: 30 -
Arrays: Fixed-size collections of the same type (stored on the stack, unlike dynamic vectors).
let numbers: [i32; 3] = [10, 20, 30]; // [type; length] let first = numbers[0]; // Access via index (0-based)
Control Flow
Rust supports standard control flow constructs with a few Rust-specific twists.
if Expressions
if is an expression (returns a value), so you can assign its result to a variable:
let number = 7;
let result = if number % 2 == 0 { "even" } else { "odd" };
println!("{} is {}", number, result); // Output: 7 is odd
Loops
-
loop: Runs indefinitely untilbreak.let mut counter = 0; loop { counter += 1; if counter == 5 { break; // Exit loop when counter reaches 5 } } -
while: Runs while a condition is true.let mut n = 3; while n > 0 { println!("{}!", n); n -= 1; } // Output: 3! 2! 1! -
for: Iterates over collections (arrays, ranges, etc.).let numbers = [10, 20, 30]; for num in numbers { println!("Number: {}", num); } // Iterate over a range (1..=5 = 1 to 5 inclusive) for i in 1..=5 { println!("Count: {}", i); }
3. Functions and Return Values
Functions in Rust are defined with fn and can return values explicitly or implicitly.
Basic Function Syntax
// Function with no parameters or return value
fn greet() {
println!("Hello, Rust!");
}
// Function with parameters and a return value
fn add(a: i32, b: i32) -> i32 { // -> i32 specifies return type
a + b // Implicit return (no semicolon)
}
fn main() {
greet(); // Call greet()
let sum = add(5, 3);
println!("5 + 3 = {}", sum); // Output: 5 + 3 = 8
}
Key Notes:
- Parameters require type annotations (e.g.,
a: i32). - Return values are specified with
-> Type. - Omit the semicolon in the final expression to return its value (implicit return).
4. Ownership: Rust’s Secret Sauce
Ownership is Rust’s most distinctive feature, ensuring memory safety without garbage collection. It has three core rules:
- Each value in Rust has exactly one owner.
- There can be only one owner at a time.
- When the owner goes out of scope, the value is dropped (memory is freed).
Scope and Drop
A variable’s scope is the range of code where it’s valid. When a variable goes out of scope, Rust automatically calls drop, a special function that cleans up the value:
fn main() {
{
let s = String::from("hello"); // s is valid here
println!("{}", s); // Output: hello
} // s goes out of scope; `drop` is called, freeing memory
// println!("{}", s); ❌ Error: s is no longer valid
}
Move Semantics
When a value is assigned to another variable or passed to a function, ownership moves to the new variable. The original variable is no longer usable:
let s1 = String::from("hello");
let s2 = s1; // s1's ownership moves to s2
// println!("{}", s1); ❌ Error: s1 is no longer valid (value was moved)
println!("{}", s2); ✅ OK
Why? Strings (unlike integers) store data on the heap. To avoid double-free errors (freeing the same memory twice), Rust invalidates the original owner.
Cloning
To create a deep copy of a heap-allocated value (instead of moving ownership), use clone:
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy: s1 and s2 own separate data
println!("s1: {}, s2: {}", s1, s2); ✅ OK (both valid)
Borrowing
Instead of transferring ownership, you can borrow a value using references (&). Borrowed values are valid only as long as the owner exists.
Immutable Borrowing
You can borrow a value immutably (read-only) multiple times:
fn print_string(s: &String) { // s is an immutable reference
println!("{}", s);
}
fn main() {
let s = String::from("hello");
print_string(&s); // Borrow s immutably
print_string(&s); // ✅ OK: multiple immutable borrows allowed
}
Mutable Borrowing
You can borrow a value mutably (read-write), but only once at a time (prevents data races):
fn append_world(s: &mut String) { // s is a mutable reference
s.push_str(" world");
}
fn main() {
let mut s = String::from("hello");
append_world(&mut s); // Borrow s mutably
println!("{}", s); // Output: hello world
// ❌ Error: cannot borrow s as mutable more than once
// let r1 = &mut s;
// let r2 = &mut s;
}
5. Structs and Enums: Organizing Data
Structs and enums help you group related data and define custom types.
Structs: Custom Data Structures
Structs (short for “structures”) let you bundle multiple values into a single type:
// Define a struct
struct User {
username: String,
email: String,
age: u8,
is_active: bool,
}
fn main() {
// Create an instance of User
let mut alice = User {
username: String::from("alice123"),
email: String::from("[email protected]"),
age: 30,
is_active: true,
};
// Access fields with dot notation
println!("Username: {}", alice.username);
// Modify a field (requires mutable User)
alice.email = String::from("[email protected]");
}
Struct Methods
Use impl blocks to define methods (functions associated with a struct):
impl User {
// Instance method (takes &self as first parameter)
fn greet(&self) {
println!("Hello, I'm {}!", self.username);
}
// Associated function (no &self; like a static method)
fn new(username: String, email: String, age: u8) -> Self {
User {
username,
email,
age,
is_active: true, // Default value
}
}
}
fn main() {
let bob = User::new(
String::from("bob456"),
String::from("[email protected]"),
25,
);
bob.greet(); // Output: Hello, I'm bob456!
}
Enums: Enumerate Possible Values
Enums define types with a fixed set of possible values (variants):
// Define an enum
enum IpAddrKind {
V4,
V6,
}
// Enums can store data in variants
enum IpAddr {
V4(u8, u8, u8, u8), // Stores four u8 values
V6(String), // Stores a String
}
fn main() {
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
}
The Option Enum
Rust has no null, but Option<T> (from the standard library) represents a value that may be Some(T) or None:
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None // No result (division by zero)
} else {
Some(a / b) // Wrap result in Some
}
}
fn main() {
let result = divide(10.0, 2.0);
match result {
Some(val) => println!("Result: {}", val), // Output: Result: 5
None => println!("Error: division by zero"),
}
}
6. Error Handling: Graceful Failure
Rust encourages explicit error handling. It uses two main types: panic! for unrecoverable errors and Result<T, E> for recoverable errors.
panic! for Unrecoverable Errors
panic! stops execution and prints a message. Use it for bugs or unexpected failures:
fn main() {
let v = vec![1, 2, 3];
v[99]; // ❌ Panic: index out of bounds
// Or explicitly: panic!("Something went wrong!");
}
Result<T, E> for Recoverable Errors
Result<T, E> is an enum with two variants:
Ok(T): Success, containing the result.Err(E): Failure, containing an error.
Example: Reading a file (recoverable error):
use std::fs::File;
fn main() {
let file_result = File::open("hello.txt");
let file = match file_result {
Ok(f) => f, // Success: use the file
Err(e) => { // Failure: handle the error
panic!("Failed to open file: {:?}", e);
}
};
}
The ? Operator
The ? operator simplifies error propagation: if Result is Ok, it returns the value; if Err, it returns the error from the function:
use std::fs::File;
use std::io::Read;
fn read_file() -> Result<String, std::io::Error> { // Return Result
let mut file = File::open("hello.txt")?; // Propagate error with ?
let mut contents = String::new();
file.read_to_string(&mut contents)?; // Propagate error with ?
Ok(contents) // Return success
}
fn main() {
match read_file() {
Ok(contents) => println!("File contents: {}", contents),
Err(e) => println!("Error: {}", e),
}
}
7. Hands-On Project: Build a “Guess the Number” Game
Let’s apply what we’ve learned by building a CLI game where the user guesses a random number between 1 and 100.
Step 1: Create a New Project
Use cargo new to scaffold a project:
cargo new guess_the_number
cd guess_the_number
Step 2: Add Dependencies
We’ll use the rand crate for random number generation. Add it to Cargo.toml:
[dependencies]
rand = "0.8.5" # Check crates.io for the latest version
Step 3: Write the Code
Replace src/main.rs with:
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
// Generate a random number between 1 and 100
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
// Get user input
println!("Please input your guess:");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
// Parse input to a number (handle errors)
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Please enter a valid number!");
continue; // Restart the loop
}
};
// Compare guess with secret number
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break; // Exit loop on success
}
}
}
}
Step 4: Run the Game
cargo run
Play the game: enter numbers until you guess correctly!
8. Advanced Topics to Explore
Once you’ve mastered the basics, dive into these advanced concepts:
- Lifetimes: Annotate references to ensure they outlive the data they point to.
- Traits: Define shared behavior (like interfaces in other languages).
- Generics: Write flexible, reusable code (e.g.,
Vec<T>for any typeT). - Async/Await: Build concurrent applications with non-blocking I/O (using
tokioorasync-std). - Crates Ecosystem: Explore popular crates like
serde(serialization),reqwest(HTTP client), orsqlx(database access).
9. References and Further Learning
- The Rust Programming Language Book: Official free book (must-read).
- Rustlings: Interactive exercises to practice Rust.
- docs.rs: Crate documentation for exploring libraries.
- Rust Community: Reddit (r/rust), Discord (rust-lang), and Stack Overflow.
Rust has a steep learning curve, but its focus on safety and performance makes it worth the effort. Start small, experiment, and don’t hesitate to ask the community for help. Happy coding! 🦀