Introduction to Rust & Setup
What is Rust?
Rust is a modern systems programming language focused on performance, memory safety, and thread safety without a garbage collector. It is designed to provide the control of a low-level language (like C or C++) while ensuring that the programmer cannot make common mistakes that lead to crashes or security vulnerabilities.
Created by Mozilla and now governed by the Rust Foundation, Rust introduces a unique concept called the Ownership System. This system allows the compiler to manage memory automatically and safely at compile-time, meaning your program doesn't need a "garbage collector" to pause execution and clean up memory.
Why Rust?
- Zero-cost Abstractions: You can use high-level programming features (like iterators and closures) without any performance penalty compared to writing it manually in a low-level way.
- Memory Safety: Rust prevents "Null Pointer" errors and "Buffer Overflows" by checking your code during compilation. If it compiles, it is memory-safe.
- Fearless Concurrency: Rust's type system prevents "Data Races" (where two threads try to change the same piece of data at once), making multi-threaded programming much safer.
- Growing Ecosystem: From high-performance web servers (Actix, Axum) to WebAssembly (WASM) and embedded systems, Rust is versatile.
Rust consistently ranks as one of the most loved programming languages in the Stack Overflow Developer Survey because it gives developers confidence that their code will run safely and efficiently.
Installation & Toolchain
The best way to install Rust is via rustupβthe official toolchain installer. It not only installs the compiler but also manages different versions of Rust and its essential tools.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh rustc --version cargo --version
After installation, restart your terminal. You can update Rust anytime using rustup update.
Windows users: The installer will guide you through setting up the Visual C++ Build Tools if needed. This is required because Rust relies on these tools to link your code into an executable.
Recommended IDE Setup
While you can use any text editor, the industry standard for Rust is Visual Studio Code with the following extension:
- rust-analyzer: This is essential. It provides autocomplete, type hints, and real-time error checking as you type.
Understanding Cargo
In Rust, you rarely call the compiler (rustc) directly. Instead, you use Cargo, Rust's all-in-one build system and package manager.
Cargo handles three main tasks:
- Building your code: Compiling your project and managing dependencies.
- Downloading Crates: "Crates" are Rust libraries. Cargo downloads them from
crates.ioautomatically. - Managing Projects: It organizes your folder structure and handles versioning.
Your First Rust Program
Every Rust program starts with a main() function β this is the entry point of your application.
fn main() { println!("Hello, Rust Mastery!"); println!("Welcome to safe systems programming!"); }
Welcome to safe systems programming!
Breaking Down the Code
fnkeyword: Used to declare a function. In Rust, functions are the primary way to group logic.println!: Notice the!. This tells Rust thatprintln!is a Macro, not a regular function. Macros are used when a function needs to do complex things, like varying the number of arguments it can take.Semicolons (;): Every statement in Rust must end with a semicolon. If you omit it, Rust might treat the line as a return value.Static Typing: Rust knows exactly what every piece of data is at compile time, which is why it's so fast.
π» Try It Yourself - Multi-Language Compiler
Practice Rust and many other programming languages right here in your browser! Switch between languages, modify the code, and click "Run" to see results instantly.
π‘ Practice Tips:
- Switch to Rust in the language selector and try the ownership examples
- Experiment with Rust's memory safety features and borrowing
- Try other systems languages like C++, Go, or C to compare safety features
- Use the "Load Example" button to see Rust-specific code samples
- Use Ctrl+Enter to quickly run your code
Create your first project using Cargo. Run cargo new hello_rust in your terminal, then modify src/main.rs to print your name and your goal for learning Rust. Run it using cargo run.
Experiment with build modes. Try using cargo run --release. Notice that it takes longer to compile, but the resulting binary is highly optimized for speed.
Pro Tip: Use cargo check frequently. It checks if your code will compile without actually building the binary, which is much faster and helps you catch errors quickly.
Variables & Ownership
Introduction to Variables
Variables are used to store data in memory. In Rust, variables are safe, predictable, and designed to prevent many common programming errors.
Rust focuses heavily on memory safety and performance, which is why variables and ownership are such important concepts in the language.
Immutability by Default
In Rust, variables are immutable by default. This means their values cannot be changed after they are created.
Immutability helps make programs safer and easier to understand because values remain predictable.
let x = 5; // x = 6; // Error! let mut y = 10; y = 11; // OK
In the example above:
xis immutable and cannot be changedyis mutable because it uses themutkeyword
Why Rust Uses Immutability
Rust encourages immutability because it reduces bugs and makes concurrent programming safer.
Immutable variables cannot accidentally change during program execution, making debugging easier.
Variable Declaration Syntax
Variables in Rust are declared using the let keyword.
let name = "Rust"; let age = 5; let is_fast = true;
Rust automatically infers the data type in many situations.
Explicit Type Annotation
You can explicitly define variable types when needed.
let score: i32 = 100; let price: f64 = 19.99;
Type annotations are useful when Rust cannot infer the type automatically.
Common Rust Data Types
| Type | Description | Example |
|---|---|---|
| i32 | 32-bit integer | 10 |
| f64 | 64-bit floating point | 3.14 |
| bool | Boolean value | true |
| char | Single Unicode character | 'A' |
| &str | String slice | "Hello" |
Mutable Variables
Mutable variables allow values to change during program execution.
let mut counter = 0; counter = counter + 1; println!("{}", counter);
Use mutable variables only when necessary to maintain code safety and clarity.
Shadowing Variables
Rust allows variables to be redefined using the same name. This feature is called shadowing.
let number = 5; let number = number + 1; println!("{}", number);
Unlike mutation, shadowing creates a completely new variable.
Difference Between Mutation and Shadowing
| Mutation | Shadowing |
|---|---|
Uses mut |
Uses another let |
| Changes the same variable | Creates a new variable |
| Cannot change type | Can change type |
Introduction to Ownership
Ownership is one of Rustβs most important concepts. It allows Rust to manage memory safely without needing a garbage collector.
Each value in Rust has a variable that's called its owner. When the owner goes out of scope, the value is dropped.
The Three Ownership Rules
- Each value in Rust has one owner
- There can only be one owner at a time
- When the owner goes out of scope, the value is automatically dropped
Ownership and Scope
Scope defines where a variable is valid.
{
let language = "Rust";
println!("{}", language);
}
// language no longer exists here
Once the block ends, Rust automatically cleans up the variable from memory.
Ownership Transfer
When assigning certain types to another variable, ownership moves to the new variable.
let s1 = String::from("hello"); let s2 = s1; // println!("{}", s1); // Error
After the move, s1 is no longer valid because ownership transferred to s2.
Cloning Data
If you want both variables to remain valid, use clone().
let s1 = String::from("Rust"); let s2 = s1.clone(); println!("{}", s1); println!("{}", s2);
The clone() method creates a deep copy of the data.
Copy Types
Simple types such as integers are copied automatically instead of moved.
let x = 5; let y = x; println!("{}", x); println!("{}", y);
Both variables remain valid because integers implement the Copy trait.
References and Borrowing
Rust allows borrowing data using references so ownership does not transfer.
fn print_text(text: &String) { println!("{}", text); } let message = String::from("Hello"); print_text(&message); println!("{}", message);
The ampersand & creates a reference to the value.
Benefits of Ownership
- Prevents memory leaks
- Prevents dangling pointers
- Improves memory safety
- Eliminates the need for garbage collection
Common Beginner Mistakes
- Trying to use variables after ownership moves
- Forgetting to use
mutfor mutable variables - Confusing references with copies
- Using values outside their scope
Practice Exercise
Create a mutable variable called count and increase its value.
Then create a String variable and experiment with ownership transfer and cloning.
let mut count = 1; count = count + 1; println!("{}", count); let name = String::from("Rust"); let copied_name = name.clone(); println!("{}", copied_name);
Summary
In this lecture, you learned:
- How Rust variables work
- The difference between mutable and immutable variables
- How ownership manages memory safely
- How values move and clone in Rust
- How borrowing and references work
Data Types & Functions
Basic (Scalar) Data Types
Go is a statically typed language, meaning every variable must have a defined type. Here are the most common basic types:
- Integers: Used for whole numbers. The most common is
int, but you can use specific sizes likeint8, int16, int32, int64(and their unsigned counterpartsuint). - Floating-point: Used for decimals. Go provides
float32andfloat64(the default for decimals). - Boolean: The
booltype represents eithertrueorfalse. - String: The
stringtype represents a sequence of characters, wrapped in double quotes "like this".
Type Inference
You don't always have to explicitly declare the type. Using the short declaration operator :=, Go infers the type automatically based on the value assigned.
var age int = 25 // Explicit declaration name := "Alice" // Inferred as string isReady := true // Inferred as bool
Compound Types
Compound types allow you to group multiple values together.
- Arrays: Fixed-size sequences of the same type.
var arr [5]int - Slices: Dynamic-size sequences. Slices are the most used "list" type in Go.
numbers := []int{1, 2, 3} - Maps: Key-value pairs (dictionaries).
emails := map[string]string{"John": "john@mail.com"} - Structs: Custom types that group different types of data together.
type User struct { Name string; Age int }
Functions
Functions are the building blocks of Go. They are declared using the func keyword.
Basic Function Syntax
func add(a int, b int) int { return a + b } // Usage: result := add(10, 20) // result is 30
Multiple Return Values
One of Go's most powerful features is the ability for a function to return more than one value. This is commonly used to return a result and an error simultaneously.
func divide(a, b float64) (float64, string) { if b == 0 { return 0, "Division by zero error!" } return a / b, "Success" } // Usage: val, msg := divide(10, 0) fmt.Println(msg) // Prints: Division by zero error!
Variadic Functions
A variadic function can take an unlimited number of arguments of a specific type using the ... syntax.
func sumAll(nums ...int) int { total := 0 for _, n := range nums { total += n } return total } // Usage: fmt.Println(sumAll(1, 2, 3, 4, 5)) // result: 15
Create a program that:
- Defines a
structcalledStudentwith fields forName(string) andGrade(float64). - Writes a function called
checkPassthat takes aStudentstruct and returns abool(true if grade >= 50, otherwise false). - Create a slice of 3 Students and use a loop to print whether each student passed or failed.
Control Flow
Introduction to Control Flow
Control flow determines how a program makes decisions and repeats actions. In Rust, control flow structures allow programs to react dynamically to different conditions.
The most common control flow tools are:
ifexpressionselsestatements- Loops
matchexpressions
If Expressions
Rust uses if expressions to execute code only when a condition is true.
let condition = true; let number = if condition { 5 } else { 6 };
In this example:
- If
conditionis true,numberbecomes 5 - Otherwise,
numberbecomes 6
If Expressions Return Values
Unlike many programming languages, Rustβs if statements are expressions. This means they can return values.
let age = 18; let status = if age >= 18 { "Adult" } else { "Minor" }; println!("{}", status);
Both branches must return the same type.
Boolean Conditions
Conditions inside an if statement must evaluate to a boolean value.
let number = 10; if number > 5 { println!("Number is greater than 5"); }
Rust does not automatically convert integers into booleans.
Using Else Statements
The else keyword defines code that executes when the condition is false.
let logged_in = false; if logged_in { println!("Welcome back!"); } else { println!("Please log in"); }
Else If Conditions
Use else if when checking multiple conditions.
let score = 85; if score >= 90 { println!("Grade A"); } else if score >= 75 { println!("Grade B"); } else { println!("Grade C"); }
Conditions are checked from top to bottom.
Infinite Loops
The loop keyword creates an infinite loop that continues until manually stopped.
loop { println!("Running forever"); }
Infinite loops are useful for game engines, servers, and continuous processes.
Breaking Out of Loops
Use the break keyword to stop a loop.
let mut counter = 0; loop { counter += 1; if counter == 5 { break; } } println!("Loop ended");
Returning Values from Loops
Rust loops can return values using break.
let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; println!("{}", result);
Output:
20
While Loops
The while loop repeats while a condition remains true.
let mut number = 3; while number != 0 { println!("{}", number); number -= 1; } println!("LIFTOFF!");
For Loops
The for loop is commonly used to iterate through collections or ranges.
for number in 1..5 { println!("{}", number); }
The range 1..5 includes 1 up to 4.
Inclusive Ranges
Use ..= to include the final value in the range.
for number in 1..=5 { println!("{}", number); }
This prints numbers from 1 to 5.
Looping Through Arrays
You can iterate through array elements using a for loop.
let fruits = ["Apple", "Banana", "Orange"]; for fruit in fruits { println!("{}", fruit); }
The Match Expression
The match expression is Rustβs powerful pattern matching system.
let number = 2; match number { 1 => println!("One"), 2 => println!("Two"), 3 => println!("Three"), _ => println!("Other"), }
The underscore _ acts as a default case.
Why Match is Powerful
The match statement is safer and more expressive than many traditional switch statements.
- Checks all possible cases
- Supports pattern matching
- Improves readability
- Works with enums and complex data types
Nested Control Flow
Control flow structures can be nested inside one another.
let age = 20; let has_ticket = true; if age >= 18 { if has_ticket { println!("Entry allowed"); } }
Common Beginner Mistakes
- Using non-boolean values as conditions
- Creating infinite loops accidentally
- Forgetting to update loop counters
- Using mismatched return types in
ifexpressions - Missing the default case in
match
Practice Exercise
Create a program that:
- Checks whether a number is even or odd
- Prints numbers from 1 to 10 using a loop
- Uses
matchto display the name of a weekday
let number = 8; if number % 2 == 0 { println!("Even"); } else { println!("Odd"); }
Summary
In this lecture, you learned:
- How Rust control flow works
- How to use
ifexpressions - How loops operate in Rust
- How to use
whileandforloops - How powerful
matchexpressions are
Loops & Iterators
Looping with 'for'
The for loop is the most idiomatic way to iterate in Rust. Instead of using a counter and indexing into a collection, Rust uses the IntoIterator trait, which allows the loop to safely traverse elements without risk of "out of bounds" errors.
for i in 1..=5 { println!("{}", i); }
Range Syntax: Rust uses ranges to create sequences of numbers efficiently.
1..5β 1, 2, 3, 4 (Exclusive: stops before 5)1..=5β 1, 2, 3, 4, 5 (Inclusive: includes 5)0..10β Common for indexing arrays from 0 to 9
// Advanced range operations for i in 0..10 { ... } // Standard 0 to 9 for i in (1..=10).step_by(2) { ... } // 1, 3, 5, 7, 9 for i in (1..100).rev() { ... } // Countdown from 99 to 1
Iterating over Arrays, Vectors & Strings
When iterating over collections, you must decide how you want to handle Ownership. There are three main ways to create an iterator:
iter(): Borrows each element immutably (&T). The original collection stays intact.iter_mut(): Borrows each element mutably (&mut T). Allows you to change values inside the loop.into_iter(): Consumes the collection and takes ownership of the elements (T). The original collection is destroyed.
let fruits = ["Apple", "Banana", "Mango"]; // 1. Basic iteration (borrowing) for fruit in fruits { println!("I like {}", fruit); } // 2. Iterating with the index using .enumerate() for (index, fruit) in fruits.iter().enumerate() { println!("Fruit # {}: {}", index, fruit); }
while Loop
The while loop is used when the number of iterations is not known in advance, and you want to continue as long as a specific condition remains true.
let mut number = 1; while number <= 5 { println!("Number: {}", number); number += 1; // Must manually increment to avoid infinite loop }
The Powerful 'loop' Keyword
loop creates an infinite loop. Unlike while, it doesn't check a condition at the start. It is often used for server listening loops or retry logic where you want the program to run until a specific break condition is met.
let mut counter = 0; loop { counter += 1; if counter > 10 { break; // Exit the loop immediately } if counter % 2 == 0 { continue; // Skip the rest of this iteration } println!("Counter: {}", counter); }
Returning Values from Loops
One of Rust's most unique features is that loop is an expression. This means you can return a value from a loop by passing it to the break keyword.
let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; // This value is assigned to 'result' } }; println!("Final Result: {}", result); // Result: 20
Iterators in Rust (Functional Approach)
Rustβs real power lies in its iterator system. Iterators are lazy, meaning they do nothing until you call a "consuming" method like collect() or a for loop.
Commonly used adapter methods:
map(): Transforms each element.filter(): Keeps only elements that match a condition.sum(): Adds all elements together.collect(): Transforms the iterator back into a collection (like aVec).
let numbers = vec![1, 2, 3, 4, 5]; // 1. Summing elements let sum: i32 = numbers.iter().sum(); // 2. Chaining methods: Filter and then Map let squared_evens: Vec= numbers.iter() .filter(|&x| x % 2 == 0) // Keep only even numbers .map(|x| x * x) // Square them .collect(); // Turn back into a Vector println!("Sum: {}", sum); println!("Squared Evens: {:?}", squared_evens);
Pro Tips:
- Prefer
forloops overwhilewhen possible β they are more idiomatic and prevent infinite loops caused by forgetting to increment a counter. - When in doubt, use
iter(). Only useinto_iter()if you specifically need to destroy the original collection to move its data. - Remember that
mapandfilterare lazy. If you don't callcollect()or a loop, the code inside them will never even run!
Ownership & Borrowing
Introduction to Ownership
Ownership is one of the most unique and important concepts in Rust. It is the system that allows Rust to manage memory safely without using a garbage collector.
Instead of automatically cleaning unused memory at random times, Rust follows strict ownership rules at compile time. These rules prevent memory leaks, dangling pointers, and data races.
Understanding ownership is essential because almost every Rust program depends on it.
The Three Ownership Rules
Rust ownership is based on three core rules:
- Each value in Rust has a variable called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped.
These rules make memory management automatic and safe.
Variable Scope
A variable is only valid inside the scope where it is declared.
fn main() { { let name = "Rust"; println!("{}", name); } // name is no longer valid here }
When the scope ends, Rust automatically frees the memory associated with the variable.
Ownership Transfer
Some values are moved instead of copied. After a move occurs, the original variable can no longer be used.
fn main() { let s1 = String::from("hello"); let s2 = s1; // println!("{}", s1); }
In this example, ownership of the string moves from s1 to s2. The variable s1 becomes invalid after the move.
Why Rust Uses Moves
Rust uses move semantics to prevent multiple variables from trying to free the same memory location.
This eliminates common bugs such as:
- Double free errors
- Dangling pointers
- Memory corruption
Copy Types
Simple primitive values are copied instead of moved because they are stored directly on the stack.
fn main() { let x = 10; let y = x; println!("{}", x); println!("{}", y); }
Integers, booleans, characters, and floating-point numbers usually implement the Copy trait.
Cloning Data
If you want to create a deep copy of heap data, you can use the clone() method.
fn main() { let s1 = String::from("Rust"); let s2 = s1.clone(); println!("{}", s1); println!("{}", s2); }
Unlike moves, cloning creates a completely separate copy of the data.
Ownership and Functions
Passing a variable to a function may transfer ownership depending on the type.
fn print_string(text: String) { println!("{}", text); } fn main() { let msg = String::from("Hello"); print_string(msg); // msg is no longer valid here }
The function takes ownership of the string parameter.
Returning Ownership
Functions can also return ownership back to the caller.
fn give_back(s: String) -> String { s }
This allows ownership to move safely between functions.
References & Borrowing
Instead of transferring ownership, you can "borrow" a value using references (&).
fn calculate_length(s: &String) -> usize { s.len() }
Borrowing allows a function to use a value without taking ownership of it.
This means the original variable remains valid after the function call.
fn main() { let text = String::from("Rust"); let length = calculate_length(&text); println!("Length: {}", length); println!("{}", text); }
Mutable References
By default, references are immutable. To modify borrowed data, you must use mutable references.
fn change(text: &mut String) { text.push_str(" language"); }
Mutable references allow safe modification while still preventing dangerous memory access.
fn main() { let mut name = String::from("Rust"); change(&mut name); println!("{}", name); }
Borrowing Rules
Rust enforces strict borrowing rules to guarantee memory safety.
- You can have multiple immutable references at the same time.
- You can only have one mutable reference at a time.
- You cannot combine mutable and immutable references simultaneously.
Multiple Immutable References
let s = String::from("hello"); let r1 = &s; let r2 = &s; println!("{}, {}", r1, r2);
This is allowed because immutable references only read data.
Single Mutable Reference
let mut s = String::from("hello"); let r1 = &mut s;
Rust prevents multiple mutable references because simultaneous modification could cause data races.
Dangling References
Rust prevents dangling references at compile time.
// This code will not compile fn dangle() -> &String { let s = String::from("hello"); &s }
The compiler stops this because the local variable would be destroyed after the function ends.
Slices and Borrowing
Slices are references to parts of collections without taking ownership.
let word = &text[0..4];
Slices are heavily used with strings and arrays.
Benefits of Ownership
- Memory safety without garbage collection.
- Prevention of data races.
- Better performance.
- Automatic resource cleanup.
- Safer concurrent programming.
Common Beginner Mistakes
- Trying to use variables after ownership has moved.
- Creating multiple mutable references.
- Confusing cloning with borrowing.
- Forgetting to use
mutfor mutable references. - Returning references to local variables.
Summary
Ownership and borrowing are the foundation of Rustβs memory safety model. Instead of relying on garbage collection, Rust uses compile-time rules to manage memory safely and efficiently.
Although ownership may feel difficult at first, mastering it allows developers to write high-performance and reliable systems software with confidence.
Structs & Enums
Introduction to Structs
Structs are custom data types that allow you to group related values together. They are similar to objects or classes in other programming languages, but Rust structs only store data.
Structs help organize code and make programs easier to understand and maintain.
Defining a Struct
A struct is created using the struct keyword.
struct User { username: String, email: String, active: bool, }
This struct stores information related to a user account.
Creating Struct Instances
You can create instances of a struct by providing values for each field.
let user1 = User { username: String::from("rustacean"), email: String::from("rust@example.com"), active: true, };
Each field must receive a value of the correct type.
Accessing Struct Fields
Struct fields are accessed using dot notation.
println!("{}", user1.username);
This prints the value stored inside the username field.
Mutable Structs
To modify struct fields, the struct instance must be mutable.
let mut user1 = User { username: String::from("rustacean"), email: String::from("rust@example.com"), active: true, }; user1.active = false;
Without the mut keyword, Rust will prevent modifications.
Struct Update Syntax
Rust provides a convenient way to create new struct instances using existing values.
let user2 = User { email: String::from("new@example.com"), ..user1 };
The ..user1 syntax copies remaining fields from another struct instance.
Tuple Structs
Tuple structs are structs without named fields.
struct Color(u8, u8, u8); let black = Color(0, 0, 0);
Tuple structs are useful when field names are unnecessary.
Unit-Like Structs
Rust also supports structs without fields.
struct Logger;
These are commonly used for traits or marker types.
Methods on Structs
Methods are defined using impl blocks.
struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } }
The &self parameter refers to the current struct instance.
Associated Functions
Associated functions belong to a struct but do not use self.
impl Rectangle { fn square(size: u32) -> Rectangle { Rectangle { width: size, height: size, } } }
These functions are often used as constructors.
Introduction to Enums
Enums allow a value to be one of several possible variants. They are extremely powerful and widely used in Rust.
enum Direction {
Up,
Down,
Left,
Right,
}
The variable can only hold one variant at a time.
Enums With Data
Enum variants can also store additional data.
enum Message { Text(String), Number(i32), Quit, }
This makes enums much more flexible than traditional enums in many other languages.
The Option Enum
Rust uses the Option enum to represent optional values safely.
let some_number = Some(5); let no_value: Option<i32> = None;
The Option type helps prevent null pointer errors.
Pattern Matching
Pattern matching allows you to check enum variants and execute different code depending on the result.
match some_option { Some(i) => println!("{}", i), None => println!("None"), }
The match statement is one of Rustβs most powerful control flow features.
Match Expressions
Every possible pattern must be handled in a match expression.
let number = 3; match number { 1 => println!("One"), 2 => println!("Two"), 3 => println!("Three"), _ => println!("Other"), }
The underscore _ acts as a default case.
The if let Syntax
When you only care about one pattern, if let provides a shorter alternative.
if let Some(x) = some_option { println!("{}", x); }
This is cleaner than writing a full match expression for simple cases.
The Result Enum
Rust also uses enums for error handling through the Result type.
enum Result<T, E> {
Ok(T),
Err(E),
}
This allows programs to safely handle operations that may fail.
Benefits of Structs and Enums
- Improve code organization.
- Create custom data models.
- Provide safer error handling.
- Reduce bugs caused by invalid states.
- Support powerful pattern matching.
Common Beginner Mistakes
- Forgetting to make structs mutable.
- Confusing tuple structs with tuples.
- Ignoring all enum variants in match statements.
- Trying to use null instead of
Option. - Moving ownership accidentally when updating structs.
Practice
Create a struct called Book with fields for title, author, and pages.
Then create an enum called Status with variants:
- Available
- Borrowed
- Reserved
Use a match statement to print different messages for each status.
Summary
Structs and enums are fundamental building blocks in Rust. Structs organize related data, while enums represent multiple possible states safely and efficiently.
Combined with pattern matching, these features allow Rust developers to write expressive, safe, and maintainable programs.
Collections & Strings
Introduction to Collections
Collections are data structures used to store multiple values together. Rust provides several powerful collection types that help developers manage and organize data efficiently.
Unlike arrays, many collections in Rust are dynamic, meaning they can grow or shrink during program execution.
The most commonly used collections include:
- Vectors
- Strings
- Hash Maps
These collections are stored on the heap, allowing flexible memory usage.
Vectors (Dynamic Arrays)
Vectors are dynamic arrays that can store multiple values of the same type. They are one of the most commonly used collections in Rust.
let mut v = vec![1, 2, 3]; v.push(4);
The vec! macro creates a vector containing initial values.
The push() method adds new elements to the end of the vector.
Creating Empty Vectors
You can also create an empty vector and add elements later.
let mut numbers: Vec<i32> = Vec::new(); numbers.push(10); numbers.push(20);
Because the vector starts empty, Rust requires an explicit type declaration.
Accessing Vector Elements
Vector elements can be accessed using indexing or the safer get() method.
let first = v[0]; match v.get(1) { Some(value) => println!("{}", value), None => println!("No value found"), }
Using get() is safer because it prevents runtime crashes caused by invalid indexes.
Iterating Through Vectors
You can loop through vector elements using a for loop.
for value in &v { println!("{}", value); }
The &v creates an immutable reference so ownership is not moved.
Modifying Vector Elements
Mutable references allow vector elements to be modified during iteration.
for value in &mut v { *value += 1; }
The * operator dereferences the mutable reference to access the actual value.
Vector Memory and Ownership
Vectors follow Rustβs ownership rules. When a vector is assigned to another variable, ownership moves unless the vector is cloned.
let v1 = vec![1, 2, 3]; let v2 = v1; // v1 is no longer valid
To create a full copy, use the clone() method.
let v2 = v1.clone();
Strings in Rust
Rust provides two main string types:
String&str(string slice)
A String is growable and heap-allocated, while &str is an immutable reference to string data.
Creating Strings
let name = String::from("Rust"); let language = "Programming";
The first variable creates an owned string, while the second is a string slice.
Appending to Strings
Strings can grow dynamically using methods such as push_str() and push().
let mut text = String::from("Hello"); text.push_str(" World"); text.push('!');
The final string becomes Hello World!.
String Concatenation
Strings can be combined using the + operator or the format! macro.
let s1 = String::from("Hello"); let s2 = String::from(" Rust"); let s3 = s1 + &s2;
The + operator moves ownership of the first string.
The format! macro is often cleaner and safer.
let message = format!("{} {}", s1, s2);
String Slices
String slices allow references to parts of a string without taking ownership.
let text = String::from("Rustacean"); let slice = &text[0..4]; println!("{}", slice);
This slice contains the value Rust.
UTF-8 and Strings
Rust strings are stored as UTF-8 encoded text. This allows support for international languages and Unicode characters.
Because UTF-8 characters may use multiple bytes, direct indexing into strings is not allowed.
// This is invalid in Rust // let ch = text[0];
Instead, Rust encourages safer approaches such as iterators.
Iterating Over Characters
for ch in text.chars() { println!("{}", ch); }
The chars() method iterates through Unicode characters safely.
Hash Maps
Hash maps store data as key-value pairs.
use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert("Alice", 90); scores.insert("Bob", 85);
Hash maps are useful for fast lookups and storing related data.
Accessing Hash Map Values
match scores.get("Alice") { Some(score) => println!("{}", score), None => println!("No score found"), }
The get() method returns an Option because the key may not exist.
Ownership and Collections
Collections often take ownership of inserted values.
let name = String::from("Rust"); scores.insert(name, 100); // name is no longer valid
This behavior prevents invalid memory access and ensures safety.
Benefits of Rust Collections
- Memory safety
- High performance
- Flexible dynamic storage
- Powerful ownership tracking
- Efficient data management
Common Beginner Mistakes
- Trying to index strings directly.
- Forgetting ownership transfer in collections.
- Using indexing without checking bounds.
- Confusing
Stringand&str. - Mutating immutable vectors.
Practice
Create a vector that stores five numbers and write a loop that prints only even numbers.
Then create a string and append additional text using push_str().
Summary
Collections and strings are essential building blocks in Rust. Vectors provide dynamic storage, strings allow safe UTF-8 text handling, and hash maps organize key-value data efficiently.
Mastering these collections is important for building real-world Rust applications, APIs, command-line tools, and systems software.
Error Handling
Introduction to Error Handling
Error handling is an essential part of writing reliable and safe programs. Rust provides a powerful system that helps developers handle errors explicitly instead of ignoring them.
Unlike many languages that rely heavily on exceptions,
Rust encourages developers to handle failures using types such as
Result<T, E> and Option<T>.
- Improves application stability
- Prevents unexpected crashes
- Encourages safer programming practices
- Makes failures more predictable
Panic vs Result
Rust groups errors into two categories:
- Recoverable errors using
Result<T, E> - Unrecoverable errors using
panic!
Recoverable errors are situations the program can handle gracefully, while unrecoverable errors indicate serious bugs or invalid states.
Understanding panic!
The panic! macro immediately stops program execution
when something goes critically wrong.
fn main() { panic!("Something went wrong!"); }
When a panic occurs, Rust prints an error message and stack trace.
Common Causes of Panic
- Accessing invalid array indexes
- Calling
unwrap()on an error value - Invalid assumptions in the program
- Unexpected runtime states
Recoverable Errors with Result
The Result enum represents operations that may succeed or fail.
enum Result<T, E> {
Ok(T),
Err(E),
}
Ok contains a successful value,
while Err contains error information.
Reading Files with Result
Many standard library functions return a Result.
use std::fs::File; fn main() { let file = File::open("data.txt"); match file { Ok(f) => println!("File opened!"), Err(error) => println!("Error: {:?}", error), } }
Pattern Matching with Result
Rust commonly uses match expressions
to handle different outcomes.
let result: Result<int, string> = Ok(10); match result { Ok(value) => println!("Value: {}", value), Err(msg) => println!("Error: {}", msg), }
The unwrap() Method
The unwrap() method extracts the successful value from a Result.
let file = File::open("notes.txt").unwrap();
If the operation fails, unwrap() causes a panic.
The expect() Method
The expect() method works like unwrap()
but allows a custom error message.
let file = File::open("config.txt") .expect("Failed to open config file");
Propagating Errors
Instead of handling errors immediately, functions can return them to the caller.
use std::fs::File; use std::io::Error; fn open_file() -> Result<File, Error> { let file = File::open("data.txt")?; Ok(file) }
The ? Operator
The ? operator simplifies error propagation.
If the operation succeeds, the value is returned. If it fails, the error is automatically returned from the function.
fn divide(a: f64, b: f64) -> Result<f64, string> { if b == 0.0 { return Err("Cannot divide by zero".to_string()); } Ok(a / b) }
Option vs Result
Rust also provides the Option<T> enum
for values that may or may not exist.
| Type | Purpose |
|---|---|
| Option<T> | Represents missing values |
| Result<T, E> | Represents recoverable errors |
Using Option
fn find_number(numbers: Vec<i32>, target: i32) -> Option<usize> { for (index, value) in numbers.iter().enumerate() { if *value == target { return Some(index); } } None }
Handling Multiple Error Types
Large applications may encounter different kinds of errors.
use std::num::ParseIntError; fn parse_number(input: &str) -> Result<i32, ParseIntError> { input.parse::<i32>() }
Creating Custom Errors
Developers can define custom error types using enums.
enum AppError {
NotFound,
InvalidInput,
}
Error Handling Best Practices
- Use
Resultfor recoverable errors - Avoid unnecessary panics
- Use descriptive error messages
- Prefer the
?operator for cleaner code - Handle errors close to where they occur
Common Mistakes
- Overusing
unwrap() - Ignoring error values
- Using panic for recoverable situations
- Returning unclear error messages
- Not propagating errors properly
Mini Practice
Try creating the following:
- A function returning
Result<T, E> - A file-reading example with
match - A custom error enum
- A function using the
?operator - An
Optionexample returningSomeorNone
Modules & Crates
Cargo is Rust's build system and package manager. Use Cargo.toml to manage dependencies.
Traits & Generics
Defining Shared Behavior
pub trait Summary { fn summarize(&self) -> String; }
Capstone Project: Secure CLI
Create a Command Line Interface (CLI) tool that securely manages passwords or encrypts files using crates like clap and rust-crypto.
// Project goals: // 1. Parse CLI arguments // 2. Handle file I/O safely // 3. Implement robust error handling // 4. Use traits for polymorphic output
Lifetimes
Content coming soon...
Smart Pointers (Box, Rc, RefCell)
Content coming soon...
Concurrency (Threads & Channels)
Content coming soon...
Closures & Advanced Iterators
Content coming soon...
Async Rust (Tokio)
Content coming soon...
Final Project β Rust Web Service
Content coming soon...