Welcome to Rust
Rust is a modern systems programming language that focuses on performance, reliability, and productivity. Developed by Mozilla Research, Rust provides memory safety without garbage collection through its innovative ownership system.
Known for its "fearless concurrency" and zero-cost abstractions, Rust enables developers to write efficient, safe code while preventing common programming errors like null pointer dereferences, buffer overflows, and data races. This makes Rust ideal for systems programming, web assembly, embedded systems, and performance-critical applications.
Introduction to Rust
How to set up a Rust development environment and write your first "Hello, World!" program.
Rust is designed to be safe, concurrent, and practical. Setting up a Rust development environment is straightforward with the official Rust toolchain.
Step 1: Install Rust
-
All Platforms
Use rustup, the Rust toolchain installer. Open your terminal and run:curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shOr on Windows, download and run the rustup-init.exe from rustup.rs.
Step 2: Verify Installation
Open a terminal and type:
rustc --version
This should display the installed Rust compiler version.
Step 3: Create and Run Your First Rust Program
-
Create a new Rust project using Cargo:
cargo new hello_world cd hello_world -
Examine the generated
src/main.rsfile:fn main() { println!("Hello, world!"); } -
Run the program:
cargo runYou should see the output:
Hello, world!
Congratulations! You have successfully set up a Rust environment and run your first program. 🎉
Rust Syntax Basics
Rust syntax is designed to be expressive and safe. Understanding the basic syntax is crucial for writing correct Rust programs.
1. Basic Structure of a Rust Program
Every Rust program has a specific structure:
fn main() { // Main function - program entry point
// Program statements go here
println!("Hello, Rust!");
}
2. Semicolons and Blocks
Rust uses semicolons to terminate statements and braces {} to define code blocks:
let x = 5; // Statement ends with semicolon
if x > 3 { // Braces define the if block
println!("x is greater than 3");
} // Closing brace
3. Comments
Rust supports single-line and documentation comments:
// This is a single-line comment
/// This is a documentation comment
/// It supports Markdown and is used by rustdoc
fn my_function() {
/*
This is a multi-line comment
It can span multiple lines
*/
}
4. Case Sensitivity
Rust is case-sensitive, meaning it distinguishes between uppercase and lowercase letters:
let my_var = 5; // Different from myvar
let MyVar = 10; // Different from myVar
println!("{}", my_var); // Outputs: 5
println!("{}", MyVar); // Outputs: 10
5. Variables and Type Inference
Rust has strong, static typing with excellent type inference:
let x = 10; // Compiler infers i32
let y = 3.14; // Compiler infers f64
let z = 'A'; // Character
let name = "Rust"; // String slice &str
// Explicit type annotation
let a: i32 = 10;
let b: f64 = 3.14;
let c: char = 'A';
let d: &str = "Rust";
Conclusion
Understanding Rust syntax is essential for writing correct and efficient programs. Key takeaways include:
- Rust programs start execution from the
main()function - Statements end with semicolons
- Code blocks are defined with braces
{} - Rust is case-sensitive
- Rust has strong static typing with excellent type inference
Output with println!
The println! macro in Rust is used to display output on the screen. It's one of the most commonly used features for basic output and debugging.
1. Basic println! Usage
The simplest way to use println! is with string literals:
fn main() {
println!("Hello, World!"); // Outputs: Hello, World!
println!("{}", 42); // Outputs: 42
}
2. Formatting with Placeholders
You can use placeholders to format output:
println!("Hello {}!", "Rust"); // Outputs: Hello Rust!
println!("{} + {} = {}", 5, 3, 8); // Outputs: 5 + 3 = 8
3. Positional and Named Arguments
Rust supports positional and named arguments in formatting:
// Positional arguments
println!("{0} {1} {0}", "foo", "bar"); // Outputs: foo bar foo
// Named arguments
println!("{name} is {age} years old", name = "Alice", age = 25);
4. Formatting Specifiers
Rust provides various formatting specifiers for different data types:
let number = 42;
let float = 3.14159;
println!("Decimal: {}", number); // 42
println!("Binary: {:b}", number); // 101010
println!("Hex: {:x}", number); // 2a
println!("Octal: {:o}", number); // 52
println!("Float: {:.2}", float); // 3.14
println!("Scientific: {:e}", float); // 3.14159e0
5. Printing Variables and Expressions
You can print variables and expressions:
let name = "Alice";
let age = 25;
println!("Name: {}, Age: {}", name, age);
// Expressions work too
println!("5 + 3 = {}", 5 + 3);
6. Debug and Display Traits
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
let point = Point { x: 10, y: 20 };
// Debug formatting
println!("{:?}", point); // Outputs: Point { x: 10, y: 20 }
println!("{:#?}", point); // Pretty-print:
// Point {
// x: 10,
// y: 20,
// }
Conclusion
The println! macro is a fundamental tool for output in Rust. Key points:
- Use
{}as placeholders for values - Supports positional and named arguments
- Rich formatting options for different data types
- Can print debug information with
{:?}
Arithmetic Operators in Rust
Rust provides standard arithmetic operators for mathematical calculations. These operators work with numeric data types like integers and floating-point numbers.
Basic Arithmetic Operators
fn main() {
let a = 15;
let b = 4;
println!("a + b = {}", a + b); // Addition: 19
println!("a - b = {}", a - b); // Subtraction: 11
println!("a * b = {}", a * b); // Multiplication: 60
println!("a / b = {}", a / b); // Division: 3 (integer division)
println!("a % b = {}", a % b); // Modulus: 3
let x = 15.0;
let y = 4.0;
println!("x / y = {}", x / y); // Division: 3.75 (float division)
}
Type Considerations
// Rust requires explicit type conversion
let a: i32 = 10;
let b: f64 = 3.14;
// This would cause a compile error:
// let result = a + b;
// Correct approach with explicit conversion
let result = a as f64 + b;
println!("Result: {}", result); // 13.14
Overflow Handling
let max_u8 = 255u8;
// This would panic in debug mode or wrap in release mode
// let overflow = max_u8 + 1;
// Safe arithmetic operations
let checked = max_u8.checked_add(1); // Returns None
let wrapping = max_u8.wrapping_add(1); // Returns 0
let saturating = max_u8.saturating_add(1); // Returns 255
println!("Checked: {:?}", checked); // None
println!("Wrapping: {}", wrapping); // 0
println!("Saturating: {}", saturating); // 255
Common Pitfalls
- Integer division truncates the fractional part:
5 / 2equals2, not2.5 - Mixing different numeric types requires explicit conversion
- Division by zero causes a panic at runtime
- Integer overflow behavior depends on compilation mode
Comparison Operators in Rust
Comparison operators compare two values and return a boolean result (true or false). These are essential for conditional statements and loops.
Comparison Operators
fn main() {
let x = 7;
let y = 10;
println!("x == y: {}", x == y); // Equal to: false
println!("x != y: {}", x != y); // Not equal to: true
println!("x > y: {}", x > y); // Greater than: false
println!("x < y: {}", x < y); // Less than: true
println!("x >= 7: {}", x >= 7); // Greater than or equal: true
println!("y <= 7: {}", y <= 7); // Less than or equal: false
// Comparing different types requires conversion
let int_val: i32 = 5;
let float_val: f64 = 5.0;
// This works because we convert to the same type
println!("Equal: {}", int_val as f64 == float_val); // true
}
Using Comparisons in Conditions
let age = 18;
if age >= 18 {
println!("You are an adult");
} else {
println!("You are a minor");
}
// Pattern matching with comparisons
match age {
0..=17 => println!("Minor"),
18..=64 => println!("Adult"),
_ => println!("Senior"),
}
Comparing Custom Types
#[derive(PartialEq, PartialOrd)]
struct Point {
x: i32,
y: i32,
}
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
// Now we can compare Point instances
println!("p1 == p2: {}", p1 == p2); // false
// println!("p1 < p2: {}", p1 < p2); // This would need FullOrd derivation
Common Pitfalls
- Comparing different numeric types requires explicit conversion
- Custom types need to derive comparison traits to use operators
- Floating-point comparisons for exact equality can be problematic due to precision issues
- NaN values always compare as not equal, even to themselves
Logical Operators in Rust
Logical operators combine boolean expressions and return true or false. Rust provides three logical operators: && (AND), || (OR), and ! (NOT).
Logical Operators
fn main() {
let is_sunny = true;
let is_warm = false;
println!("is_sunny && is_warm: {}", is_sunny && is_warm); // AND: false
println!("is_sunny || is_warm: {}", is_sunny || is_warm); // OR: true
println!("!is_warm: {}", !is_warm); // NOT: true
// Complex conditions
let age = 25;
let has_license = true;
if age >= 18 && has_license {
println!("You can drive");
}
}
Short-Circuit Evaluation
// In AND (&&), if first condition is false, second isn't evaluated
// In OR (||), if first condition is true, second isn't evaluated
fn expensive_check() -> bool {
println!("Expensive check performed!");
true
}
let x = false;
if x && expensive_check() { // expensive_check() not called
println!("This won't execute");
}
let y = true;
if y || expensive_check() { // expensive_check() not called
println!("This will execute without calling expensive_check");
}
Boolean Operations on Other Types
// Option and Result types have boolean-like operations
let some_value: Option<i32> = Some(5);
let none_value: Option<i32> = None;
// Using and_then, or_else for chaining operations
let result = some_value.and_then(|x| Some(x * 2)); // Some(10)
let result2 = none_value.and_then(|x| Some(x * 2)); // None
println!("Result: {:?}", result);
println!("Result2: {:?}", result2);
Common Pitfalls
- Using bitwise operators (
&,|) instead of logical operators (&&,||) - Forgetting that logical operators only work with boolean types
- Not leveraging short-circuit evaluation for performance
Bitwise Operators in Rust
Bitwise operators perform operations on individual bits of integer types. They are used in low-level programming, embedded systems, and performance optimization.
Bitwise Operators
fn main() {
let a: u8 = 0b0110; // binary: 0110 (6)
let b: u8 = 0b0011; // binary: 0011 (3)
println!("a = {:08b} ({})", a, a);
println!("b = {:08b} ({})", b, b);
println!("a & b = {:08b} ({})", a & b, a & b); // AND: 0010 (2)
println!("a | b = {:08b} ({})", a | b, a | b); // OR: 0111 (7)
println!("a ^ b = {:08b} ({})", a ^ b, a ^ b); // XOR: 0101 (5)
println!("!a = {:08b} ({})", !a, !a); // NOT: 11111001 (249)
println!("a << 1 = {:08b} ({})", a << 1, a << 1); // Left shift: 1100 (12)
println!("b >> 1 = {:08b} ({})", b >> 1, b >> 1); // Right shift: 0001 (1)
}
Practical Applications
// Setting and checking flags
const FLAG_A: u8 = 1 << 0; // 0001
const FLAG_B: u8 = 1 << 1; // 0010
const FLAG_C: u8 = 1 << 2; // 0100
let mut settings = 0;
settings |= FLAG_A; // Set flag A
settings |= FLAG_C; // Set flag C
// Check if flag B is set
if settings & FLAG_B != 0 {
println!("Flag B is set");
} else {
println!("Flag B is not set");
}
// Count set bits (population count)
let value: u32 = 0b10101100;
let count = value.count_ones();
println!("Number of set bits in {}: {}", value, count);
Bit Manipulation Methods
let num: u8 = 0b10101010;
// Various bit manipulation methods
println!("Leading zeros: {}", num.leading_zeros());
println!("Trailing zeros: {}", num.trailing_zeros());
println!("Count ones: {}", num.count_ones());
println!("Rotate left: {:08b}", num.rotate_left(2));
println!("Swap bytes: {:08b}", num.swap_bytes());
// Check power of two
let power_of_two = 16u32;
println!("{} is power of two: {}", power_of_two, power_of_two.is_power_of_two());
Common Pitfalls
- Confusing bitwise AND
&with logical AND&& - Shift operations on signed integers maintain sign (arithmetic shift)
- Shifting beyond the bit width causes panic in debug mode
- Forgetting that bitwise NOT on signed integers also flips the sign bit
Assignment Operators in Rust
Assignment operators assign values to variables. Rust provides compound assignment operators that combine assignment with arithmetic or bitwise operations.
Assignment Operators
fn main() {
let mut x = 10; // Simple assignment
println!("x = {}", x);
x += 5; // Equivalent to x = x + 5
println!("x += 5: {}", x); // 15
x -= 3; // Equivalent to x = x - 3
println!("x -= 3: {}", x); // 12
x *= 2; // Equivalent to x = x * 2
println!("x *= 2: {}", x); // 24
x /= 4; // Equivalent to x = x / 4
println!("x /= 4: {}", x); // 6
x %= 4; // Equivalent to x = x % 4
println!("x %= 4: {}", x); // 2
// Bitwise assignment operators
let mut y: u8 = 5;
y &= 3; // AND assignment: y = y & 3
y |= 8; // OR assignment: y = y | 8
y ^= 4; // XOR assignment: y = y ^ 4
println!("Final y: {}", y);
}
Multiple Assignment and Destructuring
// Multiple assignment (destructuring)
let (a, b, c) = (10, 20, 30);
println!("a={}, b={}, c={}", a, b, c);
// Swap values
let mut x = 5;
let mut y = 10;
println!("Before: x={}, y={}", x, y);
std::mem::swap(&mut x, &mut y);
println!("After: x={}, y={}", x, y);
// Compound assignment in loops
let mut sum = 0;
for i in 1..=5 {
sum += i; // Add i to sum
}
println!("Sum: {}", sum); // 15
Assignment with Different Types
// Type conversion during assignment
let int_val: i32 = 42;
let float_val: f64 = int_val as f64; // Explicit conversion
// Assignment from function returns
let result = {
let x = 5;
let y = 10;
x + y // No semicolon - this expression is returned
};
println!("Result: {}", result); // 15
// Assignment with pattern matching
let (success, value) = if true {
(true, 42)
} else {
(false, 0)
};
println!("Success: {}, Value: {}", success, value);
Common Pitfalls
- Forgetting
mutkeyword when variable needs to be mutable - Trying to assign to immutable variables
- Mixing types in compound assignments without explicit conversion
- Shadowing variables instead of mutating them
Integer Data Types in Rust
Rust provides several integer types with explicit sizes and signed/unsigned variants. Choosing the right integer type depends on the required range and memory constraints.
Basic Integer Types
fn main() {
// Signed integers (can represent negative numbers)
let i8_val: i8 = 100; // 8-bit signed (-128 to 127)
let i16_val: i16 = 1000; // 16-bit signed
let i32_val: i32 = 100000; // 32-bit signed (default)
let i64_val: i64 = 100000; // 64-bit signed
let i128_val: i128 = 100000; // 128-bit signed
let isize_val: isize = 100000; // Architecture-dependent signed
// Unsigned integers (only non-negative numbers)
let u8_val: u8 = 100; // 8-bit unsigned (0 to 255)
let u16_val: u16 = 1000; // 16-bit unsigned
let u32_val: u32 = 100000; // 32-bit unsigned
let u64_val: u64 = 100000; // 64-bit unsigned
let u128_val: u128 = 100000; // 128-bit unsigned
let usize_val: usize = 100000; // Architecture-dependent unsigned
// Type inference
let inferred = 42; // Compiler infers i32
let explicit: u64 = 42; // Explicit type
println!("i8 range: {} to {}", i8::MIN, i8::MAX);
println!("u8 range: {} to {}", u8::MIN, u8::MAX);
println!("usize size: {} bytes", std::mem::size_of::<usize>());
}
Integer Operations
let a = 10;
let b = 3;
println!("a + b = {}", a + b); // 13
println!("a - b = {}", a - b); // 7
println!("a * b = {}", a * b); // 30
println!("a / b = {}", a / b); // 3 (integer division)
println!("a % b = {}", a % b); // 1 (modulus)
// Checked operations (avoid panics)
match a.checked_add(b) {
Some(result) => println!("Safe addition: {}", result),
None => println!("Addition would overflow"),
}
// Wrapping operations
let wrapped = a.wrapping_add(b);
println!("Wrapped addition: {}", wrapped);
Type Conversion and Casting
// Safe casting between integer types
let small: u8 = 255;
let large: u16 = small as u16; // 255
// This would lose data but is allowed
let large_num: i32 = 300;
let small_num: u8 = large_num as u8; // 44 (300 - 256)
// String to integer
let str_val = "123";
let int_val: i32 = str_val.parse().unwrap(); // 123
let int_val2: i32 = "123".parse().expect("Not a number!");
// Different parsing approaches
let num1 = "42".parse::<i32>(); // Ok(42)
let num2 = "42abc".parse::<i32>(); // Err
Common Pitfalls
- Integer overflow panics in debug mode, wraps in release mode
- Integer division truncates fractional parts
- Casting between types can silently lose data
- Parsing strings can fail - always handle Result types
Floating-Point Data Types in Rust
Floating-point types represent real numbers with fractional parts. Rust provides two floating-point types with different precision levels.
Floating-Point Types
fn main() {
let f32_val: f32 = 3.14159; // Single precision (32 bits)
let f64_val: f64 = 3.14159265358979; // Double precision (64 bits, default)
// Type inference
let inferred_float = 3.14; // Compiler infers f64
println!("f32: {:.10}", f32_val);
println!("f64: {:.10}", f64_val);
// Special values
println!("sqrt(-1): {:?}", (-1.0f64).sqrt()); // NaN (Not a Number)
println!("1.0/0.0: {:?}", 1.0 / 0.0); // inf (Infinity)
println!("-1.0/0.0: {:?}", -1.0 / 0.0); // -inf
// Checking special values
let nan = f64::NAN;
println!("Is NaN: {}", nan.is_nan());
println!("Is finite: {}", f64_val.is_finite());
println!("Is infinite: {}", f64::INFINITY.is_infinite());
}
Floating-Point Operations
let a = 2.5;
let b = 1.5;
println!("a + b = {}", a + b); // 4.0
println!("a - b = {}", a - b); // 1.0
println!("a * b = {}", a * b); // 3.75
println!("a / b = {}", a / b); // 1.6666666666666667
// Mathematical functions
println!("sqrt(a) = {}", a.sqrt());
println!("powf(a, b) = {}", a.powf(b));
println!("sin(pi/2) = {}", (std::f64::consts::PI / 2.0).sin());
// Rounding operations
let num = 3.75;
println!("floor: {}", num.floor()); // 3.0
println!("ceil: {}", num.ceil()); // 4.0
println!("round: {}", num.round()); // 4.0
println!("trunc: {}", num.trunc()); // 3.0
println!("fract: {}", num.fract()); // 0.75
Type Conversion and Precision
// String to floating-point
let str_val = "3.14159";
let float_val: f64 = str_val.parse().unwrap();
// Floating-point to integer (truncation)
let f = 3.99;
let i = f as i32; // i becomes 3
// Integer to floating-point
let x = 5;
let y = x as f64; // y becomes 5.0
// Be careful with precision
let precise: f64 = 0.1 + 0.2;
println!("0.1 + 0.2 = {}", precise); // 0.30000000000000004
println!("Equal to 0.3: {}", precise == 0.3); // false
// Better approach for comparisons
let tolerance = 1e-10;
println!("Close to 0.3: {}", (precise - 0.3).abs() < tolerance); // true
Common Pitfalls
- Floating-point numbers have limited precision and rounding errors
- Comparing floating-point numbers for exact equality is often problematic
- Operations with very large or very small numbers can cause overflow/underflow
- NaN values propagate through calculations and break comparisons
Strings in Rust
Rust has two main string types: String (owned, growable) and &str (string slice, borrowed). Understanding both is crucial for effective Rust programming.
Creating and Using Strings
fn main() {
// Different ways to create strings
let s1 = String::from("Hello");
let s2 = "World".to_string();
let s3 = String::new();
let s4 = "String slice"; // This is &str, not String
// Concatenation
let s5 = s1 + " " + &s2; // Note: s1 is moved here
println!("s5: {}", s5);
// Format macro for complex concatenation
let s6 = format!("{} {}!", "Hello", "Rust");
println!("s6: {}", s6);
// String slices (&str)
let slice = "Hello World";
let hello = &slice[0..5]; // "Hello"
let world = &slice[6..11]; // "World"
println!("hello: {}, world: {}", hello, world);
// String length and capacity
let mut text = String::from("Hello");
println!("Length: {}", text.len()); // 5
println!("Capacity: {}", text.capacity()); // 5
println!("Is empty: {}", text.is_empty()); // false
text.push_str(" Rust!");
println!("After push_str: {}", text); // Hello Rust!
}
String Comparison and Searching
let str1 = "apple";
let str2 = "banana";
// Comparison
if str1 == str2 {
println!("Strings are equal");
} else if str1 < str2 {
println!("{} comes before {}", str1, str2);
} else {
println!("{} comes after {}", str1, str2);
}
// Searching
let text = "Hello World";
if let Some(pos) = text.find("World") {
println!("Found 'World' at position {}", pos);
}
// Pattern matching with contains
if text.contains("Hello") {
println!("Text contains 'Hello'");
}
// Starts with / ends with
println!("Starts with 'Hello': {}", text.starts_with("Hello"));
println!("Ends with 'World': {}", text.ends_with("World"));
String Modification
let mut text = String::from("Hello");
// Append characters and strings
text.push('!'); // Add single character
text.push_str(" Rust"); // Add string slice
// Insert at position
text.insert(5, ','); // Insert character at index 5
text.insert_str(6, " dear"); // Insert string at index 6
// Replace
let replaced = text.replace("dear", "wonderful");
// Remove characters
let mut remove_example = String::from("Hello");
let removed_char = remove_example.remove(4); // Removes 'o' at index 4
remove_example.pop(); // Remove last character
println!("Modified: {}", text);
println!("Replaced: {}", replaced);
println!("After removal: {}", remove_example);
Iterating Over Strings
let text = "Hello 世界"; // Mix of ASCII and non-ASCII
// Iterate by characters (Unicode scalar values)
println!("Characters:");
for c in text.chars() {
print!("{} ", c);
}
println!();
// Iterate by bytes
println!("Bytes:");
for b in text.bytes() {
print!("{} ", b);
}
println!();
// Iterate with indices
for (i, c) in text.char_indices() {
println!("Character at {}: {}", i, c);
}
Common Pitfalls
- Indexing strings with
[]is not allowed - use slicing with caution - String slices must occur on valid UTF-8 boundaries
- Confusing
String(owned) with&str(borrowed) - Forgetting that some string operations move ownership
String Functions in Rust
Rust's String type provides many useful methods for string manipulation, including trimming, case conversion, splitting, and pattern matching.
String Manipulation Methods
fn main() {
let text = " Hello World ";
// Trimming
let trimmed = text.trim();
println!("Trimmed: '{}'", trimmed); // 'Hello World'
let left_trimmed = text.trim_start();
let right_trimmed = text.trim_end();
println!("Left trimmed: '{}'", left_trimmed);
println!("Right trimmed: '{}'", right_trimmed);
// Case conversion
let mixed = "Hello World";
let uppercase = mixed.to_uppercase();
let lowercase = mixed.to_lowercase();
println!("Uppercase: {}", uppercase); // HELLO WORLD
println!("Lowercase: {}", lowercase); // hello world
// Repeat strings
let repeated = "abc".repeat(3);
println!("Repeated: {}", repeated); // abcabcabc
}
String Splitting and Joining
// Splitting a string
let data = "apple,banana,cherry";
let fruits: Vec<&str> = data.split(',').collect();
println!("Fruits: {:?}", fruits); // ["apple", "banana", "cherry"]
// Splitting with multiple delimiters
let text = "apple, banana; cherry";
let items: Vec<&str> = text.split([',', ';']).map(|s| s.trim()).collect();
println!("Items: {:?}", items); // ["apple", "banana", "cherry"]
// Splitting into lines
let multiline = "Line 1\nLine 2\nLine 3";
for line in multiline.lines() {
println!("Line: {}", line);
}
// Joining strings
let joined = fruits.join("-");
println!("Joined: {}", joined); // apple-banana-cherry
// Using collect to join
let collected: String = fruits.into_iter().collect();
println!("Collected: {}", collected); // applebananacherry
Pattern Matching with Strings
let text = "The quick brown fox";
// Using patterns with string methods
let words: Vec<&str> = text.split_whitespace().collect();
println!("Words: {:?}", words);
// Matching with patterns
match text {
"hello" => println!("It's hello!"),
s if s.starts_with("The") => println!("Starts with 'The'"),
s if s.contains("fox") => println!("Contains 'fox'"),
_ => println!("Something else"),
}
// Using matches! macro
if text.matches("o").count() == 3 {
println!("Text contains exactly three 'o' characters");
}
String Validation and Inspection
let text = "Hello123";
// Checking string properties
println!("Is ASCII: {}", text.is_ascii());
println!("Is empty: {}", text.is_empty());
println!("Length: {}", text.len());
println!("Number of chars: {}", text.chars().count());
// Checking character categories
println!("All alphabetic: {}", text.chars().all(|c| c.is_alphabetic()));
println!("Any digit: {}", text.chars().any(|c| c.is_numeric()));
// Finding positions
if let Some(pos) = text.find('1') {
println!("First digit at position: {}", pos);
}
// RFind (reverse find)
if let Some(pos) = text.rfind('l') {
println!("Last 'l' at position: {}", pos);
}
Common Pitfalls
- String indexing is not allowed - use
chars()or pattern matching - Slicing can panic if not on character boundaries
split()returns an iterator, need tocollect()to get Vec- Some operations consume the string, others work on references
String Formatting in Rust
Rust provides powerful string formatting through the format! macro and related formatting traits. This allows for flexible and type-safe string creation.
Basic Formatting with format!
fn main() {
let name = "Alice";
let age = 25;
let score = 95.5;
// Basic formatting
let message = format!("Name: {}, Age: {}, Score: {}", name, age, score);
println!("{}", message);
// Positional arguments
let pos_message = format!("{0} {1} {0}", "foo", "bar");
println!("{}", pos_message); // foo bar foo
// Named arguments
let named_message = format!("{name} is {age} years old", name = "Bob", age = 30);
println!("{}", named_message);
}
Formatting Specifiers
let number = 42;
let float = 3.14159;
// Number formatting
println!("Decimal: {}", number); // 42
println!("Binary: {:b}", number); // 101010
println!("Hex: {:x}", number); // 2a
println!("Hex uppercase: {:X}", number); // 2A
println!("Octal: {:o}", number); // 52
println!("Pointer: {:p}", &number); // memory address
// Float formatting
println!("Float: {}", float); // 3.14159
println!("Float 2 decimal: {:.2}", float); // 3.14
println!("Scientific: {:e}", float); // 3.14159e0
println!("Scientific upper: {:E}", float); // 3.14159E0
// Padding and alignment
println!("Right aligned: {:>10}", number); // " 42"
println!("Left aligned: {:<10}", number); // "42 "
println!("Center aligned: {:^10}", number); // " 42 "
println!("Zero padded: {:010}", number); // "0000000042"
Custom Formatting with Traits
use std::fmt;
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
// Implement Display trait for custom formatting
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let point = Point { x: 10, y: 20 };
// Using Display trait (user-facing)
println!("Point: {}", point); // Point: (10, 20)
// Using Debug trait (developer-facing)
println!("Point: {:?}", point); // Point: Point { x: 10, y: 20 }
println!("Point: {:#?}", point); // Pretty-printed:
// Point {
// x: 10,
// y: 20,
// }
}
Advanced Formatting Options
let value = 255;
// Formatting with different bases
println!("Decimal: {}", value); // 255
println!("Binary: {:b}", value); // 11111111
println!("Hex: {:x}", value); // ff
println!("Hex with prefix: {:#x}", value); // 0xff
println!("Octal: {:o}", value); // 377
println!("Octal with prefix: {:#o}", value); // 0o377
// Formatting with sign
let positive = 42;
let negative = -42;
println!("Positive: {:+}", positive); // +42
println!("Negative: {:+}", negative); // -42
// Formatting with thousands separator (using crate)
// Typically you'd use a formatting crate for this
// Conditional formatting
let maybe_value = Some(42);
println!("Option: {:?}", maybe_value); // Some(42)
println!("Option: {}", maybe_value.unwrap_or(0)); // 42
Formatting Collections
let numbers = vec![1, 2, 3, 4, 5];
let words = vec!["apple", "banana", "cherry"];
// Formatting vectors
println!("Numbers: {:?}", numbers); // [1, 2, 3, 4, 5]
println!("Numbers: {:#?}", numbers); // Pretty-printed
println!("Words: {}", words.join(", ")); // apple, banana, cherry
// Custom collection formatting
let formatted = format!("First: {}, Last: {}", numbers[0], numbers[numbers.len() - 1]);
println!("{}", formatted); // First: 1, Last: 5
// Using iterators with format
let squares: Vec<String> = numbers.iter()
.map(|&x| format!("{}²={}", x, x*x))
.collect();
println!("Squares: {}", squares.join(", "));
Common Pitfalls
- Forgetting that format! returns a String, doesn't print
- Mixing up Display (
{}) and Debug ({:?}) formatting - Format strings are checked at compile time - invalid formats cause errors
- Custom types need to implement Display or Debug for formatting
Arrays in Rust
Arrays are fixed-size collections of elements of the same type stored in contiguous memory. Rust arrays have a compile-time fixed size and are stack-allocated.
Fixed-Size Arrays
fn main() {
// Declaration and initialization
let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // Type annotation
let inferred = [1, 2, 3, 4, 5]; // Type inferred as [i32; 5]
let same_value = [0; 5]; // [0, 0, 0, 0, 0]
// Accessing elements
println!("First element: {}", numbers[0]); // 1
println!("Last element: {}", numbers[4]); // 5
// Modifying elements (array must be mutable)
let mut mutable_array = [1, 2, 3, 4, 5];
mutable_array[2] = 100;
println!("Modified: {:?}", mutable_array);
// Array size
println!("Array length: {}", numbers.len());
// Iterating through array
println!("Array elements:");
for i in 0..numbers.len() {
println!("numbers[{}] = {}", i, numbers[i]);
}
// Better: using iterators
for element in &numbers {
println!("Element: {}", element);
}
// With enumerate for index
for (i, element) in numbers.iter().enumerate() {
println!("numbers[{}] = {}", i, element);
}
}
Multi-dimensional Arrays
// 2D array (matrix)
let matrix: [[i32; 3]; 3] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// Accessing elements
println!("Element at [1][2]: {}", matrix[1][2]); // 6
// Nested loops for 2D array
for row in &matrix {
for element in row {
print!("{} ", element);
}
println!();
}
// 3D array
let cube: [[[i32; 2]; 2]; 2] = [
[[1, 2], [3, 4]],
[[5, 6], [7, 8]]
];
println!("Element at [1][0][1]: {}", cube[1][0][1]); // 6
Array Methods and Operations
let arr = [1, 2, 3, 4, 5];
// Slicing arrays
let slice = &arr[1..4]; // [2, 3, 4]
println!("Slice: {:?}", slice);
// Array patterns in match
match arr {
[1, 2, 3, 4, 5] => println!("Exact match!"),
[first, .., last] => println!("First: {}, Last: {}", first, last),
}
// Converting to slices
let slice_ref: &[i32] = &arr;
println!("Slice length: {}", slice_ref.len());
// Array bounds checking
// This would panic at runtime:
// println!("{}", arr[10]);
// Safe access with get()
if let Some(element) = arr.get(2) {
println!("Element at index 2: {}", element); // 3
}
if let None = arr.get(10) {
println!("Index 10 is out of bounds");
}
Array Limitations and Alternatives
// Arrays have fixed size determined at compile time
// This won't work:
// let size = 5;
// let arr = [0; size]; // Error: size must be constant
// Use Vec for dynamic arrays
use std::convert::TryInto;
// Converting between arrays and vectors
let vec = vec![1, 2, 3, 4, 5];
// Convert Vec to array (if you know the size)
let array: [i32; 5] = vec.try_into().unwrap_or_else(|v: Vec<i32>| panic!("Expected length 5, got {}", v.len()));
// Array from function
fn create_array() -> [i32; 3] {
[1, 2, 3] // Return type must match exactly
}
Common Pitfalls
- Array indices start at 0, not 1
- Array size must be known at compile time
- Index out of bounds causes panic at runtime
- Arrays are different types if they have different sizes
- Cannot return arrays from functions that have different sizes
Array Indexing in Rust
Array indexing allows access to individual elements in an array using their position. Rust uses zero-based indexing and provides bounds checking for safety.
Basic Array Indexing
fn main() {
let numbers = [10, 20, 30, 40, 50];
// Accessing elements by index
println!("numbers[0] = {}", numbers[0]); // First element: 10
println!("numbers[2] = {}", numbers[2]); // Third element: 30
println!("numbers[4] = {}", numbers[4]); // Last element: 50
// Modifying elements (array must be mutable)
let mut mutable_numbers = [10, 20, 30, 40, 50];
mutable_numbers[1] = 25; // Change second element from 20 to 25
mutable_numbers[3] = 45; // Change fourth element from 40 to 45
// Display modified array
for (i, &value) in mutable_numbers.iter().enumerate() {
println!("numbers[{}] = {}", i, value);
}
}
Bounds Checking and Safe Access
let arr = [1, 2, 3, 4, 5];
// Unsafe indexing (panics if out of bounds)
// println!("{}", arr[10]); // This would panic!
// Safe indexing with get()
match arr.get(2) {
Some(&value) => println!("Element at index 2: {}", value), // 3
None => println!("Index 2 is out of bounds"),
}
match arr.get(10) {
Some(&value) => println!("Element at index 10: {}", value),
None => println!("Index 10 is out of bounds"), // This executes
}
// Using get() with if let
if let Some(&value) = arr.get(1) {
println!("Second element: {}", value); // 2
}
// Using get_mut() for mutable access
let mut mutable_arr = [1, 2, 3];
if let Some(element) = mutable_arr.get_mut(1) {
*element = 100; // Modify through mutable reference
}
println!("Modified: {:?}", mutable_arr); // [1, 100, 3]
Array Slicing
let numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
// Creating slices
let full_slice = &numbers[..]; // Entire array
let start_slice = &numbers[3..]; // From index 3 to end: [3, 4, 5, 6, 7, 8, 9]
let end_slice = &numbers[..5]; // From start to index 4: [0, 1, 2, 3, 4]
let middle_slice = &numbers[2..7]; // From index 2 to 6: [2, 3, 4, 5, 6]
let exclusive_end = &numbers[2..=6]; // From index 2 to 6 inclusive: [2, 3, 4, 5, 6]
println!("Full: {:?}", full_slice);
println!("Start from 3: {:?}", start_slice);
println!("Up to 5: {:?}", end_slice);
println!("Middle 2..7: {:?}", middle_slice);
// Slices are references and don't own the data
// This allows multiple read-only accesses
let slice1 = &numbers[0..3];
let slice2 = &numbers[3..6];
println!("Slice1: {:?}, Slice2: {:?}", slice1, slice2);
Pattern Matching with Arrays
let point = [10, 20];
// Destructuring arrays
let [x, y] = point;
println!("x: {}, y: {}", x, y); // x: 10, y: 20
// Pattern matching in match statements
match point {
[0, 0] => println!("Origin"),
[x, 0] => println!("On x-axis at {}", x),
[0, y] => println!("On y-axis at {}", y),
[x, y] => println!("At ({}, {})", x, y),
}
// Using .. to ignore parts of the array
let larger = [1, 2, 3, 4, 5];
match larger {
[first, .., last] => println!("First: {}, Last: {}", first, last), // 1, 5
_ => println!("Other pattern"),
}
// Matching specific patterns
let coords = [[1, 2], [3, 4], [5, 6]];
for &[x, y] in &coords {
println!("Coordinate: ({}, {})", x, y);
}
Common Pitfalls
- Accessing out-of-bounds indices causes panic at runtime
- Array indices start at 0, not 1
- Slicing beyond array bounds causes panic
- Forgetting that slices are references, not owned data
Array Functions and Methods in Rust
While Rust arrays have a fixed size and limited built-in methods, there are many ways to work with arrays using standard library functions, iterators, and conversions.
Array Utilities and Conversions
fn main() {
let numbers = [5, 2, 8, 1, 9, 3];
// Array length
println!("Array length: {}", numbers.len());
// Check if empty (arrays are never empty if size > 0)
println!("Is empty: {}", numbers.is_empty()); // false
// First and last elements
println!("First: {:?}", numbers.first()); // Some(5)
println!("Last: {:?}", numbers.last()); // Some(3)
// Converting to slices (most array methods work on slices)
let slice: &[i32] = &numbers;
println!("Slice length: {}", slice.len());
}
Working with Array Iterators
let numbers = [1, 2, 3, 4, 5];
// Basic iteration
println!("Elements:");
for num in &numbers {
println!("{}", num);
}
// Using iterator methods
let sum: i32 = numbers.iter().sum();
println!("Sum: {}", sum); // 15
let doubled: Vec<i32> = numbers.iter().map(|&x| x * 2).collect();
println!("Doubled: {:?}", doubled); // [2, 4, 6, 8, 10]
// Filtering
let evens: Vec<&i32> = numbers.iter().filter(|&&x| x % 2 == 0).collect();
println!("Evens: {:?}", evens); // [2, 4]
// Finding elements
if let Some(&value) = numbers.iter().find(|&&x| x == 3) {
println!("Found 3 at some position");
}
// Position of element
if let Some(pos) = numbers.iter().position(|&x| x == 4) {
println!("4 found at position: {}", pos); // 3
}
Array Manipulation Techniques
// Since arrays are fixed-size, we often work with slices or convert to Vec
// Sorting (this consumes the array and returns a new one)
let mut unsorted = [5, 2, 8, 1, 9];
unsorted.sort();
println!("Sorted: {:?}", unsorted); // [1, 2, 5, 8, 9]
// Reverse
unsorted.reverse();
println!("Reversed: {:?}", unsorted); // [9, 8, 5, 2, 1]
// Fill array with value
let mut arr = [0; 5];
arr.fill(42);
println!("Filled: {:?}", arr); // [42, 42, 42, 42, 42]
// Windows and chunks (working with slices)
let data = [1, 2, 3, 4, 5, 6];
for window in data.windows(3) {
println!("Window: {:?}", window); // [1,2,3], [2,3,4], [3,4,5], [4,5,6]
}
for chunk in data.chunks(2) {
println!("Chunk: {:?}", chunk); // [1,2], [3,4], [5,6]
}
Array Conversions
use std::convert::TryInto;
// Converting between arrays and other types
let vec = vec![1, 2, 3, 4, 5];
// Vec to array (must know exact size)
let array: [i32; 5] = match vec.try_into() {
Ok(arr) => arr,
Err(_) => panic!("Cannot convert Vec to array - wrong size"),
};
// Array to Vec
let back_to_vec = array.to_vec();
println!("Back to Vec: {:?}", back_to_vec);
// TryFrom conversion (more idiomatic)
let another_array: [i32; 5] = vec.try_into().expect("Wrong size");
// Converting between different array sizes
let small = [1, 2, 3];
// This doesn't work directly:
// let large: [i32; 5] = small; // Error
// Manual conversion
let mut large = [0; 5];
large[..3].copy_from_slice(&small);
println!("Extended: {:?}", large); // [1, 2, 3, 0, 0]
Multi-dimensional Array Operations
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// Flattening 2D array
let flattened: Vec<i32> = matrix.iter().flatten().cloned().collect();
println!("Flattened: {:?}", flattened); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// Transpose (manual implementation)
let mut transpose = [[0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
transpose[j][i] = matrix[i][j];
}
}
println!("Transpose: {:?}", transpose); // [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
// Working with rows and columns
for (i, row) in matrix.iter().enumerate() {
println!("Row {}: {:?}", i, row);
}
// Column iteration (more complex)
for col in 0..3 {
let column: Vec<i32> = matrix.iter().map(|row| row[col]).collect();
println!("Column {}: {:?}", col, column);
}
Common Pitfalls
- Arrays don't have many built-in methods - use slices or iterators
- Cannot resize arrays - use Vec for dynamic sizing
- Array conversions require exact size matching
- Multi-dimensional array operations often require manual loops
Vectors in Rust
Vectors (Vec<T>) are growable arrays that can dynamically resize. They are heap-allocated and one of the most commonly used data structures in Rust.
Creating and Using Vectors
fn main() {
// Different ways to create vectors
let numbers: Vec<i32> = Vec::new(); // Empty vector
let mut primes = vec![2, 3, 5, 7, 11]; // Initialized with macro
let zeros = vec![0; 5]; // 5 elements, all 0
// Adding elements
primes.push(13); // Add to end
primes.insert(2, 4); // Insert at index 2
// Accessing elements
println!("First: {}", primes[0]); // 2
println!("Second: {}", primes.get(1).unwrap()); // 3 (safe access)
// Size and capacity
println!("Length: {}", primes.len()); // 7
println!("Capacity: {}", primes.capacity()); // Current capacity
println!("Is empty: {}", primes.is_empty()); // false
// Reserve capacity
primes.reserve(10);
println!("New capacity: {}", primes.capacity());
// Remove elements
let removed = primes.remove(2); // Remove element at index 2
let popped = primes.pop(); // Remove last element
println!("Removed: {}, Popped: {:?}", removed, popped);
println!("Final vector: {:?}", primes);
}
Vector Operations
let mut vec = vec![1, 2, 3, 4, 5];
// Iterating
println!("Elements:");
for i in 0..vec.len() {
println!("vec[{}] = {}", i, vec[i]);
}
// Better: using iterators (borrowed)
println!("Using iterator:");
for element in &vec {
println!("Element: {}", element);
}
// Mutable iteration
for element in &mut vec {
*element *= 2; // Double each element
}
println!("Doubled: {:?}", vec); // [2, 4, 6, 8, 10]
// Range-based for loop with values (consumes vector)
// for element in vec { ... } // This would move vec
// Useful vector methods
vec.clear(); // Remove all elements
vec.extend([1, 2, 3].iter().copied()); // Add multiple elements
vec.truncate(2); // Truncate to first 2 elements
vec.shrink_to_fit(); // Reduce capacity to fit current size
println!("After operations: {:?}", vec);
Vector vs Array
// Arrays: Fixed size, stack allocation
let arr = [1, 2, 3, 4, 5];
// arr.push(6); // Error - fixed size
// Vectors: Dynamic size, heap allocation
let mut vec = vec![1, 2, 3, 4, 5];
vec.push(6); // OK - can grow
vec.pop(); // Remove last element
// Converting between arrays and vectors
let array = [1, 2, 3];
let vector = array.to_vec(); // Array to Vec
let from_vec: Vec<i32> = vec![4, 5, 6];
// Try to convert Vec to array (must be exact size)
if let Ok(array_back) = from_vec.try_into() {
let arr: [i32; 3] = array_back;
println!("Back to array: {:?}", arr);
}
Advanced Vector Usage
// Vector of different types using enums
#[derive(Debug)]
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Float(10.12),
SpreadsheetCell::Text(String::from("blue")),
];
println!("Row: {:?}", row);
// Splitting vectors
let mut numbers = vec![1, 2, 3, 4, 5, 6];
let second_half = numbers.split_off(3); // numbers now has [1,2,3], second_half has [4,5,6]
println!("First: {:?}, Second: {:?}", numbers, second_half);
// Drain elements (remove range and get iterator)
let mut data = vec![1, 2, 3, 4, 5];
let drained: Vec<i32> = data.drain(1..4).collect(); // data becomes [1,5]
println!("Drained: {:?}, Remaining: {:?}", drained, data);
// Retain elements matching condition
let mut values = vec![1, 2, 3, 4, 5, 6];
values.retain(|&x| x % 2 == 0); // Keep only even numbers
println!("Even values: {:?}", values); // [2, 4, 6]
Vector Performance Considerations
// Pre-allocate capacity when size is known
let mut large_vec = Vec::with_capacity(1000);
for i in 0..1000 {
large_vec.push(i);
}
// This avoids repeated reallocations
println!("Length: {}, Capacity: {}", large_vec.len(), large_vec.capacity());
// Collect from iterator with size hint
let collected: Vec<i32> = (0..1000).collect();
println!("Collected length: {}", collected.len());
// Using into_iter() vs iter()
let vec1 = vec![1, 2, 3];
// This consumes vec1:
// for element in vec1.into_iter() { ... }
// This borrows vec1:
for element in vec1.iter() {
println!("{}", element);
}
// vec1 is still usable here
Common Pitfalls
- Accessing elements with
[]can panic if index is out of bounds - Use
get()for bounds-checked access - Vectors can be less efficient than arrays for very small, fixed-size data
- Inserting/erasing in the middle is O(n) operation
- Forgetting that some operations consume the vector
References in Rust
References are non-owning pointers that allow you to access data without taking ownership. Rust's reference system is central to its memory safety guarantees.
Basic Reference Operations
fn main() {
let number = 42;
let number_ref = &number; // Immutable reference
println!("Value: {}", number);
println!("Address: {:p}", number_ref);
println!("Value through reference: {}", *number_ref); // Dereferencing
// Mutable references
let mut mutable_number = 100;
let mutable_ref = &mut mutable_number;
*mutable_ref += 50; // Modify through mutable reference
println!("Modified value: {}", mutable_number); // 150
// References in functions
let text = String::from("Hello");
let length = calculate_length(&text); // Pass reference, not ownership
println!("Length of '{}': {}", text, length); // text still usable here
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but since it's a reference, nothing is dropped
Reference Rules and Limitations
let mut data = String::from("Hello");
// Multiple immutable references are allowed
let ref1 = &data;
let ref2 = &data;
println!("{}, {}", ref1, ref2); // Both can be used
// But only one mutable reference at a time
let mut_ref1 = &mut data;
// let mut_ref2 = &mut data; // This would cause compile error!
println!("{}", mut_ref1);
// Cannot mix mutable and immutable references
// let immutable_ref = &data; // This would error
// let mutable_ref = &mut data; // if this exists
// References must always be valid
fn dangling_reference_example() {
// This function would not compile:
// let reference = dangle(); // Error: returns reference to dropped value
}
// fn dangle() -> &String {
// let s = String::from("hello");
// &s // s is dropped here, so reference would be invalid
// }
Working with References in Data Structures
// Struct with references require lifetimes
#[derive(Debug)]
struct Book<'a> {
title: &'a str,
author: &'a str,
}
fn main() {
let title = "The Rust Programming Language";
let author = "Steve Klabnik and Carol Nichols";
let book = Book { title, author };
println!("Book: {:?}", book);
// References in collections
let strings = vec!["hello", "world"];
let string_refs: Vec<&str> = strings.iter().map(|s| *s).collect();
println!("String refs: {:?}", string_refs);
// Reference to slice
let array = [1, 2, 3, 4, 5];
let slice_ref = &array[1..4]; // Reference to part of array
println!("Slice: {:?}", slice_ref); // [2, 3, 4]
}
Reference Patterns and Destructuring
let point = (10, 20);
// Pattern matching with references
match &point {
(x, y) => println!("Point at ({}, {})", x, y), // x and y are &i32
}
// Destructuring references
let &(x, y) = &point;
println!("x: {}, y: {}", x, y);
// Reference patterns in function parameters
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({}, {})", x, y);
}
print_coordinates(&point);
// Using ref in patterns (less common in modern Rust)
let maybe_value = Some(42);
if let Some(ref value) = maybe_value {
println!("Got a reference to: {}", value); // value is &i32
}
Common Pitfalls
- Trying to create multiple mutable references to the same data
- Mixing mutable and immutable references
- Returning references to local variables (dangling references)
- Forgetting that references have lifetime constraints
- Confusing
&Twith&mut Tin function signatures
Borrowing in Rust
Borrowing is Rust's mechanism for allowing temporary access to data without transferring ownership. The borrow checker enforces rules at compile time to prevent data races and memory errors.
Borrowing Rules
fn main() {
let mut s = String::from("hello");
// Rule 1: Any number of immutable borrows
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point
// Rule 2: Only one mutable borrow in scope
let r3 = &mut s;
// println!("{}", r1); // This would error - can't use r1 while r3 exists
r3.push_str(", world");
println!("{}", r3);
// Rule 3: Cannot mix mutable and immutable borrows
// let r4 = &s; // This would error
// let r5 = &mut s; // if both exist
demonstrate_scope();
}
fn demonstrate_scope() {
let mut data = String::from("test");
{
let borrow1 = &data; // First immutable borrow
println!("First borrow: {}", borrow1);
} // borrow1 goes out of scope here
// Now we can borrow mutably because no other references exist
let borrow2 = &mut data;
borrow2.push_str(" modified");
println!("After mutable borrow: {}", borrow2);
}
Function Parameters and Borrowing
// Functions that borrow vs take ownership
fn take_ownership(s: String) -> String {
println!("I own: {}", s);
s // Return ownership
}
fn borrow_readonly(s: &String) {
println!("I can read: {}", s);
// s.push_str("!"); // Error - s is immutable reference
}
fn borrow_mutable(s: &mut String) {
s.push_str("!");
println!("I modified: {}", s);
}
fn main() {
let mut text = String::from("Hello");
borrow_readonly(&text); // Immutable borrow
borrow_mutable(&mut text); // Mutable borrow
println!("After functions: {}", text); // text still owned here
let returned = take_ownership(text); // Ownership transferred
// println!("{}", text); // Error - text was moved
println!("Returned: {}", returned);
}
Borrowing with Collections
fn main() {
let mut vec = vec![1, 2, 3, 4, 5];
// Borrowing elements from vector
let first = &vec[0];
// vec.push(6); // This would error - can't mutate while borrowed
println!("First element: {}", first);
// After first is no longer used, we can modify
vec.push(6);
println!("Vector: {:?}", vec);
// Iterators and borrowing
for element in &vec { // Immutable borrow
println!("Element: {}", element);
}
// Mutable iteration
for element in &mut vec { // Mutable borrow
*element *= 2;
}
println!("Doubled: {:?}", vec);
// Using iter() vs into_iter()
let doubled: Vec<i32> = vec.iter().map(|x| x * 2).collect(); // vec still usable
// let consumed: Vec<i32> = vec.into_iter().collect(); // vec consumed
}
Lifetimes and Borrowing
// Simple case - compiler can infer lifetimes
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
// Explicit lifetime annotation needed for structs
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
println!("Excerpt: {}", i.part);
}
Common Borrowing Patterns
// Returning references from functions
fn get_first_char(s: &String) -> Option<char> {
s.chars().next()
}
// Working with Option<&T>
fn find_number(numbers: &[i32], target: i32) -> Option<&i32> {
numbers.iter().find(|&&x| x == target)
}
// Borrowing in match expressions
fn process_option(opt: &Option<String>) {
match opt {
Some(s) => println!("Got: {}", s), // s is &String
None => println!("Got nothing"),
}
}
// Using RefCell for interior mutability (advanced)
use std::cell::RefCell;
let data = RefCell::new(42);
{
let mut borrow = data.borrow_mut();
*borrow += 1;
}
println!("Data: {}", *data.borrow());
Common Pitfalls
- Trying to mutate a borrowed value without a mutable reference
- Creating dangling references by returning references to local data
- Forgetting that borrowed data cannot be moved while borrowed
- Mixing different kinds of borrows in the same scope
- Not understanding lifetime elision rules
Variables in Rust
Variables in Rust are immutable by default and have move semantics. Understanding variable binding, mutability, and ownership is crucial for writing correct Rust code.
Variable Declaration and Mutability
fn main() {
// Immutable by default
let x = 5;
// x = 6; // Error: cannot assign twice to immutable variable
// Mutable variables
let mut y = 10;
y = 15; // This is allowed
println!("y: {}", y);
// Variable shadowing (different from mutability)
let z = 5;
let z = z + 1; // New variable with same name
let z = z * 2; // Another new variable
println!("z: {}", z); // 12
// Shadowing with different type
let spaces = " ";
let spaces = spaces.len(); // This is allowed with shadowing
println!("Spaces: {}", spaces);
// Constants (must have type annotation)
const MAX_POINTS: u32 = 100_000;
println!("Max points: {}", MAX_POINTS);
}
Variable Scope and Ownership
fn main() {
// Variable comes into scope
let s = String::from("hello"); // s is valid from this point forward
takes_ownership(s); // s's value moves into the function...
// ... and is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // x would move into the function,
// but i32 is Copy, so it's okay to still
// use x afterward
// println!("{}", s); // This would error - s was moved
println!("x: {}", x); // This works - x is Copy
} // Here, x goes out of scope, then s. But since s's value was moved, nothing
// special happens.
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.
Pattern Matching and Destructuring
// Destructuring tuples
let point = (10, 20);
let (x, y) = point;
println!("x: {}, y: {}", x, y);
// Destructuring structs
struct Point {
x: i32,
y: i32,
}
let point = Point { x: 10, y: 20 };
let Point { x: a, y: b } = point;
println!("a: {}, b: {}", a, b);
// Shorthand destructuring (same variable names)
let Point { x, y } = point;
println!("x: {}, y: {}", x, y);
// Pattern matching in let
let some_option = Some(5);
if let Some(value) = some_option {
println!("Got a value: {}", value);
}
// Multiple patterns
let number = 7;
match number {
1 => println!("One"),
2 | 3 | 5 | 7 => println!("Prime"),
_ => println!("Other"),
}
Type Inference and Annotations
// Rust has excellent type inference
let x = 5; // Compiler infers i32
let y = 3.14; // Compiler infers f64
let z = "hello"; // Compiler infers &str
// Sometimes explicit types are needed
let collected: Vec<i32> = (0..5).collect();
let parsed: i32 = "42".parse().unwrap();
// Turbofish syntax for explicit types
let numbers = (0..5).collect::<Vec<i32>>();
let float_vec = vec![1.0, 2.0, 3.0f64];
// Type aliases
type Kilometers = i32;
let distance: Kilometers = 100;
println!("Distance: {} km", distance);
Advanced Variable Patterns
// Variables in different contexts
fn main() {
// @ bindings in patterns
let value = Some(42);
match value {
Some(x @ 1..=100) => println!("Got small number: {}", x),
Some(x) => println!("Got large number: {}", x),
None => println!("Got nothing"),
}
// Ignoring parts of patterns
let (x, _, z) = (1, 2, 3);
println!("x: {}, z: {}", x, z);
// .. to ignore remaining parts
let numbers = (1, 2, 3, 4, 5);
match numbers {
(first, .., last) => println!("First: {}, Last: {}", first, last),
}
// ref patterns (less common in modern Rust)
let maybe_string = Some(String::from("hello"));
if let Some(ref s) = maybe_string {
println!("s is a reference to: {}", s); // s is &String
}
// maybe_string is still usable here
// ref mut for mutable references
let mut maybe_num = Some(42);
if let Some(ref mut n) = maybe_num {
*n += 1;
}
println!("Maybe num: {:?}", maybe_num); // Some(43)
}
Common Pitfalls
- Forgetting that variables are immutable by default
- Confusing variable shadowing with mutability
- Trying to use moved values after ownership transfer
- Not understanding the difference between Copy and Move types
- Forgetting that pattern matching can move values
Conditional Statements: if, else if, else
Conditional statements allow your program to make decisions and execute different code blocks based on conditions. Rust's if expressions are more powerful than in many languages.
Basic if Statement
fn main() {
let number = 7;
// Simple if statement
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
// if as an expression
let condition = true;
let result = if condition { 5 } else { 6 };
println!("The value of result is: {}", result);
// Each arm must return the same type
// This would error:
// let invalid = if condition { 5 } else { "six" };
}
if-else if-else Chain
let score = 85;
if score >= 90 {
println!("Grade: A");
} else if score >= 80 {
println!("Grade: B");
} else if score >= 70 {
println!("Grade: C");
} else if score >= 60 {
println!("Grade: D");
} else {
println!("Grade: F");
}
// Using if expressions for assignment
let grade = if score >= 90 {
'A'
} else if score >= 80 {
'B'
} else if score >= 70 {
'C'
} else if score >= 60 {
'D'
} else {
'F'
};
println!("Your grade is: {}", grade);
Pattern Matching with if let
// if let for concise pattern matching
let some_value: Option<i32> = Some(42);
// Traditional match
match some_value {
Some(42) => println!("The answer to everything!"),
Some(x) => println!("Got some other value: {}", x),
None => println!("Got nothing"),
}
// Equivalent with if let
if let Some(42) = some_value {
println!("The answer to everything!");
} else if let Some(x) = some_value {
println!("Got some other value: {}", x);
} else {
println!("Got nothing");
}
// Combining conditions with patterns
let config_max = Some(3u8);
if let Some(max) = config_max && max > 5 {
println!("The maximum is configured to be {}", max);
} else {
println!("Maximum not configured or too small");
}
Complex Conditions
// Multiple conditions
let age = 25;
let has_license = true;
if age >= 18 && has_license {
println!("You can drive");
} else if age >= 18 && !has_license {
println!("You need to get a license first");
} else {
println!("You are too young to drive");
}
// Nested if statements
let number = 15;
if number % 2 == 0 {
println!("Number is even");
if number % 4 == 0 {
println!("Number is divisible by 4");
} else {
println!("Number is not divisible by 4");
}
} else {
println!("Number is odd");
if number % 3 == 0 {
println!("Number is divisible by 3");
}
}
// Using boolean expressions directly
let is_even = number % 2 == 0;
if is_even {
println!("The number is even");
}
Conditional with let-else
// let-else for pattern matching that must succeed
fn process_user_input(input: &str) {
let Ok(number) = input.parse::<i32>() else {
println!("Please input a valid number!");
return;
};
println!("You entered: {}", number);
}
// This is equivalent to:
fn process_user_input_alt(input: &str) {
let number = match input.parse::<i32>() {
Ok(n) => n,
Err(_) => {
println!("Please input a valid number!");
return;
}
};
println!("You entered: {}", number);
}
Common Pitfalls
- Forgetting that
ifconditions must be bool (no truthy/falsy values) - Mismatched types in if expression arms
- Using assignment
=instead of comparison==in conditions - Overusing nested if statements when match would be clearer
- Forgetting that patterns in if let can move values
for Loop in Rust
The for loop is the most common loop in Rust, used to iterate over collections, ranges, and any type that implements the Iterator trait.
Basic for Loop
fn main() {
// Iterate over a range
for number in 1..=5 {
println!("Count: {}", number);
}
// Iterate over array or vector
let numbers = [10, 20, 30, 40, 50];
for number in numbers {
println!("Number: {}", number);
}
// Iterate with reference (doesn't consume collection)
let names = vec!["Alice", "Bob", "Charlie"];
for name in &names {
println!("Name: {}", name);
}
// names is still usable here
// Iterate with mutable reference
let mut scores = vec![85, 92, 78];
for score in &mut scores {
*score += 5; // Add 5 to each score
}
println!("Updated scores: {:?}", scores);
}
Advanced for Loop Usage
// Using enumerate to get index
let fruits = ["apple", "banana", "cherry"];
for (index, fruit) in fruits.iter().enumerate() {
println!("Fruit {}: {}", index, fruit);
}
// Iterating over characters in a string
let text = "Hello";
for c in text.chars() {
println!("Character: {}", c);
}
// Iterating over bytes in a string
for byte in text.bytes() {
println!("Byte: {}", byte);
}
// Using ranges with steps
for i in (0..10).step_by(2) {
println!("Even: {}", i); // 0, 2, 4, 6, 8
}
// Reverse iteration
for i in (0..5).rev() {
println!("Countdown: {}", i); // 4, 3, 2, 1, 0
}
// Iterating over HashMap
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("red", 1);
map.insert("green", 2);
map.insert("blue", 3);
for (key, value) in &map {
println!("{}: {}", key, value);
}
Nested for Loops
// Multiplication table
for i in 1..=5 {
for j in 1..=5 {
print!("{}\t", i * j);
}
println!();
}
// Pattern printing
for i in 1..=5 {
for _ in 1..=i {
print!("*");
}
println!();
}
// Iterating over 2D array
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
for row in matrix.iter() {
for element in row.iter() {
print!("{} ", element);
}
println!();
}
Custom Iterators with for
// Any type that implements IntoIterator can be used in for loops
struct Counter {
count: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Counter {
Counter { count: 0, max }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
// Now we can use Counter in for loops
for number in Counter::new(5) {
println!("Counter: {}", number); // 1, 2, 3, 4, 5
}
// Using iterator adapters
let result: Vec<i32> = (1..=10)
.filter(|x| x % 2 == 0)
.map(|x| x * x)
.collect();
for value in result {
println!("Processed: {}", value); // 4, 16, 36, 64, 100
}
Common Pitfalls
- Using
forwith types that don't implement Iterator - Accidentally consuming collections with
into_iter() - Modifying a collection while iterating over it
- Forgetting that range upper bound is exclusive:
1..5gives 1,2,3,4 - Using wrong iterator method (
iter()vsinto_iter()vsiter_mut())
while Loop in Rust
The while loop repeats a block of code as long as a condition is true. It's useful when you don't know in advance how many iterations are needed.
Basic while Loop
fn main() {
// Count from 1 to 5
let mut i = 1;
while i <= 5 {
println!("Count: {}", i);
i += 1;
}
// User input validation
let mut input = String::new();
println!("Enter a positive number: ");
// This is simplified - real input handling would be more complex
while input.trim().parse::<i32>().unwrap_or(0) <= 0 {
input.clear();
println!("Invalid input. Enter a positive number: ");
// In real code, you would read from stdin here
input = "42".to_string(); // Simulating valid input
}
println!("Thank you! You entered: {}", input);
}
while let Pattern
// while let for pattern matching that might fail
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
// Pop elements until empty
while let Some(top) = stack.pop() {
println!("Popped: {}", top);
}
// Processing streams of data
let mut data = vec![Some(1), Some(2), None, Some(4)];
let mut iter = data.iter_mut();
while let Some(Some(value)) = iter.next() {
*value *= 2;
}
println!("Processed: {:?}", data); // [Some(2), Some(4), None, Some(8)]
// Reading until sentinel value (conceptual)
let mut values = vec![1, 2, 3, 0, 4, 5];
let mut sum = 0;
let mut index = 0;
while index < values.len() && values[index] != 0 {
sum += values[index];
index += 1;
}
println!("Sum until zero: {}", sum); // 6
Practical while Loop Examples
// Game loop example
fn game_loop() {
let mut game_over = false;
let mut score = 0;
while !game_over {
// Simulate game round
score += 10;
println!("Score: {}", score);
// Check win condition
if score >= 100 {
game_over = true;
println!("You win!");
}
// In real game, this would have some condition
if score > 50 {
// Simulating random game over
game_over = true;
println!("Game over!");
}
}
}
// Processing until condition
fn find_first_negative(numbers: &[i32]) -> Option<usize> {
let mut index = 0;
while index < numbers.len() {
if numbers[index] < 0 {
return Some(index);
}
index += 1;
}
None
}
let data = [1, 2, -3, 4, 5];
if let Some(pos) = find_first_negative(&data) {
println!("First negative at position: {}", pos);
}
// Infinite loop with break (often better than while true)
let mut counter = 0;
loop {
counter += 1;
if counter > 5 {
break;
}
println!("Counter: {}", counter);
}
Loop Control in while
let mut x = 0;
// Using continue
while x < 10 {
x += 1;
if x % 2 == 0 {
continue; // Skip even numbers
}
println!("Odd: {}", x); // 1, 3, 5, 7, 9
}
// Using break with value
let mut attempt = 0;
let result = loop {
attempt += 1;
if attempt > 3 {
break "failed";
}
// Simulate some operation that might succeed
if attempt == 2 {
break "success";
}
};
println!("Result: {}", result); // success
// Nested loops with labels
let mut i = 0;
'outer: while i < 3 {
let mut j = 0;
while j < 3 {
if i * j > 4 {
println!("Breaking both loops at i={}, j={}", i, j);
break 'outer;
}
println!("i={}, j={}", i, j);
j += 1;
}
i += 1;
}
Common Pitfalls
- Infinite loops when condition never becomes false
- Forgetting to update the loop control variable
- Using
while trueinstead ofloopfor infinite loops - Modifying data being iterated over (can cause bugs or panics)
- Off-by-one errors in loop conditions
Loop Control Statements
Rust provides loop control statements to alter the normal flow of loops: break, continue, and loop labels. These work with all loop types (loop, while, for).
break and continue
fn main() {
// break exits the loop immediately
for i in 1..=10 {
if i == 5 {
break; // Exit loop when i reaches 5
}
println!("{}", i);
}
// Output: 1 2 3 4
// continue skips the rest of current iteration
for i in 1..=10 {
if i % 2 == 0 {
continue; // Skip even numbers
}
println!("{}", i); // Only odd numbers printed
}
// Output: 1 3 5 7 9
// break with value (only in loop, not while or for)
let result = loop {
let number = 7;
if number > 5 {
break number * 2; // Break with value
}
};
println!("The result is {}", result); // 14
}
Loop Labels
// Labeling loops for control
'outer: for i in 1..=3 {
'inner: for j in 1..=3 {
if i * j > 4 {
println!("Breaking outer loop at i={}, j={}", i, j);
break 'outer; // Break both loops
}
println!("i={}, j={}", i, j);
}
}
/* Output:
i=1, j=1
i=1, j=2
i=1, j=3
i=2, j=1
i=2, j=2
Breaking outer loop at i=2, j=3
*/
// Continue with labels
'outer: for i in 1..=3 {
for j in 1..=3 {
if i == 2 && j == 2 {
println!("Skipping i=2, j=2");
continue 'outer; // Continue outer loop
}
println!("i={}, j={}", i, j);
}
}
/* Output:
i=1, j=1
i=1, j=2
i=1, j=3
i=2, j=1
Skipping i=2, j=2
i=3, j=1
i=3, j=2
i=3, j=3
*/
Practical Loop Control Examples
// Search example with break
fn find_number(numbers: &[i32], target: i32) -> Option<usize> {
for (index, &number) in numbers.iter().enumerate() {
if number == target {
return Some(index); // Early return works like break
}
}
None
}
let data = [10, 20, 30, 40, 50];
if let Some(pos) = find_number(&data, 30) {
println!("Found at position: {}", pos);
}
// Input validation with continue
let mut valid_inputs = 0;
for attempt in 1..=5 {
// Simulate input - some are invalid
let input = if attempt % 2 == 0 { "42" } else { "invalid" };
if input.parse::<i32>().is_err() {
println!("Attempt {}: Invalid input, skipping...", attempt);
continue;
}
valid_inputs += 1;
println!("Attempt {}: Valid input received", attempt);
}
println!("Total valid inputs: {}", valid_inputs);
// Complex condition with break and continue
let mut count = 0;
for i in 1..=100 {
if i % 3 == 0 && i % 5 == 0 {
println!("FizzBuzz");
continue;
}
if i % 3 == 0 {
println!("Fizz");
continue;
}
if i % 5 == 0 {
println!("Buzz");
continue;
}
count += 1;
if count > 10 {
println!("Reached limit of 10 numbers");
break;
}
println!("{}", i);
}
Loop Control in Different Contexts
// In while loops
let mut x = 0;
while x < 10 {
x += 1;
if x == 5 {
continue; // Skip the rest when x is 5
}
if x == 8 {
break; // Exit when x is 8
}
println!("x = {}", x);
}
// Output: 1 2 3 4 6 7
// In nested loops without labels
for i in 1..=3 {
for j in 1..=3 {
if i * j > 4 {
println!("Breaking inner loop at i={}, j={}", i, j);
break; // Only breaks inner loop
}
println!("i={}, j={}", i, j);
}
}
/* Output:
i=1, j=1
i=1, j=2
i=1, j=3
i=2, j=1
i=2, j=2
Breaking inner loop at i=2, j=3
i=3, j=1
Breaking inner loop at i=3, j=2
*/
// Using return instead of break for function exit
fn process_data(data: &[i32]) -> Option<i32> {
for &value in data {
if value < 0 {
println!("Negative value found, aborting");
return None; // Exit entire function
}
if value == 0 {
println!("Zero found, skipping");
continue; // Continue to next iteration
}
println!("Processing: {}", value);
}
Some(42) // Success case
}
Common Pitfalls
- Using
breakwhencontinueis more appropriate - Forgetting that
breakwith value only works inloop - Overusing loop labels can make code harder to read
breakwithout labels only exits the innermost loop- Placing code after
continuethat will never execute
Nested Loops in Rust
Nested loops are loops within loops. They are essential for working with multi-dimensional data, matrices, and complex patterns.
Basic Nested Loops
fn main() {
// Simple nested loop
for i in 1..=3 {
for j in 1..=3 {
println!("({}, {})", i, j);
}
}
/* Output:
(1,1) (1,2) (1,3)
(2,1) (2,2) (2,3)
(3,1) (3,2) (3,3) */
// Multiplication table
println!("\nMultiplication Table:");
for i in 1..=5 {
for j in 1..=5 {
print!("{}\t", i * j);
}
println!();
}
}
Pattern Printing with Nested Loops
let rows = 5;
// Right triangle
for i in 1..=rows {
for _ in 1..=i {
print!("*");
}
println!();
}
/* Output:
*
**
***
****
*****
*/
// Inverted triangle
for i in (1..=rows).rev() {
for _ in 1..=i {
print!("*");
}
println!();
}
/* Output:
*****
****
***
**
*
*/
// Pyramid
for i in 1..=rows {
// Print spaces
for _ in 1..=(rows - i) {
print!(" ");
}
// Print stars
for _ in 1..=(2 * i - 1) {
print!("*");
}
println!();
}
/* Output:
*
***
*****
*******
*********
*/
Working with 2D Arrays
// Matrix operations
const ROWS: usize = 3;
const COLS: usize = 3;
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// Print matrix
println!("Matrix:");
for i in 0..ROWS {
for j in 0..COLS {
print!("{} ", matrix[i][j]);
}
println!();
}
// Sum of all elements
let mut sum = 0;
for i in 0..ROWS {
for j in 0..COLS {
sum += matrix[i][j];
}
}
println!("Sum of all elements: {}", sum);
// Transpose matrix
println!("Transpose:");
for j in 0..COLS {
for i in 0..ROWS {
print!("{} ", matrix[i][j]);
}
println!();
}
// Element-wise operations
let mut result = [[0; COLS]; ROWS];
for i in 0..ROWS {
for j in 0..COLS {
result[i][j] = matrix[i][j] * 2;
}
}
println!("Doubled matrix: {:?}", result);
Mixed Loop Types
// while inside for
for i in 1..=3 {
let mut j = 1;
while j <= 3 {
println!("i={}, j={}", i, j);
j += 1;
}
}
// loop inside for
for i in 1..=2 {
let mut j = 1;
loop {
println!("i={}, j={}", i, j);
j += 1;
if j > 2 {
break;
}
}
}
// Using different loop controls in nested loops
'outer: for i in 1..=3 {
for j in 1..=3 {
if i == 2 && j == 2 {
println!("Skipping i=2, j=2");
continue 'outer; // Continue to next i
}
if i * j > 6 {
println!("Product too large at i={}, j={}", i, j);
break 'outer; // Break both loops
}
println!("i={}, j={}, product={}", i, j, i * j);
}
}
Performance Considerations
// Cache-friendly iteration (row-major order)
let mut matrix = [[0; 1000]; 1000];
// Good: Iterate row by row (cache-friendly)
for i in 0..1000 {
for j in 0..1000 {
matrix[i][j] = i + j;
}
}
// Bad: Iterate column by column (cache-unfriendly)
// for j in 0..1000 {
// for i in 0..1000 {
// matrix[i][j] = i + j;
// }
// }
// Using iterators for nested loops (often more efficient)
let data = vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]];
let flat: Vec<i32> = data.iter().flatten().cloned().collect();
println!("Flattened: {:?}", flat);
let sum: i32 = data.iter().flatten().sum();
println!("Total sum: {}", sum);
// Using enumerate for indices in nested loops
for (i, row) in data.iter().enumerate() {
for (j, &value) in row.iter().enumerate() {
println!("data[{}][{}] = {}", i, j, value);
}
}
Common Pitfalls
- O(n²) time complexity can be inefficient for large data
- Using wrong loop variables in inner vs outer loops
- Cache-unfriendly access patterns in multi-dimensional arrays
- Excessive nesting reduces readability (try to keep to 2-3 levels)
- Forgetting to break/continue the correct loop when using labels
Functions in Rust
Functions are reusable blocks of code that perform specific tasks. Rust functions are first-class citizens and support features like pattern matching, multiple return values, and closures.
Function Definition and Usage
// Function declaration
fn greet() {
println!("Hello, world!");
}
// Function with parameters and return type
fn add_numbers(a: i32, b: i32) -> i32 {
a + b // No semicolon - this is an expression, not a statement
}
// Function with explicit return
fn subtract_numbers(a: i32, b: i32) -> i32 {
return a - b; // Explicit return with semicolon
}
// Function with multiple parameters
fn print_info(name: &str, age: u32) {
println!("{} is {} years old", name, age);
}
fn main() {
greet();
let sum = add_numbers(5, 3);
println!("5 + 3 = {}", sum);
let difference = subtract_numbers(10, 4);
println!("10 - 4 = {}", difference);
print_info("Alice", 30);
}
Function Parameters and Ownership
// Pass by value (takes ownership)
fn take_ownership(s: String) {
println!("I own: {}", s);
} // s is dropped here
// Pass by immutable reference (borrows)
fn borrow_immutable(s: &String) -> usize {
s.len()
} // s is not dropped - it's just a reference
// Pass by mutable reference (mutably borrows)
fn borrow_mutable(s: &mut String) {
s.push_str(" world");
}
fn main() {
let s1 = String::from("hello");
// take_ownership(s1); // s1 is moved, can't use it after
// println!("{}", s1); // This would error
let len = borrow_immutable(&s1); // s1 is still owned here
println!("Length: {}, s1: {}", len, s1);
let mut s2 = String::from("hello");
borrow_mutable(&mut s2);
println!("After mutation: {}", s2);
}
Returning Values from Functions
// Returning multiple values using tuples
fn calculate_stats(numbers: &[i32]) -> (i32, i32, f64) {
let sum: i32 = numbers.iter().sum();
let count = numbers.len() as i32;
let average = sum as f64 / count as f64;
(sum, count, average) // Return tuple
}
// Returning Option for functions that might fail
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}
// Returning Result for error handling
fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse()
}
// Early return with condition
fn find_first_even(numbers: &[i32]) -> Option<usize> {
for (i, &num) in numbers.iter().enumerate() {
if num % 2 == 0 {
return Some(i); // Early return
}
}
None // Return None if no even numbers found
}
fn main() {
let data = [1, 2, 3, 4, 5];
let (sum, count, avg) = calculate_stats(&data);
println!("Sum: {}, Count: {}, Average: {:.2}", sum, count, avg);
match divide(10.0, 2.0) {
Some(result) => println!("Division result: {}", result),
None => println!("Cannot divide by zero"),
}
match parse_number("42") {
Ok(num) => println!("Parsed number: {}", num),
Err(e) => println!("Parse error: {}", e),
}
}
Advanced Function Features
// Generic functions
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
// Function pointers
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(f(arg))
}
// Closures (anonymous functions)
let closure = |x: i32| -> i32 { x * 2 };
let closure_inferred = |x| x * 2; // Type inference
// Higher-order functions
fn apply_to_numbers(numbers: &[i32], f: fn(i32) -> i32) -> Vec<i32> {
numbers.iter().map(|&x| f(x)).collect()
}
fn main() {
let numbers = [1, 2, 3, 4, 5];
let biggest = largest(&numbers);
println!("Largest number: {}", biggest);
let result = do_twice(add_one, 5);
println!("Result: {}", result); // 7
let doubled = apply_to_numbers(&numbers, |x| x * 2);
println!("Doubled: {:?}", doubled); // [2, 4, 6, 8, 10]
}
Method Syntax and Associated Functions
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Associated function (like static method)
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
// Method - takes &self
fn area(&self) -> u32 {
self.width * self.height
}
// Method - takes &mut self
fn scale(&mut self, factor: u32) {
self.width *= factor;
self.height *= factor;
}
// Method - takes self (consumes)
fn into_tuple(self) -> (u32, u32) {
(self.width, self.height)
}
}
fn main() {
let square = Rectangle::square(10);
println!("Square area: {}", square.area());
let mut rect = Rectangle { width: 30, height: 50 };
println!("Area: {}", rect.area());
rect.scale(2);
println!("Scaled area: {}", rect.area());
let (w, h) = rect.into_tuple();
println!("Dimensions: {}x{}", w, h);
}
Common Pitfalls
- Forgetting semicolon in functions that return
() - Mixing up when to use
&,&mut, or take ownership - Returning references to local variables
- Not handling all cases in functions returning Option/Result
- Confusing associated functions with methods
Modules in Rust
Modules help organize code into logical units and control privacy. Rust's module system includes modules, crates, and workspaces for large-scale code organization.
Basic Module Structure
// Declare a module
mod math {
// Public function
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// Private function (default)
fn subtract(a: i32, b: i32) -> i32 {
a - b
}
// Nested module
pub mod advanced {
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
}
}
fn main() {
// Use module functions
let sum = math::add(5, 3);
println!("5 + 3 = {}", sum);
let product = math::advanced::multiply(4, 5);
println!("4 * 5 = {}", product);
// This would error - subtract is private:
// math::subtract(10, 5);
}
Module Files and Directories
// File structure:
// src/
// main.rs
// math.rs
// math/
// advanced.rs
// In main.rs:
mod math; // This tells Rust to look for math.rs or math/mod.rs
fn main() {
math::add(5, 3);
math::advanced::multiply(4, 5);
}
// In math.rs:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub mod advanced; // Declare advanced module
// In math/advanced.rs:
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
pub fn divide(a: i32, b: i32) -> Option<i32> {
if b == 0 {
None
} else {
Some(a / b)
}
}
Use Declarations and Imports
mod math {
pub mod basic {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
pub mod advanced {
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
}
}
// Bring modules into scope
use math::basic::add;
use math::advanced::multiply;
// Rename imports
use math::advanced::multiply as mult;
// Import multiple items
use math::basic::{add, /* subtract */}; // subtract would be private
// Import all public items (use sparingly)
// use math::advanced::*;
fn main() {
// Now we can use the functions directly
let sum = add(5, 3);
let product = multiply(4, 5);
let also_product = mult(4, 5);
println!("Sum: {}, Product: {}, Also: {}", sum, product, also_product);
}
Privacy and Visibility
mod outer {
pub mod inner {
pub fn public_function() {
println!("This is public");
}
pub(crate) fn crate_visible() {
println!("Visible within crate");
}
fn private_function() {
println!("This is private");
}
// Function that uses private function
pub fn public_calls_private() {
println!("Public function calling private:");
private_function();
}
}
// Can access sibling module's private items
pub fn access_inner() {
inner::private_function(); // This works within same parent module
}
}
fn main() {
outer::inner::public_function();
outer::inner::public_calls_private();
outer::access_inner();
// These would error:
// outer::inner::private_function(); // private
// outer::inner::crate_visible(); // crate-visible, but we're in same crate
}
Advanced Module Patterns
// Prelude pattern for common imports
mod prelude {
pub use std::format;
pub use std::string::ToString;
pub use crate::math::*;
}
// Re-exporting (pub use)
mod math {
pub mod basic {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
// Re-export to make add available at math level
pub use basic::add;
}
// Now we can use math::add directly
use math::add;
// Inline modules with attributes
#[cfg(test)]
mod tests {
use super::*; // Import items from parent module
#[test]
fn test_addition() {
assert_eq!(add(2, 2), 4);
}
}
// Module with external code
#[path = "custom_path/math_utils.rs"]
mod math_utils;
fn main() {
let result = add(5, 3);
println!("5 + 3 = {}", result);
}
Crate Structure
// Typical crate structure:
// Cargo.toml
// src/
// lib.rs // Library crate root
// main.rs // Binary crate root
// math.rs // Module
// math/ // Module directory
// advanced.rs
// utils/ // Another module
// mod.rs // Module declaration file
// In lib.rs:
pub mod math;
pub mod utils;
// Re-export commonly used items
pub use math::basic::add;
pub use math::advanced::{multiply, divide};
// Library API
pub fn compute(a: i32, b: i32) -> i32 {
multiply(add(a, b), 2)
}
// In main.rs (if binary crate):
use my_crate::compute; // Assuming crate name is "my_crate"
fn main() {
let result = compute(3, 4);
println!("Result: {}", result); // (3+4)*2 = 14
}
Common Pitfalls
- Forgetting to make items
pubfor external use - Circular module dependencies
- Confusing module file structure (mod.rs vs named files)
- Overusing
pub usewhich can make API unclear - Not organizing code into modules as project grows
File Input/Output in Rust
Rust provides comprehensive file I/O operations through the std::fs and std::io modules. File operations return Result types that must be handled.
Basic File Operations
use std::fs;
use std::io::{self, Write};
use std::path::Path;
fn main() -> io::Result<()> {
// Writing to a file
fs::write("example.txt", "Hello, File!\nThis is line 2\nNumber: 42")?;
println!("File written successfully");
// Reading from a file
let content = fs::read_to_string("example.txt")?;
println!("File content:\n{}", content);
// Reading as bytes
let bytes = fs::read("example.txt")?;
println!("File size: {} bytes", bytes.len());
// Check if file exists
if Path::new("example.txt").exists() {
println!("File exists");
}
Ok(())
}
Working with File Handles
use std::fs::File;
use std::io::{BufReader, BufRead, BufWriter};
fn main() -> io::Result<()> {
// Open file for writing
let mut file = File::create("output.txt")?;
writeln!(&mut file, "Hello, File!")?;
writeln!(&mut file, "This is line 2")?;
file.write_all(b"Binary data\n")?;
// Open file for reading
let file = File::open("output.txt")?;
let reader = BufReader::new(file);
// Read line by line
for line in reader.lines() {
println!("{}", line?);
}
// Append to file
let mut file = fs::OpenOptions::new()
.append(true)
.open("output.txt")?;
writeln!(&mut file, "Appended line")?;
Ok(())
}
Different File Opening Modes
use std::fs::OpenOptions;
fn main() -> io::Result<()> {
// Various file opening modes
let file1 = File::create("new_file.txt")?; // Create or truncate
let file2 = File::open("existing_file.txt")?; // Read only
// Using OpenOptions for more control
let file3 = OpenOptions::new()
.read(true)
.write(true)
.create(true) // Create if doesn't exist
.open("data.txt")?;
let file4 = OpenOptions::new()
.append(true)
.open("log.txt")?; // Append mode
let file5 = OpenOptions::new()
.write(true)
.create_new(true) // Only create if doesn't exist
.open("brand_new.txt")?;
// Example: Append to log file
let mut log_file = OpenOptions::new()
.create(true)
.append(true)
.open("application.log")?;
writeln!(&mut log_file, "New log entry")?;
Ok(())
}
Reading Different Data Types
use std::io::{BufRead, BufReader};
fn read_mixed_data() -> io::Result<()> {
// Write mixed data
let data = "John Doe 25 85.5\nJane Smith 30 92.0\n";
fs::write("data.txt", data)?;
// Read and parse mixed data types
let file = File::open("data.txt")?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() == 4 {
let first_name = parts[0];
let last_name = parts[1];
let age: u32 = parts[2].parse().unwrap_or(0);
let score: f64 = parts[3].parse().unwrap_or(0.0);
println!("{} {}, Age: {}, Score: {}",
first_name, last_name, age, score);
}
}
Ok(())
}
fn read_structured_data() -> io::Result<()> {
// Using serde for structured data (with serde_json crate)
// #[derive(Serialize, Deserialize)]
// struct Person {
// name: String,
// age: u32,
// scores: Vec<f64>,
// }
// let person = Person { ... };
// let json = serde_json::to_string(&person)?;
// fs::write("person.json", json)?;
// let content = fs::read_to_string("person.json")?;
// let person: Person = serde_json::from_str(&content)?;
Ok(())
}
File System Operations
use std::fs;
use std::path::Path;
fn file_system_operations() -> io::Result<()> {
// Create directory
fs::create_dir("my_dir")?;
fs::create_dir_all("parent/child/grandchild")?; // Create all directories in path
// Check file metadata
let metadata = fs::metadata("example.txt")?;
println!("File size: {} bytes", metadata.len());
println!("Is file: {}", metadata.is_file());
println!("Is directory: {}", metadata.is_dir());
// Copy file
fs::copy("example.txt", "example_copy.txt")?;
// Rename/move file
fs::rename("example_copy.txt", "renamed.txt")?;
// Remove file
fs::remove_file("renamed.txt")?;
// Remove directory (must be empty)
fs::remove_dir("my_dir")?;
// Remove directory and all contents
fs::remove_dir_all("parent")?;
// List directory contents
for entry in fs::read_dir(".")? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
println!("File: {}", path.display());
} else if path.is_dir() {
println!("Directory: {}", path.display());
}
}
Ok(())
}
Error Handling in File I/O
use std::io;
fn robust_file_operations() -> io::Result<()> {
// Handling file not found gracefully
let content = fs::read_to_string("nonexistent.txt")
.unwrap_or_else(|_| "File not found".to_string());
println!("Content: {}", content);
// Using match for different error cases
match fs::read_to_string("important.txt") {
Ok(content) => println!("File content: {}", content),
Err(e) if e.kind() == io::ErrorKind::NotFound => {
println!("File not found, creating default...");
fs::write("important.txt", "Default content")?;
}
Err(e) => return Err(e), // Propagate other errors
}
// Using Result combinators
let result = fs::read_to_string("config.txt")
.and_then(|content| {
if content.is_empty() {
Err(io::Error::new(io::ErrorKind::InvalidData, "Empty file"))
} else {
Ok(content)
}
});
match result {
Ok(config) => println!("Config: {}", config),
Err(e) => println!("Error reading config: {}", e),
}
Ok(())
}
// Function that returns Result
fn read_config() -> io::Result<String> {
let content = fs::read_to_string("config.txt")?;
if content.trim().is_empty() {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Empty config"));
}
Ok(content)
}
Common Pitfalls
- Not handling I/O errors (using
unwrap()instead of proper error handling) - Forgetting that file paths are relative to current working directory
- Not closing files explicitly (Rust does this automatically, but be mindful of scope)
- Assuming files exist without checking
- Not using buffered I/O for large files
Standard Library Introduction
Rust's standard library (std) provides essential functionality for Rust programs. It includes types for I/O, collections, concurrency, and many utilities.
Key Standard Library Components
// Commonly used standard library modules
use std::collections::{HashMap, HashSet, VecDeque};
use std::io::{self, Read, Write, BufRead};
use std::fs;
use std::path::Path;
use std::time::{Duration, Instant};
use std::thread;
use std::sync::{Arc, Mutex};
fn main() {
// Option and Result - fundamental for error handling
let some_value: Option<i32> = Some(42);
let none_value: Option<i32> = None;
let ok_result: Result<i32, &str> = Ok(42);
let err_result: Result<i32, &str> = Err("Something went wrong");
// String and str
let owned_string = String::from("Hello");
let string_slice: &str = "World";
// Vectors and arrays
let vector = vec![1, 2, 3, 4, 5];
let array = [1, 2, 3, 4, 5];
println!("Standard library types are ready to use!");
}
Essential Traits and Types
use std::fmt;
use std::ops::Add;
// Common traits
#[derive(Debug, Clone, PartialEq, Eq)] // Automatically implement traits
struct Point {
x: i32,
y: i32,
}
// Manual implementation of Display trait
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
// Implementing operator overloading
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
println!("Debug: {:?}", p1); // Uses Debug trait
println!("Display: {}", p1); // Uses Display trait
println!("Sum: {}", p1 + p2); // Uses Add trait
// Clone trait
let p3 = p1.clone();
println!("Cloned: {:?}", p3);
// PartialEq trait
println!("Equal: {}", p1 == p3); // true
}
Common Utility Functions
use std::mem;
fn utility_functions() {
// Memory utilities
let x = 42;
println!("Size of i32: {}", mem::size_of::<i32>());
println!("Size of String: {}", mem::size_of::<String>());
// Take ownership and return it (moves value)
let s = String::from("hello");
let same_s = mem::take(&mut s.clone()); // Note: needs mutable reference
// Replace value
let mut v = vec![1, 2, 3];
let old_v = mem::replace(&mut v, vec![4, 5, 6]);
println!("Old: {:?}, New: {:?}", old_v, v);
// Convert to raw parts
let s = String::from("hello");
let (ptr, len, cap) = s.into_raw_parts();
// Can reconstruct string from raw parts
let _reconstructed = unsafe { String::from_raw_parts(ptr, len, cap) };
}
// Environment and program information
fn program_info() {
// Command line arguments
let args: Vec<String> = std::env::args().collect();
println!("Program name: {}", args[0]);
// Environment variables
if let Ok(path) = std::env::var("PATH") {
println!("PATH: {}", path);
}
// Current directory
if let Ok(current_dir) = std::env::current_dir() {
println!("Current directory: {}", current_dir.display());
}
}
Error Handling Utilities
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct CustomError {
message: String,
}
impl fmt::Display for CustomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "CustomError: {}", self.message)
}
}
impl Error for CustomError {}
fn might_fail(should_fail: bool) -> Result<String, CustomError> {
if should_fail {
Err(CustomError {
message: "It failed!".to_string(),
})
} else {
Ok("Success!".to_string())
}
}
fn error_handling_examples() -> Result<(), Box<dyn Error>> {
// Using ? operator for error propagation
let content = std::fs::read_to_string("file.txt")?;
// Converting between error types
let number = "42".parse::<i32>()
.map_err(|e| CustomError { message: e.to_string() })?;
// Combinator methods
let result = might_fail(false)
.and_then(|s| Ok(s + " And more!"))
.or_else(|_| Ok("Default value".to_string()));
println!("Result: {}", result?);
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
error_handling_examples()?;
Ok(())
}
Standard Library Organization
/*
Standard Library Organization:
core/ - No-std components (always available)
alloc/ - Heap allocation types (needs allocator)
std/ - Full standard library
Main modules:
- std::collections - HashMap, Vec, String, etc.
- std::io - Input/output operations
- std::fs - File system operations
- std::path - Path manipulation
- std::env - Environment variables
- std::process - Process management
- std::thread - Concurrency
- std::sync - Synchronization primitives
- std::time - Time operations
- std::net - Networking
- std::fmt - Formatting
- std::default - Default values
- std::convert - Type conversions
- std::ops - Operator overloading
- std::cmp - Comparisons
*/
// No-std programming (for embedded systems)
// #![no_std]
// use core::prelude::v1::*;
fn main() {
println!("Standard library provides batteries-included functionality");
}
Common Pitfalls
- Not understanding the difference between core, alloc, and std
- Forgetting to handle Result and Option types properly
- Using unwrap() in production code instead of proper error handling
- Not leveraging standard library traits for custom types
- Reimplementing functionality that's already in the standard library
Standard Library Collections
Rust's standard library provides efficient, generic collections for storing and manipulating data. Each collection has different performance characteristics and use cases.
Vector (Vec<T>)
fn vector_examples() {
// Creating vectors
let mut vec1 = Vec::new();
vec1.push(1);
vec1.push(2);
vec1.push(3);
let vec2 = vec![1, 2, 3, 4, 5]; // Macro syntax
let vec3 = Vec::with_capacity(10); // Pre-allocate
// Accessing elements
println!("First: {}", vec2[0]); // Panics if out of bounds
println!("First: {:?}", vec2.get(0)); // Returns Option
// Iteration
for element in &vec2 { // Borrow - doesn't consume
println!("Element: {}", element);
}
for element in vec2 { // Consumes vector
println!("Element: {}", element);
}
// vec2 is no longer usable here
// Common operations
let mut numbers = vec![1, 2, 3, 4, 5];
numbers.pop(); // Remove last
numbers.insert(2, 99); // Insert at index
numbers.remove(1); // Remove at index
numbers.retain(|&x| x % 2 == 0); // Keep only even numbers
println!("Modified: {:?}", numbers);
}
String and String Slices
fn string_examples() {
// String (owned, growable)
let mut s1 = String::new();
s1.push_str("Hello");
s1.push(' ');
s1.push_str("World");
let s2 = "Initial content".to_string();
let s3 = String::from("From string literal");
// String slices (&str) - borrowed
let literal = "Hello World"; // This is &str
let slice = &s1[0..5]; // "Hello"
// Conversion between String and &str
let owned: String = slice.to_string();
let borrowed: &str = &owned;
// String operations
let text = String::from("Hello Rust!");
println!("Length: {}", text.len()); // Number of bytes
println!("Chars count: {}", text.chars().count()); // Number of characters
// Splitting
for word in text.split_whitespace() {
println!("Word: {}", word);
}
// Concatenation
let s4 = s1 + " " + &s2; // Note: s1 is moved
let s5 = format!("{} {}", s3, "concatenated");
println!("s4: {}, s5: {}", s4, s5);
}
HashMap and HashSet
use std::collections::{HashMap, HashSet};
fn hash_examples() {
// HashMap - key-value storage
let mut scores = HashMap::new();
scores.insert("Alice", 10);
scores.insert("Bob", 20);
scores.insert("Charlie", 30);
// Accessing values
if let Some(score) = scores.get("Alice") {
println!("Alice's score: {}", score);
}
// Update values
scores.insert("Alice", 25); // Overwrite
scores.entry("Bob").or_insert(50); // Insert if not exists
scores.entry("David").or_insert(40); // Insert new
// Iteration
for (name, score) in &scores {
println!("{}: {}", name, score);
}
// HashSet - unique values
let mut set1 = HashSet::new();
set1.insert(1);
set1.insert(2);
set1.insert(3);
set1.insert(2); // Duplicate - ignored
let set2: HashSet<_> = [3, 4, 5].iter().cloned().collect();
// Set operations
println!("Union: {:?}", set1.union(&set2));
println!("Intersection: {:?}", set1.intersection(&set2));
println!("Difference: {:?}", set1.difference(&set2));
println!("Set1: {:?}", set1);
}
Other Collections
use std::collections::{VecDeque, BinaryHeap, BTreeMap, BTreeSet};
fn other_collections() {
// VecDeque - double-ended queue
let mut deque = VecDeque::new();
deque.push_back(1); // Add to end
deque.push_front(0); // Add to front
deque.pop_back(); // Remove from end
deque.pop_front(); // Remove from front
// BinaryHeap - priority queue (max-heap by default)
let mut heap = BinaryHeap::new();
heap.push(3);
heap.push(1);
heap.push(5);
heap.push(2);
while let Some(max) = heap.pop() {
println!("Max: {}", max); // 5, 3, 2, 1
}
// BTreeMap - sorted map
let mut btree = BTreeMap::new();
btree.insert(3, "three");
btree.insert(1, "one");
btree.insert(2, "two");
// Keys are sorted
for (key, value) in &btree {
println!("{}: {}", key, value); // 1, 2, 3
}
// BTreeSet - sorted set
let mut btree_set = BTreeSet::new();
btree_set.insert(3);
btree_set.insert(1);
btree_set.insert(2);
println!("BTreeSet: {:?}", btree_set); // {1, 2, 3}
}
Collection Performance Characteristics
/*
Collection | Insert | Access | Search | Remove | Use Case
--------------|--------|--------|--------|--------|---------
Vec<T> | O(1)* | O(1) | O(n) | O(n) | General purpose, indexing
VecDeque<T> | O(1)* | O(1) | O(n) | O(n) | Queue, double-ended operations
LinkedList<T> | O(1) | O(n) | O(n) | O(1) | Frequent insert/remove at ends
HashMap<K,V> | O(1)* | O(1)* | O(1)* | O(1)* | Key-value lookups
BTreeMap<K,V> | O(log n)|O(log n)|O(log n)|O(log n)| Sorted key-value
HashSet<T> | O(1)* | O(1)* | O(1)* | O(1)* | Unique elements, membership
BTreeSet<T> | O(log n)|O(log n)|O(log n)|O(log n)| Sorted unique elements
BinaryHeap<T> | O(log n)|O(1) | O(n) | O(log n)| Priority queue
* = amortized constant time, depends on hash distribution
*/
fn performance_considerations() {
// Choose the right collection for your use case
// Vec: When you need indexing or are mostly adding to the end
let mut vec = Vec::with_capacity(1000); // Pre-allocate if size known
// HashMap: When you need fast key-based lookups
use std::collections::HashMap;
let mut map = HashMap::with_capacity(1000);
// VecDeque: When you need queue-like behavior (FIFO)
use std::collections::VecDeque;
let mut queue = VecDeque::new();
// BinaryHeap: When you need priority-based access
use std::collections::BinaryHeap;
let mut heap = BinaryHeap::new();
println!("Choose collections based on your access patterns");
}
Collection Methods and Patterns
fn collection_methods() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Functional programming style
let doubled: Vec<i32> = numbers.iter().map(|&x| x * 2).collect();
let evens: Vec<&i32> = numbers.iter().filter(|&&x| x % 2 == 0).collect();
let sum: i32 = numbers.iter().sum();
println!("Doubled: {:?}", doubled);
println!("Evens: {:?}", evens);
println!("Sum: {}", sum);
// Chaining operations
let result: Vec<i32> = numbers
.iter()
.filter(|&&x| x > 5)
.map(|&x| x * 3)
.collect();
println!("Filtered and mapped: {:?}", result);
// Using collect with type inference
let squared: Vec<i32> = numbers.iter().map(|&x| x * x).collect();
let as_hashset: HashSet<i32> = numbers.iter().cloned().collect();
let as_string: String = numbers.iter().map(|x| x.to_string()).collect();
// Partitioning
let (even, odd): (Vec<i32>, Vec<i32>) = numbers
.into_iter()
.partition(|&x| x % 2 == 0);
println!("Even: {:?}, Odd: {:?}", even, odd);
}
Common Pitfalls
- Using
[]indexing that can panic instead ofget() - Not pre-allocating capacity when size is known for Vec/HashMap
- Forgetting that HashMap keys need to implement Hash and Eq
- Using the wrong collection for the access pattern
- Not handling the possibility of hash collisions in performance-critical code
Standard Library Iterators
Iterators are a fundamental abstraction in Rust for processing sequences of elements. They are lazy, composable, and often zero-cost.
Basic Iterator Usage
fn basic_iterator_examples() {
let numbers = vec![1, 2, 3, 4, 5];
// Creating iterators
let iter1 = numbers.iter(); // Immutable references
let iter2 = numbers.iter_mut(); // Mutable references
let iter3 = numbers.into_iter(); // Takes ownership (consumes)
// Using iterators
for number in numbers.iter() {
println!("Number: {}", number);
}
// Collecting into new collections
let doubled: Vec<i32> = numbers.iter().map(|&x| x * 2).collect();
let as_string: String = numbers.iter().map(|x| x.to_string()).collect();
println!("Doubled: {:?}", doubled);
println!("As string: {}", as_string);
// numbers is still usable here because we used iter()
}
Common Iterator Adaptors
fn iterator_adaptors() {
let numbers = 1..=10; // Range is an iterator
// map - transform each element
let squares: Vec<i32> = numbers.clone().map(|x| x * x).collect();
// filter - keep elements that satisfy predicate
let evens: Vec<i32> = numbers.clone().filter(|x| x % 2 == 0).collect();
// take - take first n elements
let first_three: Vec<i32> = numbers.clone().take(3).collect();
// skip - skip first n elements
let after_three: Vec<i32> = numbers.clone().skip(3).collect();
// zip - combine two iterators
let pairs: Vec<(i32, i32)> = numbers.clone()
.zip(numbers.clone().map(|x| x * 2))
.collect();
// chain - combine two iterators sequentially
let chained: Vec<i32> = (1..=3).chain(7..=10).collect();
// enumerate - get (index, value) pairs
for (i, value) in numbers.clone().enumerate() {
println!("Index: {}, Value: {}", i, value);
}
println!("Squares: {:?}", squares);
println!("Evens: {:?}", evens);
println!("First three: {:?}", first_three);
println!("Pairs: {:?}", pairs);
println!("Chained: {:?}", chained);
}
Consumer Methods
fn consumer_methods() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// sum - sum all elements
let total: i32 = numbers.iter().sum();
// product - multiply all elements
let product: i32 = numbers.iter().product();
// count - count elements
let count = numbers.iter().count();
// min/max - find minimum/maximum
let min = numbers.iter().min();
let max = numbers.iter().max();
// find - find first element matching predicate
let first_even = numbers.iter().find(|&&x| x % 2 == 0);
// position - find position of element
let pos_of_5 = numbers.iter().position(|&&x| x == 5);
// all/any - check if all/any elements satisfy predicate
let all_positive = numbers.iter().all(|&&x| x > 0);
let any_negative = numbers.iter().any(|&&x| x < 0);
// fold - accumulate values
let sum_fold = numbers.iter().fold(0, |acc, &x| acc + x);
// for_each - execute closure for each element
numbers.iter().for_each(|x| println!("Value: {}", x));
println!("Total: {}, Count: {}", total, count);
println!("Min: {:?}, Max: {:?}", min, max);
println!("First even: {:?}", first_even);
println!("All positive: {}", all_positive);
println!("Sum with fold: {}", sum_fold);
}
Creating Custom Iterators
struct Counter {
current: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Counter {
Counter { current: 0, max }
}
}
// Implement Iterator trait
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.max {
self.current += 1;
Some(self.current)
} else {
None
}
}
}
// Implementing IntoIterator for custom types
struct MyCollection {
data: Vec<i32>,
}
impl IntoIterator for MyCollection {
type Item = i32;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.data.into_iter()
}
}
fn custom_iterator_examples() {
// Using custom iterator
let counter = Counter::new(5);
for num in counter {
println!("Counter: {}", num); // 1, 2, 3, 4, 5
}
// Using iterator methods on custom iterator
let sum: u32 = Counter::new(5).sum();
let doubled: Vec<u32> = Counter::new(3).map(|x| x * 2).collect();
println!("Sum: {}", sum); // 15
println!("Doubled: {:?}", doubled); // [2, 4, 6]
// MyCollection with IntoIterator
let collection = MyCollection { data: vec![1, 2, 3] };
for value in collection { // Uses into_iter automatically
println!("Value: {}", value);
}
}
Advanced Iterator Patterns
use std::iter::{once, repeat, empty};
fn advanced_iterator_patterns() {
// Creating iterators from functions
let ones = repeat(1).take(5); // 1, 1, 1, 1, 1
let empty_iter = empty::<i32>(); // No elements
let single = once(42); // Single element: 42
// Cycle - repeat iterator endlessly
let cycled: Vec<i32> = vec![1, 2, 3].into_iter().cycle().take(7).collect();
println!("Cycled: {:?}", cycled); // [1, 2, 3, 1, 2, 3, 1]
// Windows and chunks for slices
let data = [1, 2, 3, 4, 5];
for window in data.windows(3) {
println!("Window: {:?}", window); // [1,2,3], [2,3,4], [3,4,5]
}
for chunk in data.chunks(2) {
println!("Chunk: {:?}", chunk); // [1,2], [3,4], [5]
}
// Peekable iterator (look ahead without consuming)
let mut peekable = data.iter().peekable();
while let Some(&item) = peekable.peek() {
println!("Next item will be: {}", item);
peekable.next(); // Actually consume it
}
// Inspect - debug without affecting iterator
let sum: i32 = data.iter()
.inspect(|x| println!("Processing: {}", x))
.map(|&x| x * 2)
.inspect(|x| println!("Doubled: {}", x))
.sum();
println!("Final sum: {}", sum);
}
Iterator Performance
fn iterator_performance() {
// Iterators are often zero-cost abstractions
// The generated assembly can be as efficient as hand-written loops
let numbers: Vec<i32> = (1..=1000).collect();
// This iterator chain...
let sum: i32 = numbers
.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x)
.sum();
// ...is as efficient as this manual loop:
let mut manual_sum = 0;
for &x in &numbers {
if x % 2 == 0 {
manual_sum += x * x;
}
}
assert_eq!(sum, manual_sum);
// Lazy evaluation - nothing happens until you consume
let lazy_iter = (1..)
.map(|x| x * 2)
.filter(|x| x % 3 == 0)
.take(5);
// The above doesn't do any work until we collect
let result: Vec<i32> = lazy_iter.collect();
println!("Lazy result: {:?}", result); // [6, 12, 18, 24, 30]
// Using size hints for optimization
let iter = (1..100).filter(|x| x % 2 == 0);
println!("Size hint: {:?}", iter.size_hint()); // (0, Some(99))
}
// Benchmarking iterator vs loop (conceptual)
/*
In practice, iterators are often as fast or faster than loops because:
- They can be optimized better by the compiler
- They avoid bounds checks in many cases
- They enable vectorization opportunities
*/
Common Pitfalls
- Forgetting to consume iterators (they're lazy)
- Using
into_iter()when you meantiter() - Not leveraging iterator adaptors and going back to loops unnecessarily
- Creating intermediate collections when not needed
- Ignoring iterator size hints for optimization opportunities
Structs in Rust
Structs are custom data types that let you name and package together multiple related values. Rust has three struct variants: classic, tuple, and unit structs.
Basic Struct Definition and Usage
// Classic struct with named fields
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
// Tuple struct (like a named tuple)
struct Color(i32, i32, i32);
// Unit struct (no fields)
struct Marker;
fn main() {
// Creating struct instances
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// Accessing fields
println!("User: {}, Email: {}", user1.username, user1.email);
// Creating mutable struct
let mut user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername"),
active: true,
sign_in_count: 1,
};
user2.sign_in_count = 2; // Can modify because user2 is mutable
// Tuple struct usage
let black = Color(0, 0, 0);
println!("Black: ({}, {}, {})", black.0, black.1, black.2);
// Unit struct usage
let marker = Marker;
}
Struct Methods and Associated Functions
struct Rectangle {
width: u32,
height: u32,
}
// Implementation block for methods
impl Rectangle {
// Method - takes &self (immutable reference to self)
fn area(&self) -> u32 {
self.width * self.height
}
// Method - takes &mut self (mutable reference)
fn scale(&mut self, factor: u32) {
self.width *= factor;
self.height *= factor;
}
// Method - takes self (consumes the struct)
fn into_tuple(self) -> (u32, u32) {
(self.width, self.height)
}
// Associated function (no self parameter) - like static method
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
// Method with parameters
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let mut rect = Rectangle { width: 30, height: 50 };
// Call methods
println!("Area: {}", rect.area());
rect.scale(2);
println!("Scaled area: {}", rect.area());
let small = Rectangle { width: 10, height: 10 };
println!("Can hold small? {}", rect.can_hold(&small));
// Call associated function
let square = Rectangle::square(25);
println!("Square area: {}", square.area());
// Consume the struct
let dimensions = rect.into_tuple();
println!("Dimensions: {}x{}", dimensions.0, dimensions.1);
// rect is no longer usable here
}
Struct Update Syntax and Patterns
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn struct_patterns() {
let user1 = User {
email: String::from("user1@example.com"),
username: String::from("user1"),
active: true,
sign_in_count: 1,
};
// Struct update syntax
let user2 = User {
email: String::from("user2@example.com"),
username: String::from("user2"),
..user1 // Use the rest of the fields from user1
};
// Destructuring structs
let User { username, email, active, .. } = user2;
println!("User: {}, Email: {}, Active: {}", username, email, active);
// Destructuring in function parameters
fn print_user_info(User { username, email, .. }: &User) {
println!("{} - {}", username, email);
}
print_user_info(&user1);
// Pattern matching with structs
match user1 {
User { username, active: true, .. } => {
println!("Active user: {}", username);
}
User { username, active: false, .. } => {
println!("Inactive user: {}", username);
}
}
}
Generic Structs
// Generic struct with one type parameter
struct Point<T> {
x: T,
y: T,
}
// Generic struct with multiple type parameters
struct Pair<T, U> {
first: T,
second: U,
}
// Implementation for generic struct
impl<T> Point<T> {
fn new(x: T, y: T) -> Point<T> {
Point { x, y }
}
fn x(&self) -> &T {
&self.x
}
}
// Implementation with trait bounds
impl<T: PartialEq> Point<T> {
fn is_equal(&self, other: &Point<T>) -> bool {
self.x == other.x && self.y == other.y
}
}
// Specialized implementation for specific type
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
// Using generic structs with different types
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };
let string_pair = Pair { first: "hello", second: "world" };
println!("Integer point: ({}, {})", integer_point.x, integer_point.y);
println!("Float point distance: {}", float_point.distance_from_origin());
println!("String pair: {} {}", string_pair.first, string_pair.second);
}
Advanced Struct Patterns
use std::fmt;
// Struct with lifetime parameters
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn new(text: &'a str) -> ImportantExcerpt<'a> {
ImportantExcerpt {
part: text,
}
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
// Newtype pattern (wrapper around existing type)
struct Meters(f64);
impl Meters {
fn to_kilometers(&self) -> f64 {
self.0 / 1000.0
}
}
// Implementing traits for structs
impl fmt::Display for Meters {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} meters", self.0)
}
}
fn advanced_struct_examples() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let excerpt = ImportantExcerpt::new(first_sentence);
println!("Excerpt: {}", excerpt.part);
let distance = Meters(5000.0);
println!("{} = {} kilometers", distance, distance.to_kilometers());
// Builder pattern for complex struct construction
let user = UserBuilder::new()
.username("john_doe")
.email("john@example.com")
.build();
println!("Built user: {}", user.username);
}
// Builder pattern implementation
struct UserBuilder {
username: Option<String>,
email: Option<String>,
}
impl UserBuilder {
fn new() -> UserBuilder {
UserBuilder {
username: None,
email: None,
}
}
fn username(mut self, username: &str) -> UserBuilder {
self.username = Some(username.to_string());
self
}
fn email(mut self, email: &str) -> UserBuilder {
self.email = Some(email.to_string());
self
}
fn build(self) -> User {
User {
username: self.username.unwrap_or_else(|| "unknown".to_string()),
email: self.email.unwrap_or_else(|| "unknown@example.com".to_string()),
sign_in_count: 0,
active: true,
}
}
}
Common Pitfalls
- Forgetting to make struct fields public when needed (
pub) - Confusing when to use
&selfvsselfin methods - Not handling lifetimes properly in structs with references
- Overusing generic parameters when not needed
- Forgetting to derive or implement common traits (Debug, Clone, etc.)
Enums in Rust
Enums (enumerations) allow you to define a type by enumerating its possible variants. Rust enums are much more powerful than in many other languages.
Basic Enum Definition and Usage
// Simple enum
enum Color {
Red,
Green,
Blue,
}
// Enum with data
enum Message {
Quit, // No data
Move { x: i32, y: i32 }, // Named fields like struct
Write(String), // Single piece of data
ChangeColor(i32, i32, i32), // Multiple pieces of data
}
// Enum with methods
impl Message {
fn call(&self) {
// Method body would be defined here
println!("Message called");
}
}
fn main() {
// Creating enum instances
let red = Color::Red;
let green = Color::Green;
let msg1 = Message::Write(String::from("hello"));
let msg2 = Message::Move { x: 10, y: 20 };
let msg3 = Message::ChangeColor(255, 0, 0);
// Using methods on enums
msg1.call();
// Pattern matching with enums
match red {
Color::Red => println!("It's red!"),
Color::Green => println!("It's green!"),
Color::Blue => println!("It's blue!"),
}
process_message(msg2);
}
fn process_message(msg: Message) {
match msg {
Message::Quit => {
println!("The Quit variant has no data");
}
Message::Move { x, y } => {
println!("Move to coordinates ({}, {})", x, y);
}
Message::Write(text) => {
println!("Text message: {}", text);
}
Message::ChangeColor(r, g, b) => {
println!("Change color to RGB({}, {}, {})", r, g, b);
}
}
}
The Option Enum
// Option is a built-in enum for handling optional values
// enum Option<T> {
// Some(T),
// None,
// }
fn option_examples() {
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;
// Using match with Option
match some_number {
Some(value) => println!("Got a value: {}", value),
None => println!("Got nothing"),
}
// Using if let with Option
if let Some(value) = some_string {
println!("The string is: {}", value);
}
// Option combinators
let number = Some(5);
let doubled = number.map(|x| x * 2); // Some(10)
let filtered = number.filter(|&x| x > 3); // Some(5)
let or_else = absent_number.or(Some(42)); // Some(42)
// Unwrapping options (use carefully)
let value = some_number.unwrap(); // 5
// let panic = absent_number.unwrap(); // This would panic!
let safe_value = absent_number.unwrap_or(0); // 0 (default value)
let or_else_value = absent_number.unwrap_or_else(|| 42); // 42
println!("Doubled: {:?}", doubled);
println!("Safe value: {}", safe_value);
}
The Result Enum
// Result is a built-in enum for error handling
// enum Result<T, E> {
// Ok(T),
// Err(E),
// }
fn result_examples() -> Result<(), String> {
let success: Result<i32, &str> = Ok(42);
let failure: Result<i32, &str> = Err("Something went wrong");
// Using match with Result
match success {
Ok(value) => println!("Success: {}", value),
Err(e) => println!("Error: {}", e),
}
// Using if let with Result
if let Err(error) = failure {
println!("Failed with: {}", error);
}
// Result combinators
let mapped = success.map(|x| x * 2); // Ok(84)
let and_then = success.and_then(|x| Ok(x + 10)); // Ok(52)
let or_else = failure.or_else(|_| Ok(0)); // Ok(0)
// The ? operator for error propagation
let content = read_file("example.txt")?;
println!("File content: {}", content);
// Converting between Option and Result
let some_value = Some(42);
let result: Result<i32, &str> = some_value.ok_or("No value");
Ok(())
}
fn read_file(filename: &str) -> Result<String, String> {
// Simulate file reading
if filename == "example.txt" {
Ok("File content".to_string())
} else {
Err("File not found".to_string())
}
}
// Using Result in main
fn main() -> Result<(), Box<dyn std::error::Error>> {
result_examples()?;
Ok(())
}
Advanced Enum Patterns
use std::fmt;
// Generic enums
enum MyResult<T, E> {
Success(T),
Failure(E),
}
// Enum with methods
impl<T, E> MyResult<T, E> {
fn is_success(&self) -> bool {
match self {
MyResult::Success(_) => true,
MyResult::Failure(_) => false,
}
}
fn unwrap(self) -> T {
match self {
MyResult::Success(value) => value,
MyResult::Failure(_) => panic!("Called unwrap on Failure"),
}
}
}
// Pattern matching advanced features
fn advanced_pattern_matching() {
let complex_enum = Message::Move { x: 10, y: 20 };
// Matching with guards
match complex_enum {
Message::Move { x, y } if x == 0 && y == 0 => {
println!("At origin");
}
Message::Move { x, y } if x > 0 && y > 0 => {
println!("In first quadrant: ({}, {})", x, y);
}
Message::Move { x, y } => {
println!("At coordinates: ({}, {})", x, y);
}
_ => {}
}
// @ bindings - bind to variable while also matching
let some_value = Some(15);
match some_value {
Some(x @ 1..=10) => println!("Small number: {}", x),
Some(x @ 11..=100) => println!("Medium number: {}", x),
Some(x) => println!("Large number: {}", x),
None => println!("No number"),
}
}
// Implementing traits for enums
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Color::Red => write!(f, "Red"),
Color::Green => write!(f, "Green"),
Color::Blue => write!(f, "Blue"),
}
}
}
fn main() {
let color = Color::Red;
println!("The color is: {}", color);
let my_result: MyResult<i32, &str> = MyResult::Success(42);
println!("Is success: {}", my_result.is_success());
advanced_pattern_matching();
}
Common Enum Use Cases
// State machines
enum TrafficLight {
Red,
Yellow,
Green,
}
impl TrafficLight {
fn next(&self) -> TrafficLight {
match self {
TrafficLight::Red => TrafficLight::Green,
TrafficLight::Yellow => TrafficLight::Red,
TrafficLight::Green => TrafficLight::Yellow,
}
}
fn duration(&self) -> u32 {
match self {
TrafficLight::Red => 30,
TrafficLight::Yellow => 5,
TrafficLight::Green => 45,
}
}
}
// AST (Abstract Syntax Tree) nodes
enum Expr {
Number(i32),
Add(Box<Expr>, Box<Expr>),
Multiply(Box<Expr>, Box<Expr>),
Variable(String),
}
impl Expr {
fn evaluate(&self, vars: &std::collections::HashMap<String, i32>) -> i32 {
match self {
Expr::Number(n) => *n,
Expr::Add(left, right) => left.evaluate(vars) + right.evaluate(vars),
Expr::Multiply(left, right) => left.evaluate(vars) * right.evaluate(vars),
Expr::Variable(name) => *vars.get(name).unwrap_or(&0),
}
}
}
// Command pattern
enum Command {
Move { x: i32, y: i32 },
Attack { target: String },
Wait,
Quit,
}
impl Command {
fn execute(&self) {
match self {
Command::Move { x, y } => println!("Moving to ({}, {})", x, y),
Command::Attack { target } => println!("Attacking {}", target),
Command::Wait => println!("Waiting..."),
Command::Quit => println!("Quitting"),
}
}
}
fn enum_use_cases() {
let mut light = TrafficLight::Red;
for _ in 0..5 {
println!("Light: {:?}, Duration: {}s", light, light.duration());
light = light.next();
}
// AST example: (2 + 3) * 4
let expr = Expr::Multiply(
Box::new(Expr::Add(
Box::new(Expr::Number(2)),
Box::new(Expr::Number(3)),
)),
Box::new(Expr::Number(4)),
);
let result = expr.evaluate(&std::collections::HashMap::new());
println!("Expression result: {}", result); // 20
// Command pattern
let commands = vec![
Command::Move { x: 10, y: 20 },
Command::Attack { target: "enemy".to_string() },
Command::Wait,
Command::Quit,
];
for command in commands {
command.execute();
}
}
Common Pitfalls
- Forgetting to handle all enum variants in match statements
- Using
unwrap()on Option/Result without considering failure cases - Not leveraging the ? operator for error propagation
- Creating overly complex enums when separate types would be better
- Forgetting that enum variants are namespaced under the enum type
Traits in Rust
Traits define shared behavior that types can implement. They're similar to interfaces in other languages but more powerful, supporting associated types, default implementations, and more.
Basic Trait Definition and Implementation
// Define a trait
trait Summary {
fn summarize(&self) -> String;
// Default implementation
fn summarize_author(&self) -> String {
String::from("(Unknown author)")
}
// Method with default implementation that uses other methods
fn summarize_with_author(&self) -> String {
format!("{} by {}", self.summarize(), self.summarize_author())
}
}
// Implement the trait for a type
struct NewsArticle {
headline: String,
location: String,
author: String,
content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
// Override the default implementation
fn summarize_author(&self) -> String {
self.author.clone()
}
}
// Another type implementing the same trait
struct Tweet {
username: String,
content: String,
reply: bool,
retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn main() {
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from("The Pittsburgh Penguins once again are the best hockey team in the NHL."),
};
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};
println!("Article summary: {}", article.summarize());
println!("Tweet summary: {}", tweet.summarize());
println!("Article with author: {}", article.summarize_with_author());
}
Trait Bounds and Generic Functions
use std::fmt::Display;
// Function with trait bound
fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// Multiple trait bounds
fn notify_multiple<T: Summary + Display>(item: &T) {
println!("Item: {} - Summary: {}", item, item.summarize());
}
// Alternative syntax with where clause
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Summary,
{
println!("T: {}, U summary: {}", t, u.summarize());
42
}
// Returning types that implement traits
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}
// Using trait bounds with structs
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
fn trait_bounds_examples() {
let article = NewsArticle { /* ... */ };
notify(&article);
let pair = Pair::new(3, 4);
pair.cmp_display();
let summarizable = returns_summarizable();
println!("Returned: {}", summarizable.summarize());
}
Common Standard Library Traits
use std::fmt;
use std::ops::Add;
// Derivable traits
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Point {
x: i32,
y: i32,
}
// Manual implementation of Display
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
// Implementing operator overloading with Add trait
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
// Implementing From/Into traits for conversions
impl From<(i32, i32)> for Point {
fn from(pair: (i32, i32)) -> Self {
Point {
x: pair.0,
y: pair.1,
}
}
}
fn standard_traits_examples() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
// Using Debug trait
println!("Debug: {:?}", p1);
// Using Display trait
println!("Display: {}", p1);
// Using Add trait
let sum = p1 + p2;
println!("Sum: {}", sum);
// Using From trait
let p3 = Point::from((5, 6));
let p4: Point = (7, 8).into(); // Into is automatically implemented when From is
println!("From tuple: {}", p3);
println!("Into: {}", p4);
// Using Clone trait
let cloned = p1.clone();
println!("Cloned: {}", cloned);
// Using PartialEq trait
println!("Equal: {}", p1 == cloned);
}
Advanced Trait Features
// Associated types trait Iterator { type Item; // Associated type fn next(&mut self) -> Option<Self::Item>; } // Implementing iterator with associated type struct Counter { count: u32, } impl Counter { fn new() -> Counter { Counter { count: 0 } } } impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { if self.count < 5 { self.count += 1; Some(self.count) } else { None } } } // Generic traits with associated types trait Container<T> { fn contains(&self, item: &T) -> bool; } impl<T: PartialEq> Container<T> for Vec<T> { fn contains(&self, item: &T) -> bool { self.iter().any(|x| x == item) } } // Supertraits (trait that requires another trait) trait OutlinePrint: fmt::Display { fn outline_print(&self) { let output = self.to_string(); let len = output.len(); println!("{}", "*".repeat(len + 4)); println!("*{}*", " ".repeat(len + 2)); println!("* {} *", output); println!("*{}*", " ".repeat(len + 2)); println!("{}", "*".repeat(len + 4)); } } // Implement OutlinePrint for Point (requires Display) impl OutlinePrint for Point {} // Fully Qualified Syntax for Disambiguation trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn advanced_trait_examples() { let mut counter = Counter::new(); while let Some(num) = counter.next() { println!("Counter: {}", num); } let point = Point { x: 10, y: 20 }; point.outline_print(); let person = Human; person.fly(); // Calls Human::fly Pilot::fly(&person); // Calls Pilot::fly Wizard::fly(&person); // Calls Wizard::fly }Trait Objects and Dynamic Dispatch
// Trait objects for heterogeneous collections fn trait_objects_examples() { let article = NewsArticle { /* ... */ }; let tweet = Tweet { /* ... */ }; // Vector of trait objects - can store different types that implement Summary let summaries: Vec<Box<dyn Summary>> = vec![ Box::new(article), Box::new(tweet), ]; for item in summaries { println!("Summary: {}", item.summarize()); } } // Using trait objects in function parameters fn notify_dynamic(item: &dyn Summary) { println!("Notification: {}", item.summarize()); } // Trait objects with static dispatch vs dynamic dispatch fn static_dispatch<T: Summary>(item: &T) { // Monomorphization - compiler generates specific code for each type println!("{}", item.summarize()); } fn dynamic_dispatch(item: &dyn Summary) { // Dynamic dispatch - uses vtable at runtime println!("{}", item.summarize()); } // Object-safe traits (can be used as trait objects) trait ObjectSafe { fn method(&self); } // This trait is NOT object-safe because it has generic methods // trait NotObjectSafe { // fn generic_method<T>(&self, t: T); // } fn main() { let article = NewsArticle { /* ... */ }; let tweet = Tweet { /* ... */ }; notify_dynamic(&article); notify_dynamic(&tweet); static_dispatch(&article); static_dispatch(&tweet); }Common Pitfalls
- Forgetting to bring traits into scope when using their methods
- Creating trait bounds that are too restrictive or not restrictive enough
- Confusing static dispatch (generics) with dynamic dispatch (trait objects)
- Not understanding object safety rules for trait objects
- Overusing trait objects when generics would be more efficient
Advanced Rust Concepts
Advanced Rust features provide powerful tools for writing efficient, safe, and expressive code. These include smart pointers, async/await, unsafe Rust, and more.
Smart Pointers
use std::rc::Rc;
use std::sync::Arc;
use std::cell::RefCell;
use std::sync::Mutex;
fn smart_pointer_examples() {
// Box - heap allocation with single ownership
let boxed = Box::new(5);
println!("Boxed value: {}", *boxed);
// Rc - reference counting for multiple ownership (single-threaded)
let rc1 = Rc::new(String::from("hello"));
let rc2 = Rc::clone(&rc1);
println!("Rc count: {}", Rc::strong_count(&rc1));
// Arc - atomic reference counting (thread-safe)
let arc1 = Arc::new(String::from("world"));
let arc2 = Arc::clone(&arc1);
println!("Arc value: {}", *arc1);
// RefCell - interior mutability (runtime borrow checking)
let ref_cell = RefCell::new(42);
{
let mut borrow = ref_cell.borrow_mut();
*borrow += 1;
} // borrow goes out of scope here
println!("RefCell value: {}", ref_cell.borrow());
// Mutex - mutual exclusion (thread-safe interior mutability)
let mutex = Mutex::new(100);
{
let mut guard = mutex.lock().unwrap();
*guard += 1;
}
println!("Mutex value: {}", *mutex.lock().unwrap());
// Combining Rc and RefCell for multiple ownership with mutability
let shared_mutable = Rc::new(RefCell::new(0));
let clone1 = Rc::clone(&shared_mutable);
let clone2 = Rc::clone(&shared_mutable);
*clone1.borrow_mut() += 1;
*clone2.borrow_mut() += 1;
println!("Shared mutable: {}", *shared_mutable.borrow());
}
Async/Await
// Requires tokio or async-std runtime
// #[tokio::main]
// async fn main() {
// let result = fetch_data().await;
// println!("Result: {}", result);
// }
/*
async fn fetch_data() -> String {
// Simulate async operation
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
"Data fetched".to_string()
}
async fn process_multiple() {
// Join multiple async operations
let (result1, result2) = tokio::join!(
fetch_data(),
fetch_data()
);
println!("Results: {}, {}", result1, result2);
// Select between async operations
tokio::select! {
result = fetch_data() => println!("First completed: {}", result),
_ = tokio::time::sleep(tokio::time::Duration::from_secs(2)) => {
println!("Timeout reached");
}
}
}
*/
// For this example, we'll use a synchronous version
fn async_concepts() {
println!("Async/await enables writing asynchronous code that looks synchronous");
println!("It's built on top of futures and requires an async runtime");
}
Unsafe Rust
unsafe fn unsafe_examples() {
// Dereferencing raw pointers
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println!("r1 is: {}", *r1);
*r2 = 10;
println!("r2 is: {}", *r2);
}
// Calling unsafe functions
unsafe {
dangerous_function();
}
// Creating safe abstractions over unsafe code
let mut v = vec![1, 2, 3, 4, 5];
let (a, b) = split_at_mut(&mut v, 2);
println!("First part: {:?}", a);
println!("Second part: {:?}", b);
}
unsafe fn dangerous_function() {
println!("This is an unsafe function");
}
// Safe abstraction over unsafe code
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
let ptr = slice.as_mut_ptr();
assert!(mid <= len);
unsafe {
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
// Implementing unsafe traits
unsafe trait UnsafeTrait {
// trait methods
}
unsafe impl UnsafeTrait for i32 {
// implementation
}
fn main() {
unsafe {
unsafe_examples();
}
}
Advanced Pattern Matching
fn advanced_patterns() {
// Matching ranges
let x = 5;
match x {
1..=5 => println!("One through five"),
6 | 7 | 8 => println!("Six, seven, or eight"),
_ => println!("Something else"),
}
// Destructuring nested structures
struct Point {
x: i32,
y: i32,
}
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
ChangeColor(Color),
}
let msg = Message::ChangeColor(Color::Rgb(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("Change color to RGB({}, {}, {})", r, g, b);
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change color to HSV({}, {}, {})", h, s, v);
}
}
// @ bindings
let p @ Point { x: px, y: py } = Point { x: 10, y: 20 };
println!("Point: ({}, {})", px, py);
println!("Whole point: {:?}", p);
// Matching with guards
let num = Some(4);
match num {
Some(x) if x < 5 => println!("Less than five: {}", x),
Some(x) => println!("{}", x),
None => (),
}
// if let chains (Rust 1.63+)
let some_option = Some(5);
let another_option = Some(10);
// This would work in newer Rust versions:
// if let Some(x) = some_option && let Some(y) = another_option {
// println!("x = {}, y = {}", x, y);
// }
}
Macros
// Declarative macros (macro_rules!)
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
// Using the macro
fn macro_examples() {
let v = vec![1, 2, 3];
println!("Macro-generated vector: {:?}", v);
}
// Procedural macros require separate crate
/*
// In a procedural macro crate
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
let ast = syn::parse(input).unwrap();
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
gen.into()
}
*/
// Function-like macros
/*
macro_rules! sql {
($($arg:tt)*) => { /* ... */ };
}
// Usage: sql!(SELECT * FROM users WHERE id = 1);
*/
fn main() {
macro_examples();
}
Advanced Lifetime Patterns
// Lifetime elision rules
fn first_word(s: &str) -> &str {
// Compiler applies elision rules:
// 1. Each parameter gets its own lifetime
// 2. If there's exactly one input lifetime, it's assigned to all output lifetimes
// 3. If there are multiple input lifetimes but one is &self or &mut self,
// the lifetime of self is assigned to all output lifetimes
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
// Explicit lifetime annotations
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
// Lifetime elision works here
fn level(&self) -> i32 {
3
}
// Need explicit lifetime here
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
// Higher-ranked trait bounds (HRTB)
fn call_on_ref_zero<F>(f: F)
where
F: for<'a> Fn(&'a i32),
{
let zero = 0;
f(&zero);
}
// Static lifetime
fn static_lifetime_example() -> &'static str {
"I have a static lifetime"
}
fn lifetime_advanced() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
println!("Excerpt: {}", i.part);
let static_str = static_lifetime_example();
println!("Static string: {}", static_str);
call_on_ref_zero(|x| println!("x is: {}", x));
}
Common Pitfalls
- Using unsafe code without proper validation and documentation
- Creating memory leaks with Rc/RefCell cycles
- Not understanding async runtime requirements
- Overusing macros when functions would suffice
- Misunderstanding lifetime relationships in complex data structures