Table of Contents
- Getting Started: Installing Rust
- Hello, World! Your First Rust Program
- Basic Syntax: Variables, Data Types, and Control Flow
- Ownership: Rust’s Secret Sauce
- Functions and Return Values
- Error Handling: Result, Option, and Panic
- Structs and Enums: Defining Custom Types
- Generics: Writing Flexible Code
- Traits: Defining Shared Behavior
- Modules and Packages: Organizing Code
- Concurrency: Threads, Channels, and Mutexes
- Unsafe Rust: When to Break the Rules
- Real-World Applications of Rust
- References and Further Learning
1. Getting Started: Installing Rust
Rust’s official installer, rustup, manages versions and tools. Install it on Linux, macOS, or Windows (via WSL or PowerShell):
# Linux/macOS
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Windows (PowerShell)
irm https://sh.rustup.rs | sh
Follow the prompts, then restart your terminal. Verify installation:
rustc --version # Rust compiler version (e.g., rustc 1.75.0)
cargo --version # Rust package manager (e.g., cargo 1.75.0)
cargo is Rust’s build tool and package manager—we’ll use it for creating projects, compiling, testing, and dependencies.
2. Hello, World! Your First Rust Program
Let’s create a “Hello, World!” project with cargo:
cargo new hello_world # Creates a new directory "hello_world"
cd hello_world
Cargo generates a project structure:
hello_world/
├── Cargo.toml # Manifest: project metadata, dependencies
└── src/
└── main.rs # Main source file
Open src/main.rs:
fn main() {
println!("Hello, World!");
}
fn main() is the entry point. println! is a macro (note the !) for printing to the console. Run the program:
cargo run # Compiles and runs the program
Output:
Hello, World!
3. Basic Syntax: Variables, Data Types, and Control Flow
Variables
Variables in Rust are immutable by default (cannot be reassigned). Use mut to make them mutable:
let x = 5; // Immutable
let mut y = 10; // Mutable
y = 20; // Reassign (allowed with mut)
Shadowing: Declare a new variable with the same name, overshadowing the old one:
let x = 5;
let x = x + 1; // x is now 6 (new variable, not reassignment)
Data Types
Rust is statically typed; types are inferred or explicitly declared.
Scalars: Single values
- Integers:
i8,i16,i32(default),i64,u8(unsigned), etc. - Floats:
f32,f64(default) - Booleans:
bool(true/false) - Chars:
char(Unicode, e.g.,'a','😀')
Compound: Groups of values
- Tuples: Fixed-size, heterogeneous:
let tup: (i32, f64, bool) = (500, 6.4, true); - Arrays: Fixed-size, homogeneous:
let arr: [i32; 3] = [1, 2, 3];(type[T; N])
Control Flow
If-Else:
let number = 3;
if number % 2 == 0 {
println!("Even");
} else {
println!("Odd"); // Output: Odd
}
Loops:
loop: Infinite loop (break withbreak):let mut count = 0; loop { count += 1; if count == 3 { break; // Exit loop when count is 3 } }while: Conditional loop:let mut n = 5; while n > 0 { println!("{}", n); // 5, 4, 3, 2, 1 n -= 1; }for: Iterate over collections:let arr = [10, 20, 30]; for num in arr { println!("{}", num); // 10, 20, 30 }
Match: Powerful pattern matching (like a switch on steroids):
let coin = "penny";
match coin {
"penny" => println!("1 cent"),
"nickel" => println!("5 cents"),
_ => println!("Unknown coin"), // Default case
}
4. Ownership: Rust’s Secret Sauce
Ownership is Rust’s unique system for managing memory without a garbage collector. It ensures safety and performance via three rules:
- Each value has an owner.
- There can be only one owner at a time.
- When the owner goes out of scope, the value is dropped.
Example: Strings and Ownership
let s1 = String::from("hello"); // s1 owns the String
let s2 = s1; // s1 is MOVED to s2 (s1 is now invalid)
// println!("{}", s1); // Error: s1 is no longer the owner
Why? Strings store data on the heap. Copying the entire heap data would be expensive, so Rust transfers ownership instead (move semantics). To copy data, use clone():
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy: s1 and s2 both own their data
println!("s1: {}, s2: {}", s1, s2); // Works!
Borrowing
Temporarily allow access to a value without taking ownership using references (&):
fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
} // s goes out of scope; no drop (it doesn't own the data)
let s = String::from("hello");
let len = calculate_length(&s); // Borrow s
println!("Length: {}", len); // s is still valid
Mutable Borrows: Allow modifying the borrowed value (only one mutable reference at a time to prevent data races):
fn append_world(s: &mut String) {
s.push_str(" world");
}
let mut s = String::from("hello");
append_world(&mut s); // Mutable borrow
println!("{}", s); // Output: hello world
Slices
Slices are references to a contiguous sequence of elements in a collection (no ownership):
let s = String::from("hello world");
let hello = &s[0..5]; // Slice from index 0 to 4: "hello"
let world = &s[6..11]; // Slice from index 6 to 10: "world"
5. Functions and Return Values
Define functions with fn. Parameters and return types are explicit:
// Function with parameters (i32, i32) and return type i32
fn add(a: i32, b: i32) -> i32 {
a + b // No semicolon: this is an expression (returns the value)
}
fn main() {
let sum = add(2, 3);
println!("Sum: {}", sum); // Output: 5
}
Expressions vs. Statements:
- Statements: Do not return values (e.g.,
let x = 5;). - Expressions: Return values (e.g.,
x + 5, blocks{}).
A function’s body is a block; the last expression is the return value (no return needed unless exiting early).
6. Error Handling: Result, Option, and Panic
Rust emphasizes explicit error handling. Two main error types:
Unrecoverable Errors: panic!
Use panic! for bugs (e.g., invalid state). It prints a message, unwinds the stack, and exits:
let v = vec![1, 2, 3];
v[99]; // Panic: index out of bounds
Recoverable Errors: Result Enum
Result<T, E> represents success (Ok(T)) or failure (Err(E)):
use std::fs::File;
fn main() {
let f = File::open("hello.txt"); // File::open returns Result<File, io::Error>
let f = match f {
Ok(file) => file,
Err(error) => panic!("Failed to open file: {:?}", error),
};
}
Propagating Errors with ?
The ? operator simplifies propagating errors (works in functions returning Result):
use std::fs::File;
use std::io::Read;
fn read_username_from_file() -> Result<String, std::io::Error> {
let mut f = File::open("hello.txt")?; // Return Err if open fails
let mut s = String::new();
f.read_to_string(&mut s)?; // Return Err if read fails
Ok(s) // Return Ok with the string
}
Option Enum
Option<T> represents a value that may be present (Some(T)) or absent (None):
fn find_char(s: &str, c: char) -> Option<usize> {
for (i, ch) in s.chars().enumerate() {
if ch == c {
return Some(i);
}
}
None
}
let index = find_char("hello", 'l');
match index {
Some(i) => println!("Found at index {}", i), // Output: Found at index 2
None => println!("Not found"),
}
7. Structs and Enums: Defining Custom Types
Structs: Grouping Data
Structs bundle related data:
// Define a struct
struct Rectangle {
width: u32,
height: u32,
}
// Implement methods for Rectangle
impl Rectangle {
fn area(&self) -> u32 { // &self: reference to the instance
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
let rect1 = Rectangle { width: 30, height: 50 };
println!("Area: {}", rect1.area()); // Output: 1500
let rect2 = Rectangle { width: 20, height: 40 };
println!("Can hold rect2? {}", rect1.can_hold(&rect2)); // Output: true
Enums: Multiple Variants
Enums define types with multiple possible values:
enum IpAddr {
V4(u8, u8, u8, u8), // Variant with data
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
Option<T> and Result<T, E> are built-in enums:
Option<T>:Some(T)orNone(no value).Result<T, E>:Ok(T)(success) orErr(E)(failure).
8. Generics: Writing Flexible Code
Generics allow writing code that works with multiple types without repeating logic.
Generic Functions
fn largest<T: PartialOrd>(list: &[T]) -> &T { // T must implement PartialOrd
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
let numbers = [10, 30, 20];
let max = largest(&numbers);
println!("Max: {}", max); // Output: 30
let floats = [1.5, 3.7, 2.2];
let max_float = largest(&floats);
println!("Max float: {}", max_float); // Output: 3.7
Generic Structs
struct Point<T, U> {
x: T,
y: U,
}
let int_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.5, y: 3.0 };
9. Traits: Defining Shared Behavior
Traits define a set of methods that types can implement (like interfaces in other languages).
Define a Trait
pub trait Summary {
fn summarize(&self) -> String; // Method signature
}
Implement a Trait
pub struct NewsArticle {
pub headline: String,
pub author: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {}", self.headline, self.author)
}
}
let article = NewsArticle {
headline: String::from("Rust Hits 1.0!"),
author: String::from("John Doe"),
};
println!("Summary: {}", article.summarize()); // Output: Rust Hits 1.0!, by John Doe
Trait Bounds
Use traits to constrain generics:
fn notify<T: Summary>(item: T) {
println!("Breaking news: {}", item.summarize());
}
notify(article); // Works: NewsArticle implements Summary
10. Modules and Packages: Organizing Code
Modules group related code, controlling visibility with pub (public) and private (default).
Example Module Structure
// src/lib.rs
mod garden {
pub mod vegetables { // Public module
pub struct Carrot { // Public struct
pub color: String,
}
}
}
// Use `use` to bring items into scope
use crate::garden::vegetables::Carrot;
pub fn farm() {
let carrot = Carrot { color: String::from("orange") };
println!("Carrot color: {}", carrot.color);
}
Packages and Crates
- Crate: A binary or library (compiled unit).
- Package: A bundle of crates with a
Cargo.toml.
A package can have:
- One binary crate (
src/main.rs). - Multiple binary crates (
src/bin/name.rs). - One library crate (
src/lib.rs).
11. Concurrency: Threads, Channels, and Mutexes
Rust’s concurrency model ensures safety with ownership and type checking.
Threads
Spawn threads with std::thread::spawn:
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
Message Passing with Channels
Send data between threads via channels (std::sync::mpsc):
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel(); // Transmitter (tx), Receiver (rx)
thread::spawn(move || {
let msg = String::from("Hello from spawned thread!");
tx.send(msg).unwrap(); // Send message
});
let received = rx.recv().unwrap(); // Block until message is received
println!("Got: {}", received); // Output: Hello from spawned thread!
}
Shared State with Mutexes
A Mutex<T> (mutual exclusion) allows shared access to data (only one thread at a time):
use std::sync::{Mutex, Arc};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0)); // Arc: Atomic Reference Counting
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter); // Clone the Arc (not the data)
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap(); // Lock the mutex
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap()); // Output: 10
}
12. Unsafe Rust: When to Break the Rules
Rust allows “unsafe” operations for low-level control (e.g., FFI, direct memory access). Unsafe code is enclosed in unsafe blocks and must follow these rules:
- Dereference raw pointers (
*const T,*mut T). - Call unsafe functions or methods.
- Access or modify mutable static variables.
- Implement unsafe traits.
Example: Dereferencing a raw pointer:
let mut num = 5;
let r1 = &num as *const i32; // Raw pointer (immutable)
let r2 = &mut num as *mut i32; // Raw pointer (mutable)
unsafe {
println!("r1: {}", *r1); // Dereference raw pointer
*r2 = 10;
}
println!("num: {}", num); // Output: 10
Unsafe code is not inherently dangerous—you must ensure safety!
13. Real-World Applications of Rust
- Systems Programming: Operating systems (Redox OS), kernels (Linux kernel modules), embedded systems.
- Web Development: Backends (Actix, Rocket), WebAssembly (Yew, Leptos for frontend).
- DevTools:
ripgrep(search tool),exa(ls alternative),bat(cat alternative). - Large-Scale Apps: Firefox (Servo engine), Dropbox (sync engine), Cloudflare (edge services).
14. References and Further Learning
- The Rust Programming Language Book (official tutorial).
- Rust by Example (hands-on examples).
- crates.io (Rust package registry).
- Rust Community Forum (ask questions!).
Rust’s learning curve is steep, but its safety, performance, and expressiveness make it a powerful tool for modern software engineering. Start small, experiment with cargo, and dive into projects to master its concepts! 🦀