Chapter Chapter 1: Rust Overview and Environment Setup
Learning Goals
- Understand Rust’s design philosophy and core features
- Set up a standard Rust development environment
- Learn to use Cargo (Rust’s package manager)
- Understand how Rust code is compiled and run
1.1 Rust at a Glance
1.1.1 Why Rust?
Rust is a systems programming language started at Mozilla in 2006. The goal was to keep C/C++-level performance while fixing long-standing memory-safety problems.
#![allow(unused)] fn main() { // Rust’s philosophy: safety, concurrency, practicality // No garbage collector required // Memory safety enforced at compile time // Zero-cost abstractions (high-level code without runtime overhead) }
1.1.2 Core Features
Memory Safety
Rust prevents common memory bugs (null dereference, buffer overflow, data races, dangling pointers) by enforcing strict rules at compile time (ownership and borrowing). This avoids a runtime garbage collector while keeping performance high—especially valuable for systems programming.
#![allow(unused)] fn main() { // Prevent common memory bugs at compile time fn demonstrate_memory_safety() { let string = String::from("Hello"); let slice = &string; // borrow, do not move ownership println!("{}", slice); // let mut data = vec![1, 2, 3]; // let slice = &data; // immutable borrow // data.push(4); // compile error! violates borrowing rules } }
Zero-Cost Abstractions
Rust lets you write expressive, high-level code (generics, traits, iterators) without paying runtime cost. After compilation and optimization, the generated machine code can be as efficient as hand-written low-level loops.
#![allow(unused)] fn main() { // High-level style without performance penalty fn high_level_abstraction() { let numbers: Vec<i32> = (0..1000).collect(); let sum: i32 = numbers .iter() .filter(|&&x| x % 2 == 0) // keep even numbers .map(|&x| x * x) // square .sum(); // sum println!("Sum of squared even numbers: {}", sum); } }
The Ownership System
Rust’s key innovation is ownership: every value has a single owner, and when the owner goes out of scope, the value is automatically dropped. Ownership can be moved, or values can be borrowed via references (&T and &mut T). The compiler enforces these rules to prevent double-free, use-after-free, and invalid access.
#![allow(unused)] fn main() { // Ownership and borrowing ensure memory safety fn ownership_demo() { let data = vec![1, 2, 3]; let transferred = data; // move ownership to transferred // println!("{:?}", data); // compile error! data was moved println!("{:?}", transferred); let reference = &transferred; println!("Borrowed value: {:?}", reference); let another_ref = &transferred; println!("Two borrows: {:?}, {:?}", reference, another_ref); // You cannot create a mutable borrow while immutable borrows exist // let mut_ref = &mut transferred; // compile error! } }
1.2 Installing the Rust Toolchain
1.2.1 Installing via rustup
# Install Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Reload environment
source ~/.cargo/env
# Verify
rustc --version
cargo --version
# Update Rust
rustup update
# Show installed toolchains
rustup show
1.2.2 Toolchain Management
# List installed targets
rustup target list --installed
# Add targets
rustup target add x86_64-pc-windows-msvc
rustup target add x86_64-apple-darwin
rustup target add aarch64-unknown-linux-gnu
# Switch toolchain
rustup default stable
rustup default nightly
1.2.3 Recommended Development Tools
# Install common tools
cargo install cargo-watch # watch files and rebuild
cargo install cargo-audit # check dependency vulnerabilities
cargo install cargo-clippy # lints
cargo install rust-analyzer # language server
# VS Code extensions
# - rust-analyzer
# - Rust Test Explorer
# - CodeLLDB (debugger)
1.3 Cargo: Package Manager and Build Tool
1.3.1 Creating a Project
# Create a binary project
cargo new my_project
cd my_project
# Create a library project
cargo new --lib my_library
# Generate from a template
cargo generate --git https://github.com/rustwasm/wasm-pack-template
1.3.2 The Cargo.toml Manifest
# my_project/Cargo.toml
[package]
name = "my_project" # project name
version = "0.1.0" # version
edition = "2021" # Rust edition
authors = ["Your Name <email@example.com>"]
license = "MIT"
description = "A sample Rust project"
repository = "https://github.com/user/my_project"
keywords = ["rust", "example", "demo"]
categories = ["development-tools"]
documentation = "https://docs.rs/my_project"
readme = "README.md"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.0", features = ["full"] }
rand = "0.8"
chrono = { version = "0.4", optional = true }
[dev-dependencies]
tempfile = "3.0"
mockall = "0.11"
[build-dependencies]
cc = "1.0"
[features]
default = ["json"]
json = ["serde_json"]
csv = ["serde_csv"]
chrono_time = ["chrono"]
1.3.3 Typical Project Layout
my_project/
├── src/
│ ├── main.rs
│ ├── lib.rs # optional
│ ├── mod1/
│ │ ├── mod.rs
│ │ └── submodule.rs
│ └── utils/
│ ├── mod.rs
│ └── helpers.rs
├── Cargo.toml
├── Cargo.lock # auto-generated
├── README.md
├── LICENSE
├── .gitignore
├── tests/
├── examples/
├── benches/
└── target/
├── debug/
└── release/
1.4 Your First Rust Program
1.4.1 Hello, World
// src/main.rs /* Multi-line comment This is our first Rust program */ fn main() { // println! is a macro (the ! indicates a macro) println!("Hello, Rust World!"); // variables and types let name = "Rust Developer"; let version = 1.0; let is_awesome = true; println!("Welcome {}! Rust version: {}", name, version); println!("Is Rust awesome? {}", is_awesome); // formatted output println!("{} is a {} programming language", "Rust", "modern"); println!( "{subject} {verb} {object}", subject = "Rust", verb = "is", object = "safe" ); // placeholders println!("Decimal: {}", 42); println!("Hex: {:#x}", 255); println!("Binary: {:#b}", 15); println!("Scientific: {}", 123.456789); // named arguments println!( "{language} was released in {year}!", language = "Rust", year = 2021 ); }
1.4.2 Build and Run
# Build and run (debug)
cargo run
# Build (debug)
cargo build
# Build (release, optimized)
cargo build --release
# Type-check only (fast)
cargo check
# Run an example
cargo run --example hello_world
# Run tests
cargo test
# Run benchmarks
cargo bench
1.4.3 Managing Dependencies
// src/main.rs - using external crates use rand::Rng; use serde_json::json; fn main() { let random_number = rand::thread_rng().gen_range(1..=100); println!("Random number: {}", random_number); let data = json!({ "name": "Alice", "age": 30, "skills": ["Rust", "Python", "JavaScript"] }); let json_string = serde_json::to_string_pretty(&data) .expect("JSON serialization failed"); println!("JSON data:\n{}", json_string); }
1.5 Mini Project: A Rust Dev Environment Setup Tool
1.5.1 Requirements
#![allow(unused)] fn main() { // Goals: // 1. Detect current environment state // 2. Install/update Rust toolchains // 3. Configure development tools // 4. Generate project templates // 5. Provide rollback if needed }
1.5.2 High-Level Design
use std::process; mod commands; mod utils; mod config; use commands::{EnvironmentDetector, ToolInstaller, TemplateGenerator}; use utils::{Logger, ErrorHandler}; use config::Settings; fn main() -> Result<(), Box<dyn std::error::Error>> { let logger = Logger::new("rustdev-setup"); logger.info("Starting environment setup"); let settings = Settings::load_from_file("config.toml")?; let detector = EnvironmentDetector::new(&logger); let environment = detector.detect()?; logger.info(format!("Detected environment: {:?}", environment)); let installer = ToolInstaller::new(&settings, &logger); installer.install_all(&environment)?; let generator = TemplateGenerator::new(&settings, &logger); generator.generate_templates()?; logger.info("Environment setup complete!"); Ok(()) }
1.6 Best Practices and Notes
1.6.1 Cargo Configuration Example
# ~/.cargo/config.toml
[toolchain]
channel = "stable"
targets = ["x86_64-unknown-linux-gnu"]
profile = "minimal"
[build]
target = "x86_64-unknown-linux-gnu"
[net]
git-fetch-with-cli = true
1.6.2 Quality Checks
#![allow(unused)] fn main() { use std::error::Error; fn run_quality_checks() -> Result<(), Box<dyn Error>> { std::process::Command::new("cargo") .args(["fmt", "--check"]) .status()?; std::process::Command::new("cargo") .args(["clippy"]) .status()?; std::process::Command::new("cargo") .args(["audit"]) .status()?; std::process::Command::new("cargo") .args(["test"]) .status()?; Ok(()) } }
1.7 Exercises
Exercise 1.1: Environment Detection
Create a simple tool that checks:
- Current operating system
- Rust version information
- Installed Cargo tools
Exercise 1.2: Project Template Generator
Design a scaffolding tool that can:
- Generate a standard Rust project layout
- Configure common dependencies
- Initialize a Git repository
Exercise 1.3: Toolchain Management
Build a tool that can:
- Manage multiple Rust toolchains
- Switch the default toolchain
- Install and manage targets
1.8 Chapter Summary
In this chapter you learned:
- Rust fundamentals: memory safety, ownership, zero-cost abstractions
- Environment setup: installing and managing toolchains with
rustup - Cargo workflow: project structure, dependencies, builds
- Practice project idea: a dev-environment setup tool
Key Takeaways
- Rust combines safety and performance via compile-time checks.
- Cargo is the standard workflow for building, testing, and managing dependencies.
- Hands-on practice is the fastest way to learn.
Next Steps
- Dive deeper into Rust syntax
- Learn variables and data types
- Understand ownership and borrowing in detail
- Start building small real-world projects
Practice is the best way to master Rust. See the next chapter for more!
Chapter 2: Variables, Data Types, and Control Flow
Learning objectives
- Master the basic syntax for declaring and using variables in Rust
- Understand the characteristics and typical use cases of Rust’s data types
- Use control-flow statements to express program logic
- Learn how to define and call functions
2.1 Variables and mutability
2.1.1 Basic variable bindings
In Rust, variables are declared with the let keyword and are immutable by default. This means that once a name is bound to a value, you can’t reassign it. This design improves safety and predictability by preventing accidental side effects. Rust can usually infer types automatically, but you can also annotate them explicitly (for example, let x: i32 = 5;).
#![allow(unused)] fn main() { // Basic variable bindings (immutable) fn variable_basics() { let x = 42; // integer let y = 3.14; // float let name = "Rust"; // string literal let is_rust_awesome = true; // boolean println!("Integer: {}", x); println!("Float: {}", y); println!("String: {}", name); println!("Boolean: {}", is_rust_awesome); // Variable shadowing let x = x + 10; // creates a new x; the old x is shadowed { let x = "shadowed"; // another x; the outer x is shadowed in this block println!("x inside shadowing: {}", x); } println!("x after shadowing: {}", x); } }
Result:
Integer: 42
Float: 3.14
String: Rust
Boolean: true
x inside shadowing: shadowed
x after shadowing: 52
Key points:
- Use
letto bind a value. - Identifiers are case-sensitive and typically follow
snake_case. - Bindings are scoped to a function/block and are dropped when out of scope (ownership).
2.1.2 Mutable variables
Rust’s default immutability is strict. If you need to change a value, declare the binding as mutable with the mut keyword. This allows reassignment, but you must opt in at declaration time to make the intent explicit. Mutability is scoped: within a scope, a binding is either mutable or immutable—you can’t switch it halfway through.
#![allow(unused)] fn main() { fn mutable_variables() { // Mutable variable binding let mut counter = 0; println!("Initial: {}", counter); // Update a mutable binding counter += 1; counter *= 2; println!("After update: {}", counter); // A typical use: accumulate in a loop let mut sum = 0; // initialize to 0 let numbers = vec![1, 2, 3, 4, 5]; // create a vector for num in numbers { sum += num; // accumulate } println!("Sum: {}", sum); } }
Result:
Initial: 0
After update: 2
Sum: 15
Key points:
- Declare a mutable binding with
let mut. - Mutable bindings can be reassigned.
- Mutability is often used for accumulation in loops and incremental updates.
- Prefer minimizing mutability to reduce accidental changes and bugs.
- Under Rust’s borrowing rules, mutable borrows (
&mut) are restricted to ensure safety. - Within a scope, a binding is either mutable or immutable.
Mutable bindings are a key part of Rust’s ergonomics, but in combination with ownership and the borrow checker you’ll sometimes need to structure code carefully to satisfy the compiler.
2.1.3 Constants
Constants are declared with the const keyword. They are always immutable, and their values must be known at compile time (so they cannot depend on runtime input). If declared at module scope they are globally accessible and live for the entire program. Constants are a good fit for stable configuration values or mathematical constants. A type annotation is required, and const cannot be mut.
#![allow(unused)] fn main() { // Constant declarations (always immutable, must have a type annotation) fn constants_example() { const PI: f64 = 3.14159265359; // floating-point constant const MAX_SIZE: usize = 1000; // unsigned integer constant const GREETING: &str = "Hello, World!"; // string slice constant println!("PI = {}", PI); println!("Max size: {}", MAX_SIZE); println!("Greeting: {}", GREETING); // Constant expressions const AREA: f64 = PI * 10.0 * 10.0; // circle area formula println!("Circle area: {}", AREA); } }
Result:
PI = 3.14159265359
Max size: 1000
Greeting: Hello, World!
Circle area: 314.15926535899996
Key points:
- Use the form
const NAME: Type = value;and name constants inSCREAMING_SNAKE_CASE. - The value must be a constant expression (literals and simple computations); it can’t depend on runtime input.
staticis related but has different semantics;constis more common for simple values.- Constants are computed at compile time, improving maintainability and enabling optimization.
- Constants are immutable and represent stable “facts” or configuration.
Constants improve maintainability because they encode immutable “facts” and can be optimized at compile time.
2.2 Primitive data types
Rust’s primitive data types fall into two categories: scalar types and compound types. Scalars represent a single value; compound types group multiple values. These types have a known size at compile time, which helps Rust deliver safety and performance.
Scalar types
- Integers: signed (
i8,i16,i32,i64,i128,isize) and unsigned (u8,u16,u32,u64,u128,usize). Default isi32. - Floating-point:
f32(single precision) andf64(double precision, default). - Boolean:
bool, eithertrueorfalse. - Character:
char, a Unicode scalar value (e.g.'a').
Compound types
- Tuples: fixed-size heterogeneous collections like
(i32, bool). - Arrays: fixed-length homogeneous collections like
[i32; 5](fivei32s).
2.2.1 Integer types
#![allow(unused)] fn main() { fn integer_types() { // Signed integers let i8_val: i8 = -128; // range: -128 to 127 let i16_val: i16 = -32768; // range: -32768 to 32767 let i32_val: i32 = -2147483648; // default integer type let i64_val: i64 = -9223372036854775808; let i128_val: i128 = -170141183460469231731687303715884105728; // Unsigned integers let u8_val: u8 = 255; // range: 0 to 255 let u16_val: u16 = 65535; let u32_val: u32 = 4294967295; let u64_val: u64 = 18446744073709551615; let u128_val: u128 = 340282366920938463463374607431768211455; // Platform-sized integers let isize: isize = -1; // size depends on architecture let usize: usize = 1; // size depends on architecture // Numeric literals let decimal = 98_222; // decimal (underscores allowed) let hex = 0xff; // hexadecimal let octal = 0o77; // octal let binary = 0b1111_0000; // binary let byte = b'A'; // byte literal (u8 only) println!("Integer literals: {}, {}, {}, {}", decimal, hex, octal, binary); } }
Result:
Integer literals: 98222, 255, 63, 240
Key points:
- Rust provides both signed and unsigned integers. Signed integers can represent negatives; unsigned integers are non-negative.
- There are multiple widths (8/16/32/64/128-bit) as well as platform-sized
isize/usize. isizeandusizedepend on the target architecture (commonly 64-bit on modern machines).- Numeric literals can be written in decimal, hex, octal, and binary; underscores improve readability.
- Integer types have fixed ranges. In debug builds, overflow typically panics; in release builds it wraps (unless you use checked/saturating/wrapping operations).
- Rust offers methods like
wrapping_add,checked_add, andsaturating_addfor explicit overflow behavior. - ...
2.2.2 Floating-point types
#![allow(unused)] fn main() { fn float_types() { let f32_val: f32 = 3.141592653589793; // 32-bit float let f64_val: f64 = 3.141592653589793; // 64-bit float (default) // Special values let infinity = f32::INFINITY; let neg_infinity = f32::NEG_INFINITY; let not_a_number = f32::NAN; println!("f32: {}", f32_val); println!("f64: {}", f64_val); println!("Infinity: {}", infinity); println!("Negative infinity: {}", neg_infinity); println!("NaN: {}", not_a_number); // Math let result = f32::sqrt(2.0); println!("√2 = {}", result); // Comparisons let x: f64 = 1.0; // explicitly typed let y: f64 = 0.1 + 0.1 + 0.1 + 0.1 + 0.1; println!("x == y: {}", x == y); // avoid direct float equality when possible println!("(x - y).abs() < 1e-10: {}", (x - y).abs() < 1e-10); } }
Result:
f32: 3.1415927
f64: 3.141592653589793
Infinity: inf
Negative infinity: -inf
NaN: NaN
√2 = 1.4142135
x == y: false
(x - y).abs() < 1e-10: false
Key points:
- Rust provides two float types:
f32andf64. - Floats support special values like infinity, negative infinity, and NaN.
- Floating-point arithmetic is approximate; prefer comparing within an epsilon rather than using direct equality.
- Float operators include addition (+), subtraction (-), multiplication (*), division (/), and remainder (%).
- The standard library provides many floating-point operations such as square root (
sqrt), exponentiation (exp), logarithms (log), and trigonometric functions. - Rust does not perform implicit numeric promotion between integers and floats; use explicit casts (e.g.
x as f64) when mixing numeric types. - Floats cannot be “mixed” with booleans or strings in arithmetic; convert/format explicitly when needed.
- ...
2.2.3 Boolean type
#![allow(unused)] fn main() { fn boolean_types() { let is_learning_rust = true; let is_difficult = false; // Conditional expression let message = if is_learning_rust { "Keep going!" } else { "Try harder!" }; // Boolean logic let both_true = is_learning_rust && !is_difficult; let either_or = is_learning_rust || is_difficult; println!("{} {}", message, both_true); println!("Either learning or difficult: {}", either_or); // Booleans in pattern matching match (is_learning_rust, is_difficult) { (true, false) => println!("Perfect learning situation!"), (true, true) => println!("Challenging but rewarding!"), (false, _) => println!("Maybe try something else?"), } } }
Result:
Keep going! true
Either learning or difficult: true
Perfect learning situation!
Key points:
- The boolean type has only two values:
trueandfalse. - Booleans are used in conditions (
if,while) and logical expressions (&&,||,!). - Rust does not implicitly convert between
booland numeric/string types; convert explicitly when needed.
2.2.4 Character type
In Rust, a char is a Unicode scalar value. You can iterate over the characters of a string with .chars(). A char is a logical character unit, not a single byte.
A character literal uses single quotes and represents exactly one char.
A string is stored as UTF-8 bytes ([u8]), and you can view them via .as_bytes().
Note: byte-level operations are efficient, but slicing arbitrary byte offsets may split a multi-byte character and produce invalid UTF-8.
#![allow(unused)] fn main() { fn character_types() { let c1 = 'z'; // a single character let c2 = 'ℤ'; // a Unicode character let c3 = '😊'; // an emoji println!("Chars: {}, {}, {}", c1, c2, c3); // Escape sequences let newline = '\n'; let tab = '\t'; let quote = '\''; let backslash = '\\'; // Characters in a string let string = "Hello, 世界! 🌍"; for (index, ch) in string.chars().enumerate() { println!("Character {}: {}", index, ch); } // Bytes let bytes = string.as_bytes(); println!("String length (bytes): {}", bytes.len()); } }
Result:
Chars: z, ℤ, 😊
Character 0: H
Character 1: e
Character 2: l
Character 3: l
Character 4: o
Character 5: ,
Character 6:
Character 7: 世
Character 8: 界
Character 9: !
Character 10:
Character 11: 🌍
String length (bytes): 19
Key points:
- A
charis a Unicode scalar value. - A
String/&stris UTF-8 bytes;.as_bytes()gives you a byte view. - Use
.chars()(optionally with.enumerate()) to iterate by Unicode scalar values. - String length in bytes (
.len()) is not the same as “number of characters”. - Slicing strings by byte indices can break UTF-8 unless you slice on character boundaries.
- ...
2.3 Compound types: tuples and arrays
2.3.1 Tuples
A tuple is a fixed-length ordered collection that can contain elements of different types. Its length is fixed once created, but the element types can be heterogeneous.
Tuple basics
#![allow(unused)] fn main() { fn tuple_basics() { // Create tuples let tup: (i32, f64, u8) = (500, 6.4, 1); let tup2 = (42, "Hello", true); // Access tuple elements (by index) let x = tup.0; // 500 let y = tup.1; // 6.4 let z = tup.2; // 1 println!("Tuple values: ({}, {}, {})", x, y, z); // Destructuring assignment (pattern matching) let (a, b, c) = tup; println!("Destructured: a={}, b={}, c={}", a, b, c); // Single-element tuple (note the comma) let single_tuple: (i32,) = (5,); println!("Single-element tuple: {:?}", single_tuple); } }
Result:
Tuple values: (500, 6.4, 1)
Destructured: a=500, b=6.4, c=1
Single-element tuple: (5,)
Key points:
- Tuples are fixed-length ordered collections that can contain mixed types.
- Access elements with
.0,.1, etc. - Use destructuring (
let (a, b) = tup;) to bind elements to variables. - A single-element tuple needs a trailing comma:
(5,). - Tuples are commonly used to return multiple values from a function.
- ...
Practical tuple examples
#![allow(unused)] fn main() { fn practical_tuples() { // Returning multiple values let result = divide_and_remainder(17, 5); let (quotient, remainder) = result; println!("17 divided by 5 => quotient {}, remainder {}", quotient, remainder); // Destructure directly let (sum, product) = calculate_sum_product(10, 20); println!("Sum: {}, Product: {}", sum, product); // Storing mixed-type data let person_info = ("Alice", 25, 175.5, true); let (name, age, height, is_student) = person_info; println!( "{} is {} years old, {:.1} cm tall, status: {}", name, age, height, if is_student { "student" } else { "not a student" } ); // Nested tuple let nested_tuple = (1, (2, 3), 4); let inner_tuple = nested_tuple.1; let first_inner = inner_tuple.0; // 2 println!("Value inside nested tuple: {}", first_inner); } // Functions returning tuples fn divide_and_remainder(dividend: i32, divisor: i32) -> (i32, i32) { let quotient = dividend / divisor; let remainder = dividend % divisor; (quotient, remainder) } fn calculate_sum_product(a: i32, b: i32) -> (i32, i32) { (a + b, a * b) } }
Result:
17 divided by 5 => quotient 3, remainder 2
Sum: 30, Product: 200
Alice is 25 years old, 175.5 cm tall, status: student
Value inside nested tuple: 2
Tuples in pattern matching
#![allow(unused)] fn main() { fn tuple_pattern_matching() { let coordinates = (10, 20); match coordinates { (0, 0) => println!("Origin"), (x, 0) => println!("On the X-axis, x = {}", x), (0, y) => println!("On the Y-axis, y = {}", y), (x, y) => println!("Point: ({}, {})", x, y), } // Patterns with guards let point = (15, 30); match point { (x, y) if x == y => println!("On the diagonal: ({}, {})", x, y), (x, y) if x + y == 45 => println!("x + y is 45: ({}, {})", x, y), (x, y) => println!("General point: ({}, {})", x, y), } // Destructure a function result let (name, age) = get_person_info(); println!("Person: {}, age {}", name, age); } fn get_person_info() -> (&'static str, u32) { ("Bob", 30) } }
Result:
Point: (10, 20)
x + y is 45: (15, 30)
Person: Bob, age 30
Key points:
- Tuples can hold values of different types.
- Pattern matching works well with tuples to extract and transform values.
- Tuples are a common way to return multiple values.
- Destructuring lets you assign multiple bindings at once.
- Match guards (
if ...) refine patterns with additional conditions. - ...
2.3.2 Arrays
An array is a fixed-length collection of elements of the same type. The length is known at compile time and cannot grow dynamically.
Array basics
#![allow(unused)] fn main() { fn array_basics() { // Declaration and initialization let numbers: [i32; 5] = [1, 2, 3, 4, 5]; let floats = [3.14, 2.71, 1.41, 1.73]; // type inference let chars = ['R', 'u', 's', 't']; // char array // Indexing let first = numbers[0]; let last = numbers[4]; println!("First element: {}, last element: {}", first, last); // Length println!("numbers length: {}", numbers.len()); // Repeat initialization let repeated = [0; 10]; // length 10, all zeros println!("repeated length: {}", repeated.len()); // Iteration for (index, &value) in numbers.iter().enumerate() { println!("numbers[{}] = {}", index, value); } } }
Result:
First element: 1, last element: 5
numbers length: 5
repeated length: 10
numbers[0] = 1
numbers[1] = 2
numbers[2] = 3
numbers[3] = 4
numbers[4] = 5
Key points:
- Array lengths are fixed at compile time and can’t grow dynamically.
- All elements in an array have the same type.
- Index elements with
[index]. - Use
.len()to get the length. - Use
[value; N]to initialize an array with repeated values. - Iterate safely with
.iter()(immutable) or.iter_mut()(mutable). - Use
.enumerate()when you need both index and value. - Use
.get(index)/.get_mut(index)to avoid panics on out-of-bounds access (they returnOption). - ...
Arrays and loops
#![allow(unused)] fn main() { fn array_loops() { let arr = [10, 20, 30, 40, 50]; let mut sum = 0; // Approach 1: index-based loop let len = arr.len(); for i in 0..len { sum += arr[i]; println!("Add arr[{}] = {}, running sum: {}", i, arr[i], sum); } println!("Array sum: {}", sum); // Approach 2: iterate elements directly (safer) let mut sum2 = 0; for &value in &arr { sum2 += value; println!("Value: {}", value); } println!("Recomputed sum: {}", sum2); // Approach 3: enumerate for (i, &value) in arr.iter().enumerate() { println!("Index {}: value {}", i, value); } } }
Result:
Add arr[0] = 10, running sum: 10
Add arr[1] = 20, running sum: 30
Add arr[2] = 30, running sum: 60
Add arr[3] = 40, running sum: 100
Add arr[4] = 50, running sum: 150
Array sum: 150
Value: 10
Value: 20
Value: 30
Value: 40
Value: 50
Recomputed sum: 150
Index 0: value 10
Index 1: value 20
Index 2: value 30
Index 3: value 40
Index 4: value 50
Key points:
- Use
&arr/arr.iter()for immutable iteration. - Use
arr.iter_mut()for mutable iteration. - Use
enumerate()when you need indices. - ...
Multidimensional arrays
#![allow(unused)] fn main() { fn multidimensional_arrays() { // 2D array let matrix: [[i32; 3]; 2] = [ [1, 2, 3], [4, 5, 6], ]; println!("Matrix contents:"); for (i, row) in matrix.iter().enumerate() { for (j, &value) in row.iter().enumerate() { print!("matrix[{}][{}] = {} ", i, j, value); } println!(); } // Indexing a 2D array let element = matrix[1][2]; // row 2, col 3 => 6 println!("matrix[1][2] = {}", element); // 3D array example let three_d: [[[i32; 2]; 2]; 2] = [ [[1, 2], [3, 4]], [[5, 6], [7, 8]], ]; println!("3D array contents:"); for (i, depth) in three_d.iter().enumerate() { for (j, row) in depth.iter().enumerate() { for (k, &value) in row.iter().enumerate() { print!("[{}][{}][{}] = {} ", i, j, k, value); } println!(); } } } }
Result:
Matrix contents:
matrix[0][0] = 1 matrix[0][1] = 2 matrix[0][2] = 3
matrix[1][0] = 4 matrix[1][1] = 5 matrix[1][2] = 6
matrix[1][2] = 6
3D array contents:
[0][0][0] = 1 [0][0][1] = 2
[0][1][0] = 3 [0][1][1] = 4
[1][0][0] = 5 [1][0][1] = 6
[1][1][0] = 7 [1][1][1] = 8
Key points:
- Use
.get()/.get_mut()for safe access without panics (they returnOption). - Use
.iter()with.enumerate()to traverse with indices. - Use
match(orif let) on the returnedOptionto handle out-of-bounds cases. - For multidimensional arrays, nest loops or iterators per dimension.
print!/println!help format output.- ...
Bounds checking
#![allow(unused)] fn main() { fn array_bounds_checking() { let arr = [10, 20, 30]; // Safe access if let Some(&value) = arr.get(1) { println!("arr[1] = {}", value); } // Out-of-bounds handling match arr.get(5) { Some(value) => println!("arr[5] = {}", value), None => println!("Out of bounds! Max index: {}", arr.len() - 1), } // Slices (borrowing part of an array) let slice = &arr[0..2]; // includes indices 0 and 1 println!("Slice: {:?}", slice); let slice_to_end = &arr[1..]; // from index 1 to the end println!("Slice from index 1: {:?}", slice_to_end); let slice_from_start = &arr[..2]; // from start up to index 2 (exclusive) println!("Slice up to index 2: {:?}", slice_from_start); let full_slice = &arr[..]; // the whole array println!("Full slice: {:?}", full_slice); } }
Result:
arr[1] = 20
Out of bounds! Max index: 2
Slice: [10, 20]
Slice from index 1: [20, 30]
Slice up to index 2: [10, 20]
Full slice: [10, 20, 30]
Key points:
- Use
.get()to access elements safely without panics. - Match on the returned
Optionto handle the absence of a value. - Use slices (
&arr[a..b]) to borrow a portion of an array. - ...
Practical array operations
#![allow(unused)] fn main() { fn array_operations() { let mut numbers = [64, 34, 25, 12, 22, 11, 90]; println!("Original: {:?}", numbers); // Max and min let max = numbers.iter().max().unwrap(); let min = numbers.iter().min().unwrap(); println!("Max: {}, Min: {}", max, min); // Sum and average let sum: i32 = numbers.iter().sum(); let average = sum as f64 / numbers.len() as f64; println!("Sum: {}, Average: {:.2}", sum, average); // Filter and transform let even_numbers: Vec<_> = numbers.iter() .filter(|&&x| x % 2 == 0) .copied() .collect(); println!("Even numbers: {:?}", even_numbers); let squared: Vec<_> = numbers.iter() .map(|&x| x * x) .collect(); println!("Squared: {:?}", squared); // Membership and position let contains_25 = numbers.contains(&25); let position = numbers.iter().position(|&x| x == 25); println!("Contains 25: {}, position: {:?}", contains_25, position); // Sort let mut sorted = numbers; sorted.sort(); println!("Sorted: {:?}", sorted); } }
Result:
Original: [64, 34, 25, 12, 22, 11, 90]
Max: 90, Min: 11
Sum: 258, Average: 36.86
Even numbers: [64, 34, 12, 22, 90]
Squared: [4096, 1156, 625, 144, 484, 121, 8100]
Contains 25: true, position: Some(2)
Sorted: [11, 12, 22, 25, 34, 64, 90]
Key points:
iter()returns an iterator over references to elements.sum()reduces an iterator to a total.copied()turns&TintoTforCopytypes.filter()keeps elements matching a predicate.map()transforms elements.contains()checks for membership.position()finds the index of the first match.sort()sorts in ascending order.enumerate()yields(index, item)pairs.- ...
String arrays and character processing
#![allow(unused)] fn main() { fn string_and_char_arrays() { // Array of string slices let fruits = ["apple", "banana", "orange", "grape"]; for (i, fruit) in fruits.iter().enumerate() { println!("fruits[{}] = {}", i, fruit); } // Char array let word = ['R', 'u', 's', 't']; let word_str: String = word.iter().collect(); println!("Char array to String: {}", word_str); // Iterate over chars for ch in &word { println!("Char: {}", ch); // Convert to ASCII code (safe here because these are ASCII letters) println!("ASCII: {}", *ch as u8); } // Count characters vs bytes let multi_char_str = "你好,世界! 🌍"; let chars: Vec<char> = multi_char_str.chars().collect(); println!("String: {}", multi_char_str); println!("Char count: {}", chars.len()); println!("Byte length: {}", multi_char_str.len()); } }
Result:
fruits[0] = apple
fruits[1] = banana
fruits[2] = orange
fruits[3] = grape
Char array to String: Rust
Char: R
ASCII: 82
Char: u
ASCII: 117
Char: s
ASCII: 115
Char: t
ASCII: 116
String: 你好,世界! 🌍
Char count: 8
Byte length: 23
Key points:
iter()produces an iterator over references.enumerate()provides indices.collect()materializes an iterator into a collection.chars()iterates Unicode scalar values.len()on&strreturns the byte length, not the number of characters.- ...
Arrays in functions
#![allow(unused)] fn main() { fn array_in_functions() { let arr = [1, 2, 3, 4, 5]; // Pass array slices (borrows) let sum = sum_array(&arr); let max = max_array(&arr); println!("Array: {:?}", arr); println!("Sum: {}, Max: {}", sum, max); // Modify via mutable slice let mut mut_arr = [10, 20, 30]; modify_array(&mut mut_arr); println!("After modify: {:?}", mut_arr); // Return a computed Vec let squared = square_array(&arr); println!("Squared: {:?}", squared); } // Sum of a slice fn sum_array(arr: &[i32]) -> i32 { arr.iter().sum() } // Max of a slice fn max_array(arr: &[i32]) -> i32 { arr.iter().max().copied().unwrap_or(0) } // Modify via mutable slice fn modify_array(arr: &mut [i32]) { for i in 0..arr.len() { arr[i] *= 2; } } // Produce a new Vec fn square_array(arr: &[i32]) -> Vec<i32> { arr.iter().map(|&x| x * x).collect() } }
Result:
Array: [1, 2, 3, 4, 5]
Sum: 15, Max: 5
After modify: [20, 40, 60]
Squared: [1, 4, 9, 16, 25]
Key points:
- Prefer taking
&[T](a slice) in function parameters so you can accept arrays and vectors. - Use
&mut [T]when the function needs to mutate elements. - Passing
&arrborrows the array; ownership is not moved. - Use
&mutonly when mutation is required. - ...
2.4 Strings
2.4.1 String literals and slices
In Rust, a string literal is immutable, hard-coded text written with double quotes, like "hello". Its type is &str (a string slice) which points to a UTF-8 byte sequence stored in the program binary. More generally, a slice is a view into existing data (e.g. &[T]). A string slice &str is an immutable view into a string’s UTF-8 bytes.
#![allow(unused)] fn main() { fn string_slices() { // String literals (&str) are compile-time constants let greeting = "Hello, Rust!"; let name = "World"; // Slices (borrowed views, no ownership) // Note: indexing a &str by ranges works only on valid UTF-8 boundaries. let slice = &greeting[0..5]; // "Hello" let slice_from_middle = &greeting[7..11]; // "Rust" println!("Full greeting: {}", greeting); println!("Slice: {}", slice); // String methods let trimmed = " hello ".trim(); // "hello" let uppercase = "rust".to_uppercase(); // "RUST" let lowercase = "RUST".to_lowercase(); // "rust" // Search and split let text = "one,two,three,four"; let parts: Vec<&str> = text.split(',').collect(); println!("Split parts: {:?}", parts); // Replace let replaced = "hello world".replace("world", "Rust"); println!("Replaced: {}", replaced); } }
Result:
Full greeting: Hello, Rust!
Slice: Hello
Split parts: ["one", "two", "three", "four"]
Replaced: hello Rust
Key points:
- String literals have the
'staticlifetime and are&'static str. - A slice borrows data; it doesn’t own it. String slicing by indices must be on valid UTF-8 boundaries (otherwise it panics).
&stris commonly used in function parameters to avoid unnecessary allocations.- A
&strcan refer to a string literal or aString’s contents. - Common operations include splitting (
split), replacing (replace), and searching. - Convert
&strtoStringwith.to_string()or.to_owned(). - ...
2.4.2 The String type
String is a growable, mutable, owned UTF-8 string stored on the heap. Unlike &str, a String owns its data and can be modified (for example, by appending). It dereferences to &str, so it can be used where a string slice is expected.
#![allow(unused)] fn main() { fn string_type() { // `String` owns its data let mut s = String::new(); // empty string let s1 = String::from("hello"); // from a string literal let s2 = "world".to_string(); // to a `String` // Append s.push('A'); // push a char s.push_str("pple"); // push a string slice s += " Banana"; // append with `+=` println!("String s: {}", s); // Formatting let name = "Alice"; let age = 30; let formatted = format!("{} is {} years old", name, age); println!("Formatted: {}", formatted); // Using macros println!("Test value: {}, another value: {}", 42, "text"); // Ownership example let original = String::from("original"); let moved = original; // move ownership // println!("{}", original); // compile error: original was moved println!("Moved string: {}", moved); } }
Result:
String s: Apple Banana
Formatted: Alice is 30 years old
Test value: 42, another value: text
Moved string: original
Key points:
- Create a
StringwithString::new(),String::from(...), or.to_string(). - Append with
push(char) andpush_str(string slice). - Ownership rules apply: after a move, the original binding can’t be used unless you clone.
format!builds aStringusing formatting without printing.println!prints formatted values.- ...
2.5 Control flow
2.5.1 if expressions
In Rust, if is an expression: it can produce a value. Parentheses around the condition are optional, and else / else if are supported. The condition must be a bool (there is no implicit conversion from integers).
#![allow(unused)] fn main() { fn conditional_statements() { let number = 7; // Basic if/else if number < 5 { println!("Number is less than 5"); } else if number == 5 { println!("Number equals 5"); } else { println!("Number is greater than 5"); } // `if` as an expression (produces a value) let grade = if number >= 90 { "A" } else if number >= 80 { "B" } else if number >= 70 { "C" } else { "F" }; println!("Grade: {}", grade); // Conditional assignment let status = if number % 2 == 0 { "even" } else { "odd" }; println!("{} is {}", number, status); } }
Result:
Number is greater than 5
Grade: F
7 is odd
Key points:
ifchooses branches based on a boolean condition.- Because
ifis an expression, you can assign its result to a variable. - All branches used as an expression must evaluate to compatible types.
- Rust does not implicitly convert numeric types to
bool. - ...
2.5.2 Loops
loop (infinite loop)
loop repeats forever until you explicitly break. Like many constructs in Rust, it can be used as an expression and can return a value. Labels can help control nested loops.
#![allow(unused)] fn main() { fn loop_examples() { // Basic loop let mut counter = 0; loop { counter += 1; println!("Counter: {}", counter); if counter >= 5 { break; // exit the loop } } // `loop` as an expression (returns a value) let result = loop { counter += 1; if counter == 10 { break counter; // break with a value } }; println!("Loop result: {}", result); } }
Result:
Counter: 1
Counter: 2
Counter: 3
Counter: 4
Counter: 5
Loop result: 10
Key points:
breakexits a loop.break valueexits and returns a value from aloopexpression.continueskips to the next iteration.- Labels (e.g.
'outer: loop { ... }) can control which loop tobreak/continuein nested loops. - ...
while (conditional loop)
while repeats while a condition is true. The condition must be a bool.
#![allow(unused)] fn main() { fn while_examples() { let mut number = 3; while number != 0 { println!("Countdown: {}", number); number -= 1; } println!("Liftoff!"); // Array traversal let array = [10, 20, 30, 40, 50]; let mut index = 0; while index < array.len() { println!("Index {}: value {}", index, array[index]); index += 1; } } }
Result:
Countdown: 3
Countdown: 2
Countdown: 1
Liftoff!
Index 0: value 10
Index 1: value 20
Index 2: value 30
Index 3: value 40
Index 4: value 50
Key points:
- Rust has no built-in do-while loop; you can simulate it with
loop { ...; if !cond { break } }. - Use
whilewhen you have a loop condition but don’t naturally iterate a collection. - For arrays/vectors/iterators, prefer
forloops for safety and clarity. - ...
for loops and iterators
for iterates over ranges and iterators. Syntax: for item in iterator { ... }. It works well with Rust’s ownership model.
#![allow(unused)] fn main() { fn for_loop_examples() { // Basic for loop for i in 0..5 { // 0 to 4 println!("for loop: {}", i); } // Inclusive range for i in 0..=5 { // 0 to 5 println!("inclusive: {}", i); } // Array iteration let array = [1, 2, 3, 4, 5]; for item in array { println!("array item: {}", item); } // Index + value let names = vec!["Alice", "Bob", "Charlie"]; for (index, name) in names.iter().enumerate() { println!("{}: {}", index, name); } // Iterate chars let text = "Rust"; for ch in text.chars() { println!("Char: {}", ch); } // Iterate bytes for byte in text.bytes() { println!("Byte: {}", byte); } } }
Result:
for loop: 0
for loop: 1
for loop: 2
for loop: 3
for loop: 4
inclusive: 0
inclusive: 1
inclusive: 2
inclusive: 3
inclusive: 4
inclusive: 5
array item: 1
array item: 2
array item: 3
array item: 4
array item: 5
0: Alice
1: Bob
2: Charlie
Char: R
Char: u
Char: s
Char: t
Byte: 82
Byte: 117
Byte: 115
Byte: 116
Key points:
forloops work over any iterator (ranges, arrays, vectors, strings, etc.).0..5is a half-open range (0 through 4), while0..=5includes 5.- Use
.enumerate()when you need indices. - ...
2.5.3 Pattern matching
Pattern matching with match lets you destructure values and handle multiple cases. Matches must be exhaustive (or use _ as a wildcard). Rust supports bindings, guards, and nested patterns.
#![allow(unused)] fn main() { fn pattern_matching() { let x = 42; match x { 0 => println!("zero"), 1..=10 => println!("between 1 and 10"), 20 | 30 | 40 => println!("20, 30, or 40"), n if n % 2 == 0 => println!("even: {}", n), _ => println!("other: {}", x), // wildcard } // Bind a matched value match x { 0 => println!("zero"), 1 => println!("one"), n => println!("other: {}", n), } // Compound patterns let point = (0, 7); match point { (0, 0) => println!("origin"), (0, y) => println!("on the Y axis: y = {}", y), (x, 0) => println!("on the X axis: x = {}", x), (x, y) => println!("point ({}, {})", x, y), } } }
Result:
even: 42
other: 42
on the Y axis: y = 7
Key points:
matchmust be exhaustive; use_as a catch-all.- Patterns can destructure tuples/structs and bind values.
- Guards (
if ...) allow additional conditions. if let/while letcan simplify matching onOption/Result.matchcan also be used as an expression that returns a value.- ...
2.6 Defining and calling functions
2.6.1 Function basics
In Rust, a function is a reusable block of code defined with the fn keyword. Function names typically use snake_case. Functions can take parameters and optionally return a value. Every program has a main function as the entry point. Function bodies are enclosed in {} and may contain statements and expressions. Rust is statically typed, so parameter and return types are checked at compile time.
#![allow(unused)] fn main() { // Function definition fn greet(name: &str) { println!("Hello, {}!", name); } // Function with a return value fn add(a: i32, b: i32) -> i32 { a + b // no semicolon => expression is returned } // Explicit return fn multiply(x: i32, y: i32) -> i32 { return x * y; } // Calling functions fn function_examples() { greet("Rust"); let sum = add(5, 3); let product = multiply(4, 7); println!("5 + 3 = {}", sum); println!("4 * 7 = {}", product); // A block used as an expression let result = { let a = 10; let b = 20; a + b // last expression is the block value }; println!("Block expression result: {}", result); } }
Result:
Hello, Rust!
5 + 3 = 8
4 * 7 = 28
Block expression result: 30
Key points:
- Define functions with
fn. - Parameter types come after parameter names.
- Return types come after
->. - The last expression in a function/block (without a semicolon) becomes the return value.
- Call functions with
name(args...). - Rust does not have variadic functions in stable Rust; pass a slice (
&[T]) or a collection instead. - ...
2.6.2 Parameters and return values
Parameters are defined in parentheses and must have types. Rust’s ownership rules apply: parameters can be passed by value (moving ownership) or by reference (borrowing). Use mut when you need mutability.
Return types are written after -> Type. A function returns the last expression implicitly, or you can return explicitly with return. If a function returns nothing, its return type is () (the unit type). To return multiple values, use a tuple.
#![allow(unused)] fn main() { // Multiple parameters fn calculate_area(length: f64, width: f64) -> f64 { length * width } // Accept a variable number of values via a slice fn print_values(values: &[i32]) { for value in values { println!("Value: {}", value); } } // Return a tuple fn get_coordinates() -> (i32, i32) { (10, 20) } // Return a named struct #[derive(Debug)] struct Rectangle { width: f64, height: f64, } fn create_rectangle(width: f64, height: f64) -> Rectangle { Rectangle { width, height } } fn calculate_rectangle_area(rect: &Rectangle) -> f64 { rect.width * rect.height } fn function_parameters() { let area = calculate_area(5.0, 3.0); println!("Rectangle area: {}", area); let values = vec![1, 2, 3, 4, 5]; print_values(&values); let (x, y) = get_coordinates(); println!("Coordinates: ({}, {})", x, y); let rectangle = create_rectangle(4.0, 6.0); let rect_area = calculate_rectangle_area(&rectangle); println!("Rectangle area: {}", rect_area); } }
Result:
Rectangle area: 15
Value: 1
Value: 2
Value: 3
Value: 4
Value: 5
Coordinates: (10, 20)
Rectangle area: 24
Key points:
- Function parameter and return types must be explicit.
- Parameters can be moved or borrowed depending on whether you pass by value or reference.
- Use tuples or structs to return multiple values.
- ...
2.6.3 Higher-order functions
#![allow(unused)] fn main() { // Basic functions fn add(a: i32, b: i32) -> i32 { a + b } fn multiply(a: i32, b: i32) -> i32 { a * b } // Function as a parameter fn apply_function<F>(value: i32, f: F) -> i32 where F: Fn(i32) -> i32, { f(value) } // Function as a return value fn get_operation(operation: &str) -> fn(i32, i32) -> i32 { match operation { "add" => add, "multiply" => multiply, _ => add, // default } } fn higher_order_functions() { let result1 = apply_function(5, |x| x * x); // closure let result2 = apply_function(10, |x| x + 100); println!("Square: {}", result1); println!("Plus 100: {}", result2); let operation = get_operation("add"); let result3 = operation(15, 25); println!("Function pointer result: {}", result3); } }
Result:
Square: 25
Plus 100: 110
Function pointer result: 40
Key points:
- Functions and closures can be passed as parameters.
- Functions can be returned (often as function pointers or boxed trait objects).
- Closures are anonymous functions that can capture environment.
- Function pointers (
fn(...) -> ...) represent non-capturing functions. - Generics let you accept different callable types via traits like
Fn. - ...
2.7 Practical project: a scientific calculator and data processing tool
2.7.1 Requirements
Build a feature-complete scientific calculator that supports:
- Basic and scientific operations
- Expression parsing and evaluation
- Statistical analysis
- History tracking
2.7.2 Project structure
// src/main.rs mod calculator; mod data; mod history; mod utils; use calculator::{Calculator, Operation}; use data::Statistics; use history::HistoryManager; use utils::Error; fn main() -> Result<(), Error> { println!("=== Scientific Calculator v1.0 ==="); let mut calculator = Calculator::new(); let mut history = HistoryManager::new(); // Example calculations run_example_calculations(&mut calculator, &mut history)?; Ok(()) } fn run_example_calculations( calc: &mut Calculator, history: &mut HistoryManager ) -> Result<(), Error> { // Basic operations let result1 = calc.add(10.0, 5.0)?; println!("10 + 5 = {}", result1); history.add_record("10 + 5", result1); let result2 = calc.multiply(result1, 2.0)?; println!("({}) * 2 = {}", result1, result2); history.add_record("(10 + 5) * 2", result2); // Scientific operations let result3 = calc.sqrt(16.0)?; println!("sqrt(16) = {}", result3); history.add_record("sqrt(16)", result3); let result4 = calc.sin(30.0_f64.to_radians())?; println!("sin(30°) = {}", result4); history.add_record("sin(30°)", result4); // Expression evaluation let expr_result = calc.evaluate_expression("(10 + 5) * 2 - sqrt(16)")?; println!("(10 + 5) * 2 - sqrt(16) = {}", expr_result); history.add_record("(10 + 5) * 2 - sqrt(16)", expr_result); // Statistics let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]; let stats = calc.calculate_statistics(&data)?; println!("Dataset statistics: {:?}", stats); history.display(); Ok(()) }
2.7.3 Calculator core module
#![allow(unused)] fn main() { // src/calculator/mod.rs pub mod operations; pub mod parser; pub mod evaluator; use operations::Operation; use parser::ExpressionParser; use evaluator::ExpressionEvaluator; use utils::Error; pub struct Calculator { parser: ExpressionParser, evaluator: ExpressionEvaluator, } impl Calculator { pub fn new() -> Self { Self { parser: ExpressionParser::new(), evaluator: ExpressionEvaluator::new(), } } // Basic operations pub fn add(&self, a: f64, b: f64) -> Result<f64, Error> { Ok(a + b) } pub fn subtract(&self, a: f64, b: f64) -> Result<f64, Error> { Ok(a - b) } pub fn multiply(&self, a: f64, b: f64) -> Result<f64, Error> { Ok(a * b) } pub fn divide(&self, a: f64, b: f64) -> Result<f64, Error> { if b == 0.0 { return Err(Error::DivisionByZero); } Ok(a / b) } pub fn power(&self, base: f64, exponent: f64) -> Result<f64, Error> { Ok(base.powf(exponent)) } pub fn sqrt(&self, value: f64) -> Result<f64, Error> { if value < 0.0 { return Err(Error::NegativeSquareRoot); } Ok(value.sqrt()) } pub fn sin(&self, angle: f64) -> Result<f64, Error> { Ok(angle.sin()) } pub fn cos(&self, angle: f64) -> Result<f64, Error> { Ok(angle.cos()) } pub fn tan(&self, angle: f64) -> Result<f64, Error> { Ok(angle.tan()) } pub fn ln(&self, value: f64) -> Result<f64, Error> { if value <= 0.0 { return Err(Error::InvalidLogarithm); } Ok(value.ln()) } pub fn log(&self, value: f64, base: f64) -> Result<f64, Error> { if value <= 0.0 || base <= 0.0 || base == 1.0 { return Err(Error::InvalidLogarithm); } Ok(value.log(base)) } pub fn factorial(&self, n: u64) -> Result<f64, Error> { if n > 20 { return Err(Error::FactorialTooLarge); } Ok((1..=n).product::<u64>() as f64) } // Expression evaluation pub fn evaluate_expression(&self, expression: &str) -> Result<f64, Error> { let tokens = self.parser.tokenize(expression)?; let ast = self.parser.parse(tokens)?; self.evaluator.evaluate(&ast) } // Statistics pub fn calculate_statistics(&self, data: &[f64]) -> Result<Statistics, Error> { if data.is_empty() { return Err(Error::EmptyDataSet); } let n = data.len() as f64; let sum: f64 = data.iter().sum(); let mean = sum / n; // Variance let variance: f64 = data.iter() .map(|&x| (x - mean).powi(2)) .sum::<f64>() / n; let std_dev = variance.sqrt(); // Median let mut sorted_data = data.to_vec(); sorted_data.sort_by(|a, b| a.partial_cmp(b).unwrap()); let median = if n % 2.0 == 0.0 { (sorted_data[(n as usize / 2) - 1] + sorted_data[n as usize / 2]) / 2.0 } else { sorted_data[n as usize / 2] }; let min = data.iter().cloned().fold(f64::INFINITY, f64::min); let max = data.iter().cloned().fold(f64::NEG_INFINITY, f64::max); // Mode let mut frequency = std::collections::HashMap::new(); for &value in data { *frequency.entry(value).or_insert(0) += 1; } let max_count = frequency.values().max().unwrap_or(&0).clone(); let mode: Vec<f64> = frequency .into_iter() .filter(|&(_, count)| count == max_count) .map(|(value, _)| value) .collect(); Ok(Statistics { count: data.len(), mean, median, mode, variance, std_dev, min, max, sum, }) } } }
2.7.4 Expression parser
#![allow(unused)] fn main() { // src/calculator/parser.rs use crate::utils::Error; #[derive(Debug, Clone, PartialEq)] pub enum Token { Number(f64), Identifier(String), Operator(Operator), LParen, RParen, Comma, } #[derive(Debug, Clone, PartialEq)] pub enum Operator { Add, Subtract, Multiply, Divide, Power, Sqrt, Sin, Cos, Tan, Ln, Log, Factorial, } #[derive(Debug, Clone)] pub enum AstNode { Number(f64), Identifier(String), UnaryOp(Operator, Box<AstNode>), BinaryOp(Operator, Box<AstNode>, Box<AstNode>), FunctionCall(String, Vec<AstNode>), } pub struct ExpressionParser { // Operator precedence precedence: std::collections::HashMap<Operator, i32>, } impl ExpressionParser { pub fn new() -> Self { let mut precedence = std::collections::HashMap::new(); precedence.insert(Operator::Add, 1); precedence.insert(Operator::Subtract, 1); precedence.insert(Operator::Multiply, 2); precedence.insert(Operator::Divide, 2); precedence.insert(Operator::Power, 3); precedence.insert(Operator::Sqrt, 4); precedence.insert(Operator::Factorial, 5); precedence.insert(Operator::Sin, 6); precedence.insert(Operator::Cos, 6); precedence.insert(Operator::Tan, 6); precedence.insert(Operator::Ln, 6); precedence.insert(Operator::Log, 6); Self { precedence } } pub fn tokenize(&self, input: &str) -> Result<Vec<Token>, Error> { let mut tokens = Vec::new(); let mut chars = input.chars().peekable(); while let Some(ch) = chars.next() { match ch { '0'..='9' | '.' => { let mut number_str = ch.to_string(); // Keep reading digits and decimal points while let Some(&next_ch) = chars.peek() { if next_ch.is_numeric() || next_ch == &'.' { number_str.push(chars.next().unwrap()); } else { break; } } let number: f64 = number_str.parse() .map_err(|_| Error::InvalidNumber(number_str))?; tokens.push(Token::Number(number)); } 'a'..='z' | 'A'..='Z' | '_' => { let mut ident = ch.to_string(); // Keep reading identifier characters while let Some(&next_ch) = chars.peek() { if next_ch.is_alphanumeric() || next_ch == &'_' { ident.push(chars.next().unwrap()); } else { break; } } tokens.push(Token::Identifier(ident)); } '+' => tokens.push(Token::Operator(Operator::Add)), '-' => tokens.push(Token::Operator(Operator::Subtract)), '*' => tokens.push(Token::Operator(Operator::Multiply)), '/' => tokens.push(Token::Operator(Operator::Divide)), '^' => tokens.push(Token::Operator(Operator::Power)), '(' => tokens.push(Token::LParen), ')' => tokens.push(Token::RParen), ',' => tokens.push(Token::Comma), ' ' | '\t' | '\n' | '\r' => continue, // skip whitespace _ => return Err(Error::InvalidCharacter(ch)), } } Ok(tokens) } pub fn parse(&self, tokens: Vec<Token>) -> Result<AstNode, Error> { let mut output = Vec::new(); let mut operators = Vec::new(); for token in tokens { match token { Token::Number(n) => output.push(AstNode::Number(n)), Token::Identifier(ident) => output.push(AstNode::Identifier(ident)), Token::Operator(op) => { while let Some(Token::Operator(prev_op)) = operators.last() { if self.get_precedence(prev_op) >= self.get_precedence(&op) { self.pop_operator_to_output(&mut operators, &mut output)?; } else { break; } } operators.push(Token::Operator(op)); } Token::LParen => operators.push(token), Token::RParen => { while let Some(op) = operators.pop() { match op { Token::LParen => break, Token::Operator(op) => self.pop_operator_to_output(&operators, &mut output)?, _ => return Err(Error::MismatchedParen), } } } Token::Comma => { while let Some(token) = operators.pop() { match token { Token::LParen => return Err(Error::MismatchedParen), Token::Operator(op) => self.pop_operator_to_output(&operators, &mut output)?, _ => {} } } } } } while let Some(token) = operators.pop() { match token { Token::Operator(op) => self.pop_operator_to_output(&operators, &mut output)?, Token::LParen | Token::RParen | Token::Comma => return Err(Error::MismatchedParen), } } if output.len() != 1 { return Err(Error::InvalidExpression); } Ok(output.remove(0)) } fn get_precedence(&self, op: &Operator) -> i32 { *self.precedence.get(op).unwrap_or(&0) } fn pop_operator_to_output( &self, operators: &mut Vec<Token>, output: &mut Vec<AstNode> ) -> Result<(), Error> { if let Some(Token::Operator(op)) = operators.pop() { match op { Operator::Sqrt | Operator::Sin | Operator::Cos | Operator::Tan | Operator::Ln | Operator::Factorial => { if let Some(operand) = output.pop() { output.push(AstNode::UnaryOp(op, Box::new(operand))); } else { return Err(Error::InsufficientOperands); } } _ => { if let (Some(right), Some(left)) = (output.pop(), output.pop()) { output.push(AstNode::BinaryOp(op, Box::new(left), Box::new(right))); } else { return Err(Error::InsufficientOperands); } } } } Ok(()) } } }
2.7.5 Expression evaluator
#![allow(unused)] fn main() { // src/calculator/evaluator.rs use super::parser::{AstNode, Operator}; use crate::utils::Error; pub struct ExpressionEvaluator { functions: std::collections::HashMap<String, fn(&[f64]) -> Result<f64, Error>>, } impl ExpressionEvaluator { pub fn new() -> Self { let mut functions = std::collections::HashMap::new(); // Register built-in functions functions.insert("sqrt".to_string(), |args| { if args.len() != 1 { return Err(Error::InvalidArgumentCount("sqrt".to_string(), 1, args.len())); } if args[0] < 0.0 { return Err(Error::NegativeSquareRoot); } Ok(args[0].sqrt()) }); functions.insert("sin".to_string(), |args| { if args.len() != 1 { return Err(Error::InvalidArgumentCount("sin".to_string(), 1, args.len())); } Ok(args[0].sin()) }); functions.insert("cos".to_string(), |args| { if args.len() != 1 { return Err(Error::InvalidArgumentCount("cos".to_string(), 1, args.len())); } Ok(args[0].cos()) }); functions.insert("tan".to_string(), |args| { if args.len() != 1 { return Err(Error::InvalidArgumentCount("tan".to_string(), 1, args.len())); } Ok(args[0].tan()) }); functions.insert("ln".to_string(), |args| { if args.len() != 1 { return Err(Error::InvalidArgumentCount("ln".to_string(), 1, args.len())); } if args[0] <= 0.0 { return Err(Error::InvalidLogarithm); } Ok(args[0].ln()) }); functions.insert("log".to_string(), |args| { if args.len() != 2 { return Err(Error::InvalidArgumentCount("log".to_string(), 2, args.len())); } if args[0] <= 0.0 || args[1] <= 0.0 || args[1] == 1.0 { return Err(Error::InvalidLogarithm); } Ok(args[0].log(args[1])) }); functions.insert("factorial".to_string(), |args| { if args.len() != 1 { return Err(Error::InvalidArgumentCount("factorial".to_string(), 1, args.len())); } let n = args[0] as u64; if args[0] < 0.0 || args[0] - n as f64 != 0.0 { return Err(Error::InvalidFactorialArgument); } if n > 20 { return Err(Error::FactorialTooLarge); } Ok((1..=n).product::<u64>() as f64) }); Self { functions } } pub fn evaluate(&self, ast: &AstNode) -> Result<f64, Error> { match ast { AstNode::Number(n) => Ok(*n), AstNode::Identifier(ident) => { // Handle constants and variables match ident.as_str() { "pi" => Ok(std::f64::consts::PI), "e" => Ok(std::f64::consts::E), _ => Err(Error::UndefinedVariable(ident.clone())), } } AstNode::UnaryOp(op, operand) => { let value = self.evaluate(operand)?; self.evaluate_unary_op(*op, value) } AstNode::BinaryOp(op, left, right) => { let left_val = self.evaluate(left)?; let right_val = self.evaluate(right)?; self.evaluate_binary_op(*op, left_val, right_val) } AstNode::FunctionCall(name, args) => { let arg_values: Result<Vec<f64>, _> = args.iter().map(|arg| self.evaluate(arg)).collect(); let arg_values = arg_values?; if let Some(func) = self.functions.get(name) { func(&arg_values) } else { Err(Error::UndefinedFunction(name.clone())) } } } } fn evaluate_unary_op(&self, op: Operator, value: f64) -> Result<f64, Error> { match op { Operator::Sqrt => { if value < 0.0 { Err(Error::NegativeSquareRoot) } else { Ok(value.sqrt()) } } Operator::Sin => Ok(value.sin()), Operator::Cos => Ok(value.cos()), Operator::Tan => Ok(value.tan()), Operator::Ln => { if value <= 0.0 { Err(Error::InvalidLogarithm) } else { Ok(value.ln()) } } Operator::Factorial => { if value < 0.0 || value.fract() != 0.0 { return Err(Error::InvalidFactorialArgument); } let n = value as u64; if n > 20 { return Err(Error::FactorialTooLarge); } Ok((1..=n).product::<u64>() as f64) } _ => Err(Error::InvalidOperator), } } fn evaluate_binary_op(&self, op: Operator, left: f64, right: f64) -> Result<f64, Error> { match op { Operator::Add => Ok(left + right), Operator::Subtract => Ok(left - right), Operator::Multiply => Ok(left * right), Operator::Divide => { if right == 0.0 { Err(Error::DivisionByZero) } else { Ok(left / right) } } Operator::Power => Ok(left.powf(right)), _ => Err(Error::InvalidOperator), } } } }
2.7.6 Statistics module
#![allow(unused)] fn main() { // src/data/mod.rs pub mod types; pub mod statistics; use types::Statistics; // Re-export pub use statistics::Statistics; }
#![allow(unused)] fn main() { // src/data/statistics.rs use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Statistics { pub count: usize, pub mean: f64, pub median: f64, pub mode: Vec<f64>, pub variance: f64, pub std_dev: f64, pub min: f64, pub max: f64, pub sum: f64, } impl Statistics { pub fn print_detailed(&self) { println!("=== Statistics ==="); println!("Count: {}", self.count); println!("Sum: {:.2}", self.sum); println!("Mean: {:.2}", self.mean); println!("Median: {:.2}", self.median); println!("Mode: {:?}", self.mode.iter() .map(|&x| format!("{:.2}", x)) .collect::<Vec<_>>() .join(", ")); println!("Min: {:.2}", self.min); println!("Max: {:.2}", self.max); println!("Variance: {:.4}", self.variance); println!("Std dev: {:.4}", self.std_dev); println!("=============="); } pub fn get_range(&self) -> f64 { self.max - self.min } pub fn get_coefficient_of_variation(&self) -> f64 { if self.mean == 0.0 { 0.0 } else { self.std_dev / self.mean.abs() } } } // Linear regression pub struct LinearRegression { pub slope: f64, pub intercept: f64, pub r_squared: f64, } impl LinearRegression { pub fn new(x_data: &[f64], y_data: &[f64]) -> Option<Self> { if x_data.len() != y_data.len() || x_data.is_empty() { return None; } let n = x_data.len() as f64; let sum_x: f64 = x_data.iter().sum(); let sum_y: f64 = y_data.iter().sum(); let sum_xy: f64 = x_data.iter().zip(y_data.iter()) .map(|(&x, &y)| x * y).sum(); let sum_x2: f64 = x_data.iter().map(|&x| x * x).sum(); let sum_y2: f64 = y_data.iter().map(|&y| y * y).sum(); let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x); let intercept = (sum_y - slope * sum_x) / n; // Compute R² let ss_tot: f64 = y_data.iter() .map(|&y| (y - sum_y / n).powi(2)) .sum(); let ss_res: f64 = x_data.iter().zip(y_data.iter()) .map(|(&x, &y)| { let predicted = slope * x + intercept; (y - predicted).powi(2) }) .sum(); let r_squared = 1.0 - (ss_res / ss_tot); Some(Self { slope, intercept, r_squared, }) } pub fn predict(&self, x: f64) -> f64 { self.slope * x + self.intercept } pub fn print_equation(&self) { println!("Linear regression: y = {:.4}x + {:.4}", self.slope, self.intercept); println!("Coefficient of determination (R²): {:.4}", self.r_squared); } } }
2.7.7 History management
#![allow(unused)] fn main() { // src/history/mod.rs use serde::{Deserialize, Serialize}; use std::fs::{self, File}; use std::io::{self, BufRead, BufReader, Write}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HistoryRecord { pub expression: String, pub result: f64, pub timestamp: chrono::DateTime<chrono::Utc>, } pub struct HistoryManager { records: Vec<HistoryRecord>, max_records: usize, } impl HistoryManager { pub fn new() -> Self { Self::with_capacity(100) } pub fn with_capacity(capacity: usize) -> Self { let records = Self::load_from_file().unwrap_or_default(); Self { records, max_records: capacity, } } pub fn add_record(&mut self, expression: &str, result: f64) { let record = HistoryRecord { expression: expression.to_string(), result, timestamp: chrono::Utc::now(), }; self.records.push(record); // Enforce max history size if self.records.len() > self.max_records { self.records.remove(0); } // Persist to disk self.save_to_file().ok(); } pub fn get_recent_records(&self, count: usize) -> &[HistoryRecord] { let start = if self.records.len() > count { self.records.len() - count } else { 0 }; &self.records[start..] } pub fn search_records(&self, query: &str) -> Vec<&HistoryRecord> { self.records .iter() .filter(|record| record.expression.contains(query) || record.result.to_string().contains(query) ) .collect() } pub fn clear(&mut self) { self.records.clear(); self.save_to_file().ok(); } pub fn display(&self) { if self.records.is_empty() { println!("No calculation history yet."); return; } println!("=== History ==="); for (i, record) in self.records.iter().enumerate() { println!("{}. {} = {}", i + 1, record.expression, record.result ); } println!("============="); } fn get_history_file() -> std::path::PathBuf { let mut path = dirs::home_dir().unwrap_or_default(); path.push(".rust_calculator_history.json"); path } fn load_from_file() -> io::Result<Vec<HistoryRecord>> { let path = Self::get_history_file(); if !path.exists() { return Ok(Vec::new()); } let file = File::open(path)?; let reader = BufReader::new(file); let records: Vec<HistoryRecord> = serde_json::from_reader(reader) .unwrap_or_default(); Ok(records) } fn save_to_file(&self) -> io::Result<()> { let path = Self::get_history_file(); // Ensure directory exists if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } let file = File::create(path)?; serde_json::to_writer_pretty(file, &self.records)?; Ok(()) } } }
2.7.8 Error handling
#![allow(unused)] fn main() { // src/utils/mod.rs pub mod error; pub use error::Error; }
#![allow(unused)] fn main() { // src/utils/error.rs use serde::{Deserialize, Serialize}; #[derive(Debug, thiserror::Error, Serialize, Deserialize)] pub enum Error { #[error("division by zero")] DivisionByZero, #[error("square root of a negative number: {0}")] NegativeSquareRoot, #[error("invalid logarithm: base must be > 0 and != 1, argument must be > 0")] InvalidLogarithm, #[error("invalid factorial argument: must be a non-negative integer")] InvalidFactorialArgument, #[error("factorial too large: n > 20")] FactorialTooLarge, #[error("empty dataset")] EmptyDataSet, #[error("invalid number: {0}")] InvalidNumber(String), #[error("invalid character: {0}")] InvalidCharacter(char), #[error("mismatched parentheses")] MismatchedParen, #[error("invalid expression")] InvalidExpression, #[error("insufficient operands")] InsufficientOperands, #[error("invalid operator")] InvalidOperator, #[error("undefined variable: {0}")] UndefinedVariable(String), #[error("undefined function: {0}")] UndefinedFunction(String), #[error("function {0} argument count mismatch: expected {1}, got {2}")] InvalidArgumentCount(String, usize, usize), #[error("I/O error: {0}")] Io(#[from] std::io::Error), #[error("JSON serialization error: {0}")] Json(#[from] serde_json::Error), #[error("time parsing error: {0}")] Chrono(#[from] chrono::ParseError), } }
2.8 Exercises
Exercise 2.1: Basic calculator
Implement a basic four-operation calculator:
- Support
+,-,*,/ - Handle errors (division by zero, etc.)
- Provide a user-friendly interface
Exercise 2.2: Temperature converter
Create a temperature conversion tool:
- Celsius ↔ Fahrenheit
- Celsius ↔ Kelvin
- Batch conversion
- Show conversion history
Exercise 2.3: Data analysis tool
Build a simple data processor:
- Read a CSV file
- Compute basic statistics
- Find extremes and outliers
- Generate a report
Exercise 2.4: Unit converter
Design a unit conversion system:
- Length units (meters, centimeters, feet, etc.)
- Weight units (kilograms, pounds, ounces, etc.)
- Temperature units
- Custom conversion functions
Exercise 2.5: Tuple data processor
Create a tool that processes tuple-based data:
- Parse student info tuples (name, age, score)
- Implement coordinate geometry (distance, midpoint, etc.)
- Time conversions (hours, minutes, seconds)
- Practice returning multiple values
Exercise 2.6: Array data analyzer
Build an array data processing program:
- Sorting, searching, and statistics on arrays
- Multidimensional array operations (matrix math)
- Replacing and deleting elements
- Classic algorithms (bubble sort, binary search, etc.)
2.9 Performance tips
2.9.1 Numeric computation
#![allow(unused)] fn main() { // Avoid repeated computation fn optimized_calculation(data: &[f64]) -> (f64, f64) { let n = data.len() as f64; let sum: f64 = data.iter().sum(); let mean = sum / n; // Compute variance in a single pass (given mean) let variance: f64 = data.iter() .map(|&x| (x - mean).powi(2)) .sum::<f64>() / n; let std_dev = variance.sqrt(); (mean, std_dev) } // Use iterator pipelines fn iterator_optimization() { let numbers: Vec<i32> = (1..=1000).collect(); // Chained operations let result: i32 = numbers .iter() .filter(|&&x| x % 2 == 0) // keep even numbers .map(|&x| x * x) // square .sum(); // sum println!("Sum of squares of even numbers: {}", result); } }
2.9.2 Memory management
#![allow(unused)] fn main() { // Preallocate capacity fn preallocate_example() { let mut numbers = Vec::with_capacity(1000); for i in 0..1000 { numbers.push(i); } } // Avoid unnecessary cloning fn efficient_cloning() { let original = vec![1, 2, 3, 4, 5]; // Use references instead of cloning let sum: i32 = original.iter().sum(); // Clone only when needed if sum > 10 { let cloned = original.clone(); // use cloned } } }
2.10 Summary
After this chapter, you should have learned:
Core concepts
- Variable bindings:
let,let mut,const - Primitive types: integers, floats, booleans, chars, strings
- Compound types: tuples (fixed-size heterogeneous), arrays (fixed-size homogeneous)
- Control flow:
if,loop,while,for,match - Functions: definitions, calls, parameters, return values
Hands-on project
- A complete scientific calculator
- Expression parsing and evaluation
- Statistical analysis
- History management
Best practices
- Naming conventions
- Error handling strategies
- Performance tips
- Code organization
Next chapter preview
- Ownership and borrowing
- Memory safety guarantees
- References and slices
- Lifetimes
With these fundamentals and the hands-on project practice, you now have a solid foundation in Rust. Next up: Rust’s signature feature—ownership and borrowing!
Chapter 3: Ownership and Borrowing
Table of contents
- 3.1 Introduction: Rust’s memory safety revolution
- 3.2 Ownership basics
- 3.3 References and borrowing
- 3.4 Lifetimes
- 3.5 Smart pointers
- 3.6 Practical project: Build a memory-safe file processing tool
- 3.7 Best practices
- 3.8 Summary
- 3.9 Acceptance criteria
- 3.10 Exercises
- 3.11 Further reading
Learning objectives
After this chapter, you will be able to:
- Understand the core concepts of Rust’s ownership system
- Explain how the borrow checker works
- Use lifetimes to ensure memory safety
- Handle files safely and reason about memory management
- Apply these ideas in a practical project: a memory-safe file processing tool
3.1 Introduction: Rust’s memory safety revolution
In modern programming, memory safety is a critical concern. Traditional systems languages like C and C++ can deliver great performance, but require manual memory management, which often leads to:
- Memory leaks: forgetting to free allocated memory
- Double free: freeing the same allocation twice
- Dangling pointers: accessing memory that has already been freed
- Buffer overflows: writing past the end of an allocated region
Rust achieves memory safety at compile time through its unique ownership system—without relying on a garbage collector. This is the key mechanism that allows Rust to compete with C++ on performance while providing strong memory-safety guarantees.
3.2 Ownership basics
3.2.1 What is ownership?
In Rust, every value has exactly one owner. When the owner goes out of scope, the value is automatically dropped.
The three core ownership rules (memorize these):
| Rule | Name | What it means | What happens if you violate it |
|---|---|---|---|
| 1 | One owner per value | At any time, only one variable “owns” a piece of memory | Compile error |
| 2 | Ownership can move | Assigning to another variable or passing by value moves ownership | The old binding becomes invalid |
| 3 | Dropped at end of scope | When the owner leaves its {} scope, Rust runs drop() automatically | Normal behavior |
Let’s start with a small example:
fn main() { let s1 = String::from("Hello"); let s2 = s1; // ownership moves from s1 to s2 # 3.7 Best practices ## 3.7.1 Ownership guidelines 1. **Prefer borrowing**: use references instead of taking ownership unless ownership is required. 2. **Move rather than clone**: for large data structures, prefer moving ownership rather than duplicating data. 3. **Choose the right smart pointer**: - Use `Box<T>` to own a value on the heap (single ownership) - Use `Rc<T>` for shared ownership within a single thread - Use `Arc<T>` for shared ownership across threads ## 3.7.2 Borrow-checker techniques 1. **Separate immutable and mutable borrows**: ```rust let mut data = vec![1, 2, 3]; let first = &data[0]; // Cannot take a mutable borrow here // let first_mut = &mut data[0]; ``` 2. **Use interior mutability when appropriate**: ```rust use std::cell::RefCell; let data = RefCell::new(vec![1, 2, 3]); data.borrow_mut().push(4); // mutate behind an immutable binding ``` 3. **Use lifetime parameters when needed**: ```rust fn longest_with_announcement<'a, T>( x: &'a str, y: &'a str, ann: T, ) -> &'a str where T: std::fmt::Display, { println!("Announcement: {}", ann); if x.len() > y.len() { x } else { y } } ``` ## 3.7.3 Performance optimization 1. **Avoid unnecessary ownership moves**: ```rust // Not ideal fn process_string(s: String) -> String { // process s } // Better fn process_string(s: &str) -> String { // process s.to_string() } ``` 2. **Use zero-copy techniques**: ```rust fn parse_csv_line(line: &str) -> (&str, &str, &str) { // Use slices instead of allocating new strings let mut iter = line.split(','); ( iter.next().unwrap(), iter.next().unwrap(), iter.next().unwrap(), ) } ``` 3. **Batch work to reduce allocations**: ```rust fn process_batch(items: &[Item], batch_size: usize) { for chunk in items.chunks(batch_size) { process_chunk(chunk); } } ``` # 3.8 Summary This chapter covered Rust’s ownership system—one of the language’s defining features. You learned to: 1. **Understand ownership**: every value has one owner and is dropped when the owner goes out of scope 2. **Use borrowing safely**: references let you use values without transferring ownership 3. **Manage lifetimes**: ensure references remain valid 4. **Use smart pointers**: handle more complex ownership scenarios 5. **Build a practical tool**: implement a memory-safe file processing project Rust’s ownership model makes it possible to achieve memory safety without sacrificing performance. In real-world code, using borrowing, smart pointers, and lifetime annotations effectively helps you build Rust programs that are both safe and fast. # 3.9 Acceptance criteria After completing this chapter, you should be able to: - [ ] Explain the relationship between ownership, borrowing, and lifetimes - [ ] Identify and fix borrow-checker errors - [ ] Choose an appropriate smart pointer type for a given scenario - [ ] Implement a memory-safe file processing program - [ ] Write efficient batch-processing code - [ ] Design production-grade error handling and recovery mechanisms # 3.10 Exercises 1. **Ownership transfer**: implement a function that takes ownership of a `Vec<T>` and returns a processed `Vec<T>`. 2. **Borrowing refactor**: refactor code to borrow instead of moving ownership. 3. **Lifetime annotations**: add correct lifetime parameters to more complex functions. 4. **Performance comparison**: compare performance between borrowing and ownership-moving approaches. 5. **Error handling**: add retry and rollback behavior to the file processing tool. # 3.11 Further reading - [The Rust Book: Ownership](https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html) - [The Rustonomicon: Ownership](https://doc.rust-lang.org/nomicon/ownership.html) - [Effective Rust](https://www.lurklurk.org/effective-rust/) ```shell The length of 'hello' is 5.
3.3.2 Mutable references
If you need to modify a value through a reference, you can use a mutable reference:
fn main() { let mut s = String::from("hello"); change(&mut s); // pass a mutable reference println!("{}", s); // prints "hello, world" } fn change(some_string: &mut String) { some_string.push_str(", world"); }
Result:
hello, world
Important restrictions for mutable references:
- At any given time, you can have either one mutable reference or any number of immutable references to a value.
- You cannot have a mutable reference and immutable references to the same value at the same time.
fn main() { let mut s = String::from("hello"); let r1 = &s; // immutable reference let r2 = &s; // another immutable reference // let r3 = &mut s; // compile error: cannot borrow as mutable while borrowed as immutable println!("{} and {}", r1, r2); println!("{}", r1); // Now it's okay to create a mutable reference because r1/r2 are no longer used let r3 = &mut s; println!("{}", r3); }
Result:
hello and hello
hello
hello
3.3.3 The borrow checker
Rust’s borrow checker validates references at compile time and guarantees:
- No dangling references
- References never outlive the data they refer to
fn main() { let r; { let x = 5; r = &x; // compile error: x does not live long enough } println!("{}", r); }
Compiling playground v0.0.1 (/playground)
error[E0597]: `x` does not live long enough
--> src/main.rs:5:13
|
4 | let x = 5;
| - binding `x` declared here
5 | r = &x; // compile error: x does not live long enough
| ^^ borrowed value does not live long enough
6 | }
| - `x` dropped here while still borrowed
7 | println!("{}", r);
| - borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `playground` (bin "playground") due to 1 previous error
In this example, x is dropped at the end of the inner scope, but r is used afterwards. That would create a dangling reference, so Rust rejects it.
3.4 Lifetimes
3.4.1 What is a lifetime?
A lifetime is the scope for which a reference is valid. Rust must ensure that references are always valid, which is why the borrow checker tracks lifetimes.
At its core, lifetime checking answers one question:
Will any reference outlive the data it points to?
The key rule:
A borrow cannot last longer than the value being borrowed.
In many cases, Rust can infer lifetimes automatically. But when a function returns a reference, Rust sometimes needs help.
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {}", result); } fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } }
Compiling playground v0.0.1 (/playground)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `playground` (bin "playground") due to 1 previous error
Why does this fail? The function returns a &str, but the compiler cannot tell whether the returned reference is tied to x or to y. Rust needs to know:
- Which input lifetime the output reference depends on
- That the returned reference will not outlive its source
- How to reason about the case where either
xorymight be returned
We can fix it by adding an explicit lifetime parameter:
#![allow(unused)] fn main() { fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } }
Meaning of the annotation:
'ais a generic lifetime parameter.x: &'a strmeansxis valid for at least'a.y: &'a strmeansyis valid for at least'a.-> &'a strmeans the returned reference is also valid for'a.
In practice, this tells the compiler: the returned reference will be valid for the shorter of the two input lifetimes.
Full fixed example:
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {}", result); } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
Result:
The longest string is abcd
3.4.2 Lifetime annotations
When a function returns a reference, you often need to annotate lifetimes explicitly:
#![allow(unused)] fn main() { fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } }
Here, 'a means the returned reference lives at least as long as the shorter of the two input references.
3.4.3 Lifetimes in structs
If a struct contains references, it must declare lifetime parameters:
struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } 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); }
Result:
Excerpt: Call me Ishmael
How to read this example:
novelowns the heap-allocated string data.first_sentenceis a&strslice that references data insidenovel.- The struct
ImportantExcerpt<'a>stores a reference, so its instance cannot outlive the referenced data. - The compiler can often infer the correct relationships here, but the struct still needs an explicit lifetime parameter because it contains a reference.
3.5 Smart pointers
3.5.1 Box<T>: heap allocation with single ownership
Box<T> is the most basic smart pointer in Rust. It allocates data on the heap instead of the stack. This is useful when data is large or when the size is only known at runtime.
Box<T> implements Deref and Drop, so you can access inner data via * (or through auto-deref), and the heap allocation is freed automatically when the Box goes out of scope.
Box<T> is also commonly used to represent recursive types (e.g., linked lists, trees), because the compiler needs to know a type’s size at compile time; boxing introduces indirection and makes the size known.
fn main() { let b = Box::new(5); println!("b = {}", b); } // b is dropped automatically
Result:
b = 5
3.5.2 Rc<T>: reference counting (single-threaded)
Rc<T> (Reference Counting) enables shared ownership: multiple owners can point to the same allocation. It works by tracking a reference count; cloning an Rc increments the count, dropping an Rc decrements it, and when the count reaches zero the data is freed.
Rc<T> is not thread-safe because its reference counting is non-atomic. Use it only when the data stays within a single thread.
Also note: data behind Rc<T> is generally immutable. If you need shared mutability, combine Rc<T> with interior mutability (e.g., RefCell<T>).
use std::rc::Rc; fn main() { let s = Rc::new(String::from("hello")); let s1 = Rc::clone(&s); let s2 = Rc::clone(&s); println!("s: {}, ref count: {}", s, Rc::strong_count(&s)); println!("s1: {}, ref count: {}", s1, Rc::strong_count(&s)); println!("s2: {}, ref count: {}", s2, Rc::strong_count(&s)); } // after s1/s2 are dropped, s is dropped too
Result:
s: hello, ref count: 3
s1: hello, ref count: 3
s2: hello, ref count: 3
3.5.3 Arc<T>: atomic reference counting (multi-threaded)
Arc<T> (Atomic Reference Counting) is the thread-safe version of Rc<T>. It uses atomic operations to update the reference count, which is safe across threads but has extra overhead compared to Rc<T>.
Like Rc<T>, Arc<T> is mainly for shared read-only data. For shared mutability, combine it with synchronization primitives such as Mutex<T>, RwLock<T>, or atomics.
use std::sync::Arc; fn main() { let s = Arc::new(String::from("hello")); let s1 = Arc::clone(&s); let s2 = Arc::clone(&s); println!("Reference count: {}", Arc::strong_count(&s)); println!("s1: {}", s1); println!("s2: {}", s2); }
Result:
Reference count: 3
s1: hello
s2: hello
3.6 Practical project: Build a memory-safe file processing tool
Now let’s implement a complete file-processing tool to demonstrate ownership, borrowing, and lifetimes in a realistic setting.
3.6.1 Project design
Project name: rust-file-processor
Core features:
- Safe file reading (avoid leaks and unsafe patterns)
- Streaming processing for large files
- Batch file renaming
- File integrity verification
- Concurrent file processing
3.6.2 Project layout
rust-file-processor/
├── src/
│ ├── main.rs
│ ├── processors/
│ │ ├── mod.rs
│ │ ├── csv.rs
│ │ ├── json.rs
│ │ ├── text.rs
│ │ └── image.rs
│ ├── utilities/
│ │ ├── mod.rs
│ │ ├── file_ops.rs
│ │ ├── encoding.rs
│ │ └── validation.rs
│ ├── concurrent/
│ │ ├── mod.rs
│ │ ├── worker.rs
│ │ └── pool.rs
│ └── config/
│ ├── mod.rs
│ └── settings.rs
├── tests/
├── examples/
└── fixtures/
├── sample.csv
├── sample.json
└── large_file.txt
3.6.3 Core implementation
3.6.3.1 A safe file reader
src/utilities/file_ops.rs
#![allow(unused)] fn main() { use std::fs::File; use std::io::{BufRead, BufReader, BufWriter, Read, Write}; use std::path::{Path, PathBuf}; use std::error::Error; use std::sync::Arc; use rayon::prelude::*; #[derive(Debug)] pub struct FileReader { path: Arc<PathBuf>, buffer_size: usize, encoding: Encoding, } #[derive(Debug, Clone, Copy)] pub enum Encoding { UTF8, GBK, ASCII, } impl FileReader { pub fn new<P: Into<PathBuf>>(path: P) -> Self { Self { path: Arc::new(path.into()), buffer_size: 8192, encoding: Encoding::UTF8, } } pub fn with_buffer_size(mut self, size: usize) -> Self { self.buffer_size = size; self } pub fn with_encoding(mut self, encoding: Encoding) -> Self { self.encoding = encoding; self } /// Use borrowing to avoid moving ownership. pub fn process_lines<F, T>(&self, processor: F) -> Result<T, Box<dyn Error>> where F: Fn(&str) -> Result<T, Box<dyn Error>> + Send + Sync, T: Send, { let file = File::open(&*self.path)?; let reader = BufReader::new(file); // Stream processing to avoid loading the entire file into memory. let lines = reader.lines().filter_map(|line| match line { Ok(line) => Some(line), Err(e) => { eprintln!("Warning: Skipping invalid line: {}", e); None } }); // Process lines in parallel (processor borrows each line). let results: Vec<Result<T, Box<dyn Error>>> = lines .par_iter() .map(|line| processor(line)) .collect(); // Collect results; if any processing fails, return the error. let mut processed_results = Vec::new(); for result in results { match result { Ok(result) => processed_results.push(result), Err(e) => return Err(e), } } Ok(self.combine_results(processed_results)) } /// Batch process files. pub fn process_batch<P, F, T>(&self, files: &[P], processor: F) -> Result<Vec<T>, Box<dyn Error>> where P: AsRef<Path> + Send + Sync, F: Fn(&Path) -> Result<T, Box<dyn Error>> + Send + Sync, T: Send, { files.par_iter().map(|path| { processor(path.as_ref()) }).collect() } /// Stream-process a large file. pub fn stream_process<P, F, T>(&self, output: &P, processor: F) -> Result<T, Box<dyn Error>> where P: AsRef<Path>, F: Fn(&str) -> Result<String, Box<dyn Error>>, { let input_file = File::open(&*self.path)?; let output_file = File::create(output.as_ref())?; let mut reader = BufReader::new(input_file); let mut writer = BufWriter::new(output_file); let mut buffer = String::new(); let mut results = Vec::new(); while reader.read_line(&mut buffer)? > 0 { let processed_line = processor(&buffer)?; writeln!(writer, "{}", processed_line)?; results.push(processed_line); buffer.clear(); } Ok(self.combine_results(results)) } /// Verify file integrity. pub fn verify_integrity(&self) -> Result<bool, Box<dyn Error>> { let metadata = self.path.metadata()?; let file_size = metadata.len(); // Simple integrity check: verify the file can be fully read. let file = File::open(&*self.path)?; let mut reader = BufReader::new(file); let mut buffer = Vec::new(); reader.read_to_end(&mut buffer)?; Ok(buffer.len() == file_size as usize) } /// Rename the file. pub fn rename<P: AsRef<Path>>(&self, new_path: P) -> Result<(), Box<dyn Error>> { std::fs::rename(&*self.path, new_path.as_ref())?; Ok(()) } /// Get file metadata. pub fn metadata(&self) -> Result<std::fs::Metadata, Box<dyn Error>> { self.path.metadata().map_err(|e| e.into()) } /// Combine processed results (type-specific). fn combine_results(&self, results: Vec<T>) -> T { // In a real implementation, you'd combine results based on T. // Simplified example: if !results.is_empty() { results.into_iter().next().unwrap() } else { // Return an appropriate default value based on T. todo!("Return appropriate default value based on type") } } } impl Clone for FileReader { fn clone(&self) -> Self { Self { path: Arc::clone(&self.path), buffer_size: self.buffer_size, encoding: self.encoding, } } } }
3.6.3.2 File encoding handling
src/utilities/encoding.rs
#![allow(unused)] fn main() { use std::io::{Read, Write, Result as IoResult}; use std::str; use encoding_rs::{GBK, UTF_8}; use encoding_rs_io::DecodeReaderBytesBuilder; pub enum TextEncoding { UTF8, GBK, ASCII, } impl TextEncoding { pub fn from_name(name: &str) -> Option<Self> { match name.to_lowercase().as_str() { "utf-8" | "utf8" => Some(TextEncoding::UTF8), "gbk" | "gb2312" => Some(TextEncoding::GBK), "ascii" => Some(TextEncoding::ASCII), _ => None, } } pub fn decode(&self, bytes: &[u8]) -> Result<String, Box<dyn std::error::Error>> { match self { TextEncoding::UTF8 => { Ok(String::from_utf8(bytes.to_vec())?) } TextEncoding::GBK => { let (decoded, _, _) = GBK.decode(bytes); Ok(decoded.into()) } TextEncoding::ASCII => { Ok(String::from_utf8(bytes.to_vec())?) } } } pub fn encode(&self, text: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> { match self { TextEncoding::UTF8 => { Ok(text.as_bytes().to_vec()) } TextEncoding::GBK => { let (encoded, _, _) = GBK.encode(text); Ok(encoded.to_vec()) } TextEncoding::ASCII => { Ok(text.as_bytes().to_vec()) } } } } /// A generic encoding-aware reader. pub struct EncodingReader<R> { reader: R, encoding: TextEncoding, } impl<R: Read> EncodingReader<R> { pub fn new(reader: R, encoding: TextEncoding) -> Self { Self { reader, encoding } } pub fn read_to_string(&mut self) -> Result<String, Box<dyn std::error::Error>> { let mut buffer = Vec::new(); self.reader.read_to_end(&mut buffer)?; self.encoding.decode(&buffer) } pub fn read_lines(&mut self) -> Result<Vec<String>, Box<dyn std::error::Error>> { let content = self.read_to_string()?; Ok(content.lines().map(|line| line.to_string()).collect()) } } }
3.6.3.3 Concurrent processing
src/concurrent/worker.rs
#![allow(unused)] fn main() { use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; use std::path::{Path, PathBuf}; use std::error::Error; use rayon::prelude::*; use futures::executor::ThreadPool; type Job = Box<dyn FnOnce() + Send + 'static>; pub struct WorkerPool { workers: Vec<Worker>, sender: crossbeam::channel::Sender<Job>, } struct Worker { id: usize, thread: Option<thread::JoinHandle<()>>, } impl WorkerPool { pub fn new(size: usize) -> WorkerPool { assert!(size > 0); let (sender, receiver) = crossbeam::channel::unbounded(); let receiver = Arc::new(Mutex::new(receiver)); let mut workers = Vec::with_capacity(size); for id in 0..size { workers.push(Worker::new(id, Arc::clone(&receiver))); } WorkerPool { workers, sender } } pub fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static, { let job = Box::new(f); self.sender.send(job).unwrap(); } } impl Worker { fn new(id: usize, receiver: Arc<crossbeam::channel::Receiver<Job>>) -> Worker { let thread = thread::spawn(move || loop { let job = receiver.lock().unwrap().recv(); match job { Ok(job) => { job(); } Err(_) => { // Shutdown signal received break; } } }); Worker { id, thread: Some(thread), } } } impl Drop for WorkerPool { fn drop(&mut self) { // Close the channel by dropping the sender (signals shutdown). drop(self.sender); for worker in &mut self.workers { if let Some(thread) = worker.thread.take() { thread.join().unwrap(); } } } } /// A file-processing job. pub struct FileProcessingJob { files: Vec<PathBuf>, processor: Arc<dyn Fn(&Path) -> Result<(), Box<dyn Error>> + Send + Sync>, progress: Arc<Mutex<Progress>>, } #[derive(Debug, Default)] pub struct Progress { pub total: usize, pub completed: usize, pub failed: usize, } impl FileProcessingJob { pub fn new( files: Vec<PathBuf>, processor: Arc<dyn Fn(&Path) -> Result<(), Box<dyn Error>> + Send + Sync>, ) -> Self { let total = files.len(); Self { files, processor, progress: Arc::new(Mutex::new(Progress { total, ..Default::default() })), } } pub fn execute(&self, pool: &WorkerPool) -> Result<Progress, Box<dyn Error>> { // Use rayon for parallel processing. let results: Vec<Result<(), Box<dyn Error>>> = self.files .par_iter() .map(|file| { let result = (self.processor)(file); { let mut progress = self.progress.lock().unwrap(); if result.is_ok() { progress.completed += 1; } else { progress.failed += 1; } } result }) .collect(); // If any task failed, return the error. for result in results { result?; } Ok(self.progress.lock().unwrap().clone()) } pub fn get_progress(&self) -> Progress { self.progress.lock().unwrap().clone() } } }
3.6.3.4 Text processor
src/processors/text.rs
#![allow(unused)] fn main() { use crate::utilities::file_ops::FileReader; use crate::utilities::encoding::TextEncoding; use std::path::Path; use std::error::Error; use std::collections::HashMap; pub struct TextProcessor { encoding: TextEncoding, ignore_patterns: Vec<String>, } impl TextProcessor { pub fn new(encoding: TextEncoding) -> Self { Self { encoding, ignore_patterns: Vec::new(), } } pub fn add_ignore_pattern(&mut self, pattern: String) { self.ignore_patterns.push(pattern); } /// Find and replace text. pub fn find_and_replace<P, Q>( &self, input: P, output: Q, replacements: &HashMap<&str, &str>, ) -> Result<usize, Box<dyn Error>> where P: AsRef<Path>, Q: AsRef<Path>, { let reader = FileReader::new(input).with_encoding(self.encoding); reader.stream_process(output, |line| { let mut result = line.to_string(); for (from, to) in replacements { result = result.replace(from, to); } Ok(result) })?; Ok(replacements.len()) } /// Extract text statistics. pub fn analyze_text(&self, file: &Path) -> Result<TextStats, Box<dyn Error>> { let reader = FileReader::new(file); let stats = reader.process_lines(|line| { Ok(( line.len(), line.chars().count(), line.split_whitespace().count(), )) })?; Ok(stats) } /// Deduplicate text lines. pub fn deduplicate<P, Q>(&self, input: P, output: Q) -> Result<usize, Box<dyn Error>> where P: AsRef<Path>, Q: AsRef<Path>, { let mut lines = std::fs::read_to_string(input)?; // Deduplicate while preserving order. lines.lines().collect::<std::collections::HashSet<_>>(); let reader = FileReader::new(input).with_encoding(self.encoding); let mut unique_lines = Vec::new(); reader.process_lines(|line| { if !unique_lines.contains(&line) { unique_lines.push(line); } Ok(()) })?; let output_content = unique_lines.join("\n"); std::fs::write(output, output_content)?; Ok(unique_lines.len()) } } #[derive(Debug, Default)] pub struct TextStats { pub total_lines: usize, pub total_chars: usize, pub total_words: usize, pub avg_line_length: f64, pub longest_line: usize, pub shortest_line: usize, } impl std::fmt::Display for TextStats { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Text Statistics: Total lines: {} Total characters: {} Total words: {} Average line length: {:.2} Longest line: {} characters Shortest line: {} characters", self.total_lines, self.total_chars, self.total_words, self.avg_line_length, self.longest_line, self.shortest_line) } } }
3.6.3.5 Main program
src/main.rs
use rust_file_processor::processors::text::TextProcessor; use rust_file_processor::utilities::encoding::TextEncoding; use rust_file_processor::concurrent::worker::FileProcessingJob; use std::path::PathBuf; use std::collections::HashMap; use std::sync::Arc; use std::error::Error; use clap::{Arg, Command}; use indicatif::{ProgressBar, ProgressStyle}; use rayon; fn main() -> Result<(), Box<dyn Error>> { // Configure the rayon thread pool. rayon::ThreadPoolBuilder::new() .thread_name(|i| format!("worker-{}", i)) .build_global()?; let matches = Command::new("rust-file-processor") .version("1.0") .about("Memory-safe file processing tool") .subcommand_required(true) .arg_required_else_help(true) .subcommand( Command::new("process") .about("Process files with text transformations") .arg(Arg::new("input") .required(true) .help("Input file or directory")) .arg(Arg::new("output") .required(true) .help("Output file or directory")) .arg(Arg::new("encoding") .long("encoding") .value_name("ENCODING") .help("Text encoding (utf-8, gbk, ascii)") .default_value("utf-8")) .arg(Arg::new("replace") .long("replace") .value_name("FROM=TO") .help("Text replacement in format FROM=TO") .multiple_values(true)) ) .subcommand( Command::new("analyze") .about("Analyze text files") .arg(Arg::new("input") .required(true) .help("Input file or directory")) .arg(Arg::new("encoding") .long("encoding") .value_name("ENCODING") .help("Text encoding") .default_value("utf-8")) ) .subcommand( Command::new("verify") .about("Verify file integrity") .arg(Arg::new("input") .required(true) .help("Input file or directory")) ) .get_matches(); match matches.subcommand() { Some(("process", args)) => { let input = PathBuf::from(args.value_of("input").unwrap()); let output = PathBuf::from(args.value_of("output").unwrap()); let encoding = TextEncoding::from_name(args.value_of("encoding").unwrap()) .ok_or("Invalid encoding")?; let mut processor = TextProcessor::new(encoding); if let Some(replacements) = args.values_of("replace") { let mut replace_map = HashMap::new(); for replacement in replacements { if let Some((from, to)) = replacement.split_once('=') { replace_map.insert(from, to); } } if input.is_file() { let files = vec![input.clone()]; process_files_batch(&files, &output, &replace_map, &processor)?; } else { process_directory_batch(&input, &output, &replace_map, &processor)?; } } println!("Processing completed successfully!"); } Some(("analyze", args)) => { let input = PathBuf::from(args.value_of("input").unwrap()); let encoding = TextEncoding::from_name(args.value_of("encoding").unwrap()) .ok_or("Invalid encoding")?; let processor = TextProcessor::new(encoding); if input.is_file() { let stats = processor.analyze_text(&input)?; println!("{}", stats); } else { analyze_directory(&input, &processor)?; } } Some(("verify", args)) => { let input = PathBuf::from(args.value_of("input").unwrap()); if input.is_file() { let result = rust_file_processor::utilities::file_ops::FileReader::new(&input) .verify_integrity()?; println!("File integrity: {}", if result { "OK" } else { "FAILED" }); } else { verify_directory(&input)?; } } _ => unreachable!(), } Ok(()) } fn process_files_batch( files: &[PathBuf], output: &PathBuf, replacements: &HashMap<&str, &str>, processor: &TextProcessor, ) -> Result<(), Box<dyn Error>> { let total_files = files.len(); let progress_bar = ProgressBar::new(total_files as u64); progress_bar.set_style( ProgressStyle::default_bar() .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} {msg}") .unwrap() ); for (i, file) in files.iter().enumerate() { let output_file = output.join(file.file_name().unwrap()); let replacements = replacements.clone(); let processor = processor.clone(); let result = processor.find_and_replace(file, &output_file, &replacements); match result { Ok(_) => { progress_bar.set_message(format!("Processing: {:?}", file.file_name().unwrap())); } Err(e) => { eprintln!("Error processing {:?}: {}", file, e); } } progress_bar.inc(1); } progress_bar.finish_with_message("Processing complete!"); Ok(()) } fn process_directory_batch( input_dir: &PathBuf, output_dir: &PathBuf, replacements: &HashMap<&str, &str>, processor: &TextProcessor, ) -> Result<(), Box<dyn Error>> { // Recursively find all files. let files: Vec<PathBuf> = walkdir::WalkDir::new(input_dir) .into_iter() .filter_map(|entry| entry.ok()) .filter(|entry| entry.file_type().is_file()) .map(|entry| entry.path().to_path_buf()) .collect(); std::fs::create_dir_all(output_dir)?; let total_files = files.len(); let progress_bar = ProgressBar::new(total_files as u64); progress_bar.set_style( ProgressStyle::default_bar() .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} {msg}") .unwrap() ); // Concurrent processing. let pool = rust_file_processor::concurrent::worker::WorkerPool::new(num_cpus::get()); for file in files { let output_file = output_dir.join(file.strip_prefix(input_dir).unwrap()); let replacements = replacements.clone(); let processor = processor.clone(); pool.execute(move || { let result = processor.find_and_replace(&file, &output_file, &replacements); match result { Ok(_) => println!("Processed: {:?}", file), Err(e) => eprintln!("Error processing {:?}: {}", file, e), } }); } progress_bar.set_message("Processing all files..."); drop(pool); // wait for all tasks to finish progress_bar.finish_with_message("Batch processing complete!"); Ok(()) } fn analyze_directory( input_dir: &PathBuf, processor: &TextProcessor, ) -> Result<(), Box<dyn Error>> { let files: Vec<PathBuf> = walkdir::WalkDir::new(input_dir) .into_iter() .filter_map(|entry| entry.ok()) .filter(|entry| entry.file_type().is_file()) .map(|entry| entry.path().to_path_buf()) .collect(); let progress_bar = ProgressBar::new(files.len() as u64); progress_bar.set_style( ProgressStyle::default_bar() .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} {msg}") .unwrap() ); for file in files { if let Ok(stats) = processor.analyze_text(&file) { println!("\nFile: {:?}", file); println!("{}", stats); } progress_bar.inc(1); } progress_bar.finish_with_message("Analysis complete!"); Ok(()) } fn verify_directory(input_dir: &PathBuf) -> Result<(), Box<dyn Error>> { let files: Vec<PathBuf> = walkdir::WalkDir::new(input_dir) .into_iter() .filter_map(|entry| entry.ok()) .filter(|entry| entry.file_type().is_file()) .map(|entry| entry.path().to_path_buf()) .collect(); let progress_bar = ProgressBar::new(files.len() as u64); progress_bar.set_style( ProgressStyle::default_bar() .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} {msg}") .unwrap() ); let mut failed_files = Vec::new(); for file in files { let result = rust_file_processor::utilities::file_ops::FileReader::new(&file) .verify_integrity(); match result { Ok(true) => println!("✓ {:?}", file), Ok(false) => { println!("✗ {:?}", file); failed_files.push(file); } Err(e) => { println!("✗ {:?} (Error: {})", file, e); failed_files.push(file); } } progress_bar.inc(1); } progress_bar.finish_with_message("Verification complete!"); if !failed_files.is_empty() { eprintln!("\nFailed files:"); for file in failed_files { eprintln!(" {:?}", file); } return Err("Some files failed integrity check".into()); } println!("\nAll files passed integrity check!"); Ok(()) }
3.6.3.6 Project configuration
Cargo.toml
[package]
name = "rust-file-processor"
version = "1.0.0"
edition = "2021"
authors = ["Your Name <your.email@example.com>"]
description = "Memory-safe file processing tool"
license = "MIT"
repository = "https://github.com/yourname/rust-file-processor"
[dependencies]
# Async and concurrency
rayon = "1.7"
crossbeam = "0.8"
futures = "0.3"
# File processing
walkdir = "2.4"
encoding_rs = "0.8"
encoding_rs_io = "0.1"
# CLI
clap = { version = "4.0", features = ["derive"] }
# User interface
indicatif = "0.17"
colored = "2.0"
# System information
num_cpus = "1.16"
# Error handling
anyhow = "1.0"
[dev-dependencies]
tempfile = "3.5"
criterion = "0.5"
[[example]]
name = "basic_usage"
path = "examples/basic_usage.rs"
[[example]]
name = "concurrent_processing"
path = "examples/concurrent_processing.rs"
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
3.6.4 Usage examples
3.6.4.1 Basic usage
examples/basic_usage.rs
use rust_file_processor::utilities::file_ops::FileReader; use rust_file_processor::utilities::encoding::TextEncoding; use rust_file_processor::processors::text::TextProcessor; use std::collections::HashMap; fn main() -> Result<(), Box<dyn std::error::Error>> { // Create a text processor. let processor = TextProcessor::new(TextEncoding::UTF8); // Read and analyze. let stats = processor.analyze_text("sample.txt")?; println!("Text statistics: {}", stats); // Text replacement. let mut replacements = HashMap::new(); replacements.insert("hello", "world"); replacements.insert("foo", "bar"); processor.find_and_replace("input.txt", "output.txt", &replacements)?; println!("Text replacement completed"); Ok(()) }
3.6.4.2 Concurrent processing example
examples/concurrent_processing.rs
use rust_file_processor::concurrent::worker::{FileProcessingJob, WorkerPool}; use std::path::PathBuf; use std::sync::Arc; use std::error::Error; fn main() -> Result<(), Box<dyn Error>> { // Prepare a list of files. let files = vec![ PathBuf::from("file1.txt"), PathBuf::from("file2.txt"), PathBuf::from("file3.txt"), ]; // Create a processing closure. let processor = Arc::new(|file: &Path| -> Result<(), Box<dyn Error>> { println!("Processing: {:?}", file); // Simulate file processing. std::thread::sleep(std::time::Duration::from_millis(100)); Ok(()) }); // Create the job. let job = FileProcessingJob::new(files, processor); // Create a worker pool. let pool = WorkerPool::new(num_cpus::get()); // Execute. let progress = job.execute(&pool)?; println!("Processing completed:"); println!(" Total: {}", progress.total); println!(" Completed: {}", progress.completed); println!(" Failed: {}", progress.failed); Ok(()) }
3.6.5 Performance tests
tests/performance.rs
#![allow(unused)] fn main() { use rust_file_processor::utilities::file_ops::FileReader; use std::path::Path; use std::time::Instant; #[test] fn test_large_file_processing() { // Create a large test file. let test_file = "test_large.txt"; create_large_file(test_file, 1024 * 1024); // 1MB let start = Instant::now(); // Test streaming processing. let reader = FileReader::new(test_file).with_buffer_size(8192); let line_count = reader.process_lines(|_| Ok(())); let duration = start.elapsed(); assert!(duration.as_millis() < 1000); // should finish within 1 second assert!(line_count.is_ok()); // Cleanup. std::fs::remove_file(test_file).ok(); } fn create_large_file(path: &str, size: usize) { use std::fs::File; use std::io::Write; let mut file = File::create(path).unwrap(); let line = "This is a test line for large file processing. ".repeat(10); for _ in 0..(size / line.len()) { writeln!(file, "{}", line).unwrap(); } } }
3.6.6 Production-grade considerations
3.6.6.1 Monitoring memory usage
In production, monitoring memory usage is crucial:
#![allow(unused)] fn main() { use sysinfo::{System, SystemExt, ProcessExt}; pub struct MemoryMonitor { sys: System, } impl MemoryMonitor { pub fn new() -> Self { Self { sys: System::new_all(), } } pub fn get_memory_usage(&mut self) -> MemoryUsage { self.sys.refresh_all(); MemoryUsage { total: self.sys.total_memory(), available: self.sys.available_memory(), used: self.sys.used_memory(), used_percent: (self.sys.used_memory() as f64 / self.sys.total_memory() as f64) * 100.0, } } pub fn warn_if_high_usage(&mut self, threshold: f64) -> bool { let usage = self.get_memory_usage(); if usage.used_percent > threshold { eprintln!("Warning: Memory usage is {}%", usage.used_percent); true } else { false } } } #[derive(Debug, Clone)] pub struct MemoryUsage { pub total: u64, pub available: u64, pub used: u64, pub used_percent: f64, } }
3.6.6.2 Atomic file operations
Ensure file writes are atomic:
#![allow(unused)] fn main() { use std::fs; use std::path::Path; use std::io::{self, Write, Read}; pub fn atomic_file_write<P: AsRef<Path>, C: AsRef<[u8]>>( path: P, content: C ) -> io::Result<()> { let temp_path = format!("{}.tmp", path.as_ref().display()); // Write to a temporary file. { let mut temp_file = fs::File::create(&temp_path)?; temp_file.write_all(content.as_ref())?; } // Atomically rename into place. fs::rename(&temp_path, path)?; Ok(()) } }
3.6.6.3 Error recovery
#![allow(unused)] fn main() { use std::collections::HashSet; use std::path::PathBuf; pub struct ErrorRecovery { processed_files: HashSet<PathBuf>, failed_files: Vec<PathBuf>, max_retries: usize, } impl ErrorRecovery { pub fn new(max_retries: usize) -> Self { Self { processed_files: HashSet::new(), failed_files: Vec::new(), max_retries, } } pub fn mark_processed(&mut self, file: PathBuf) { self.processed_files.insert(file); } pub fn mark_failed(&mut self, file: PathBuf) { self.failed_files.push(file); } pub fn retry_failed(&mut self) -> Result<(), Box<dyn std::error::Error>> { let mut retry_count = 0; while !self.failed_files.is_empty() && retry_count < self.max_retries { retry_count += 1; let failed = std::mem::take(&mut self.failed_files); for file in failed { if self.processed_files.contains(&file) { continue; // already processed } // Retry processing. match self.process_file(&file) { Ok(_) => { self.mark_processed(file); } Err(_) => { self.failed_files.push(file); // re-add to the failed list } } } } if !self.failed_files.is_empty() { return Err("Some files failed to process after retries".into()); } Ok(()) } fn process_file(&self, file: &Path) -> Result<(), Box<dyn std::error::Error>> { // Implement file processing logic. Ok(()) } } }
## 3.7 Best practices
### 3.7.1 Ownership guidelines
1. **Prefer borrowing**: use references instead of taking ownership unless ownership is required.
2. **Move rather than clone**: for large data structures, prefer moving ownership rather than duplicating data.
3. **Choose the right smart pointer**:
- Use `Box<T>` to own a value on the heap (single ownership)
- Use `Rc<T>` for shared ownership within a single thread
- Use `Arc<T>` for shared ownership across threads
### 3.7.2 Borrow-checker techniques
1. **Separate immutable and mutable borrows**:
```rust
let mut data = vec![1, 2, 3];
let first = &data[0];
// Cannot take a mutable borrow here
// let first_mut = &mut data[0];
```
2. **Use interior mutability when appropriate**:
```rust
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
data.borrow_mut().push(4); // mutate behind an immutable binding
```
3. **Use lifetime parameters when needed**:
```rust
fn longest_with_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: std::fmt::Display,
{
println!("Announcement: {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
```
### 3.7.3 Performance optimization
1. **Avoid unnecessary ownership moves**:
```rust
// Not ideal
fn process_string(s: String) -> String {
// process
s
}
// Better
fn process_string(s: &str) -> String {
// process
s.to_string()
}
```
2. **Use zero-copy techniques**:
```rust
fn parse_csv_line(line: &str) -> (&str, &str, &str) {
// Use slices instead of allocating new strings
let mut iter = line.split(',');
(
iter.next().unwrap(),
iter.next().unwrap(),
iter.next().unwrap(),
)
}
```
3. **Batch work to reduce allocations**:
```rust
fn process_batch(items: &[Item], batch_size: usize) {
for chunk in items.chunks(batch_size) {
process_chunk(chunk);
}
}
```
## 3.8 Summary
This chapter covered Rust’s ownership system—one of the language’s defining features. You learned to:
1. **Understand ownership**: every value has one owner and is dropped when the owner goes out of scope
2. **Use borrowing safely**: references let you use values without transferring ownership
3. **Manage lifetimes**: ensure references remain valid
4. **Use smart pointers**: handle more complex ownership scenarios
5. **Build a practical tool**: implement a memory-safe file processing project
Rust’s ownership model makes it possible to achieve memory safety without sacrificing performance. In real-world code, using borrowing, smart pointers, and lifetime annotations effectively helps you build Rust programs that are both safe and fast.
## 3.9 Acceptance criteria
After completing this chapter, you should be able to:
- [ ] Explain the relationship between ownership, borrowing, and lifetimes
- [ ] Identify and fix borrow-checker errors
- [ ] Choose an appropriate smart pointer type for a given scenario
- [ ] Implement a memory-safe file processing program
- [ ] Write efficient batch-processing code
- [ ] Design production-grade error handling and recovery mechanisms
## 3.10 Exercises
1. **Ownership transfer**: implement a function that takes ownership of a `Vec<T>` and returns a processed `Vec<T>`.
2. **Borrowing refactor**: refactor code to borrow instead of moving ownership.
3. **Lifetime annotations**: add correct lifetime parameters to more complex functions.
4. **Performance comparison**: compare performance between borrowing and ownership-moving approaches.
5. **Error handling**: add retry and rollback behavior to the file processing tool.
## 3.11 Further reading
- [The Rust Book: Ownership](https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html)
- [The Rustonomicon: Ownership](https://doc.rust-lang.org/nomicon/ownership.html)
- [Effective Rust](https://www.lurklurk.org/effective-rust/)
src/utilities/file_ops.rs
#![allow(unused)] fn main() { use std::fs::File; use std::io::{BufRead, BufReader, BufWriter, Read, Write}; use std::path::{Path, PathBuf}; use std::error::Error; use std::sync::Arc; use rayon::prelude::*; #[derive(Debug)] pub struct FileReader { path: Arc<PathBuf>, buffer_size: usize, encoding: Encoding, } #[derive(Debug, Clone, Copy)] pub enum Encoding { UTF8, GBK, ASCII, } impl FileReader { pub fn new<P: Into<PathBuf>>(path: P) -> Self { Self { path: Arc::new(path.into()), buffer_size: 8192, encoding: Encoding::UTF8, } } pub fn with_buffer_size(mut self, size: usize) -> Self { self.buffer_size = size; self } pub fn with_encoding(mut self, encoding: Encoding) -> Self { self.encoding = encoding; self } /// Use borrowing to avoid moving ownership. pub fn process_lines<F, T>(&self, processor: F) -> Result<T, Box<dyn Error>> where F: Fn(&str) -> Result<T, Box<dyn Error>> + Send + Sync, T: Send, { let file = File::open(&*self.path)?; let reader = BufReader::new(file); // Stream processing to avoid loading the entire file into memory. let lines = reader.lines().filter_map(|line| match line { Ok(line) => Some(line), Err(e) => { eprintln!("Warning: Skipping invalid line: {}", e); None } }); // Process lines in parallel (processor borrows each line). let results: Vec<Result<T, Box<dyn Error>>> = lines .par_iter() .map(|line| processor(line)) .collect(); // Collect results; if any processing fails, return the error. let mut processed_results = Vec::new(); for result in results { match result { Ok(result) => processed_results.push(result), Err(e) => return Err(e), } } Ok(self.combine_results(processed_results)) } /// Batch process files. pub fn process_batch<P, F, T>(&self, files: &[P], processor: F) -> Result<Vec<T>, Box<dyn Error>> where P: AsRef<Path> + Send + Sync, F: Fn(&Path) -> Result<T, Box<dyn Error>> + Send + Sync, T: Send, { files.par_iter().map(|path| { processor(path.as_ref()) }).collect() } /// Stream-process a large file. pub fn stream_process<P, F, T>(&self, output: &P, processor: F) -> Result<T, Box<dyn Error>> where P: AsRef<Path>, F: Fn(&str) -> Result<String, Box<dyn Error>>, { let input_file = File::open(&*self.path)?; let output_file = File::create(output.as_ref())?; let mut reader = BufReader::new(input_file); let mut writer = BufWriter::new(output_file); let mut buffer = String::new(); let mut results = Vec::new(); while reader.read_line(&mut buffer)? > 0 { let processed_line = processor(&buffer)?; writeln!(writer, "{}", processed_line)?; results.push(processed_line); buffer.clear(); } Ok(self.combine_results(results)) } /// Verify file integrity. pub fn verify_integrity(&self) -> Result<bool, Box<dyn Error>> { let metadata = self.path.metadata()?; let file_size = metadata.len(); // Simple integrity check: verify the file can be fully read. let file = File::open(&*self.path)?; let mut reader = BufReader::new(file); let mut buffer = Vec::new(); reader.read_to_end(&mut buffer)?; Ok(buffer.len() == file_size as usize) } /// Rename the file. pub fn rename<P: AsRef<Path>>(&self, new_path: P) -> Result<(), Box<dyn Error>> { std::fs::rename(&*self.path, new_path.as_ref())?; Ok(()) } /// Get file metadata. pub fn metadata(&self) -> Result<std::fs::Metadata, Box<dyn Error>> { self.path.metadata().map_err(|e| e.into()) } /// Combine processed results (type-specific). fn combine_results(&self, results: Vec<T>) -> T { // In a real implementation, you'd combine results based on T. // Simplified example: if !results.is_empty() { results.into_iter().next().unwrap() } else { // Return an appropriate default value based on T. todo!("Return appropriate default value based on type") } } } impl Clone for FileReader { fn clone(&self) -> Self { Self { path: Arc::clone(&self.path), buffer_size: self.buffer_size, encoding: self.encoding, } } } }
3.6.3.2 File encoding handling
src/utilities/encoding.rs
#![allow(unused)] fn main() { use std::io::{Read, Write, Result as IoResult}; use std::str; use encoding_rs::{GBK, UTF_8}; use encoding_rs_io::DecodeReaderBytesBuilder; pub enum TextEncoding { UTF8, GBK, ASCII, } impl TextEncoding { pub fn from_name(name: &str) -> Option<Self> { match name.to_lowercase().as_str() { "utf-8" | "utf8" => Some(TextEncoding::UTF8), "gbk" | "gb2312" => Some(TextEncoding::GBK), "ascii" => Some(TextEncoding::ASCII), _ => None, } } pub fn decode(&self, bytes: &[u8]) -> Result<String, Box<dyn std::error::Error>> { match self { TextEncoding::UTF8 => { Ok(String::from_utf8(bytes.to_vec())?) } TextEncoding::GBK => { let (decoded, _, _) = GBK.decode(bytes); Ok(decoded.into()) } TextEncoding::ASCII => { Ok(String::from_utf8(bytes.to_vec())?) } } } pub fn encode(&self, text: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> { match self { TextEncoding::UTF8 => { Ok(text.as_bytes().to_vec()) } TextEncoding::GBK => { let (encoded, _, _) = GBK.encode(text); Ok(encoded.to_vec()) } TextEncoding::ASCII => { Ok(text.as_bytes().to_vec()) } } } } /// A generic encoding-aware reader. pub struct EncodingReader<R> { reader: R, encoding: TextEncoding, } impl<R: Read> EncodingReader<R> { pub fn new(reader: R, encoding: TextEncoding) -> Self { Self { reader, encoding } } pub fn read_to_string(&mut self) -> Result<String, Box<dyn std::error::Error>> { let mut buffer = Vec::new(); self.reader.read_to_end(&mut buffer)?; self.encoding.decode(&buffer) } pub fn read_lines(&mut self) -> Result<Vec<String>, Box<dyn std::error::Error>> { let content = self.read_to_string()?; Ok(content.lines().map(|line| line.to_string()).collect()) } } }