FOREWORD: WHY SHOULD YOU CARE?
You already know how to program. Maybe you have spent years wrestling with memory leaks in C, fighting undefined behavior in C++, or waiting for the garbage collector to decide it is a good time to pause your Java application at the worst possible moment. Maybe you love Go's simplicity but wish it gave you more control. Maybe you adore Python's expressiveness but find yourself hitting performance walls. Maybe you write C# or Swift and everything is mostly fine, but you have a nagging feeling that the runtime is doing things behind your back that you cannot fully see or control.
Rust was built for people exactly like you.
This book will take you from zero Rust knowledge to a point where you can write real applications, understand the compiler's sometimes stern feedback, and appreciate why millions of developers have called Rust the most loved programming language in the Stack Overflow Developer Survey for many consecutive years. We will go deep. We will go broad. We will have fun.
Buckle up.
CHAPTER 1: THE STORY OF RUST — WHERE IT CAME FROM AND WHY IT EXISTS
Every programming language has a story, and Rust's story is particularly compelling because it begins not in a corporate boardroom or an academic research lab, but in a moment of personal frustration.
The year was 2006. Graydon Hoare, a software developer working at Mozilla, stepped into an elevator in his apartment building. The elevator software had crashed. Again. Hoare, who spent his professional life thinking about software correctness and memory safety, found himself staring at a broken display and thinking about how absurd it was that in 2006, we were still writing critical software in languages that made memory corruption bugs not just possible but practically inevitable. He went home and started working on a new programming language. He called it Rust, named after a family of fungi known for their resilience and their ability to survive in hostile environments.
Hoare worked on Rust as a personal project for three years. Mozilla noticed what he was building and officially sponsored the project in 2009. The company had a very concrete motivation: they were trying to build a next-generation browser engine called Servo, and they needed a language that could give them the raw performance of C++ without the constant parade of security vulnerabilities that came with it. C++ is extraordinarily powerful, but it places an enormous burden on the programmer to manage memory correctly. One mistake, one forgotten null check, one buffer overrun, and you have a potential security hole that attackers can exploit. Mozilla's Firefox browser, written largely in C++, had suffered from exactly these kinds of vulnerabilities for years. They wanted something better.
Rust 1.0, the first stable release, shipped in May 2015. It was a landmark moment because it represented a language that had been forged in the fires of real-world use. Servo had been pushing Rust's design for years, ensuring that the language could actually handle the demands of building a high-performance, highly concurrent piece of systems software.
What happened next surprised everyone, including Mozilla. Rust escaped its original niche. Developers who had nothing to do with browser engines started using it for web servers, command-line tools, game engines, embedded systems, operating systems, and eventually artificial intelligence. The reason was simple: Rust's core value proposition turned out to be universally attractive. You could write code that was as fast as C, as expressive as a modern high-level language, and provably free of entire categories of bugs — all without paying a runtime cost.
In 2021, Mozilla transferred stewardship of Rust to the newly formed Rust Foundation, a nonprofit organization backed by companies including Google, Microsoft, Amazon, Meta, and Huawei. This was a sign of how seriously the industry had come to take the language. Microsoft began rewriting parts of Windows in Rust. The Linux kernel accepted Rust as a second official implementation language alongside C. The United States government's Cybersecurity and Infrastructure Security Agency (CISA) began recommending Rust as a memory-safe language for critical software.
As of June 2026, Rust is at version 1.96.0, released on May 28, 2026, with a new stable release appearing every six weeks like clockwork. The crates.io package registry hosts well over 150,000 libraries. The language has never been more mature, more capable, or more welcoming to newcomers.
What Problem Does Rust Actually Solve?
To understand Rust, you need to understand the fundamental tension that has existed in systems programming for decades. On one side you have languages like C and C++, which give you direct control over memory and produce extremely fast code. On the other side you have languages like Java, C#, Python, and Go, which manage memory automatically through garbage collection and give you safety guarantees — but at the cost of runtime overhead, pauses, and a certain loss of control.
For most applications, the garbage-collected approach is perfectly fine. The overhead is acceptable, the pauses are imperceptible, and the productivity gains from not worrying about memory management are enormous. But for a certain class of problems, you need both: the performance and control of C, and the safety guarantees of a managed language. Before Rust, you had to choose one or the other.
Rust's insight was that the choice between safety and performance is a false dichotomy. The real problem is not that manual memory management is inherently unsafe. The problem is that the languages that use manual memory management do not give the compiler enough information to verify that the programmer is doing it correctly. Rust solves this by introducing a system of rules about how memory can be accessed and shared — rules that are enforced at compile time by a component of the compiler called the borrow checker. If your code violates these rules, it does not compile. If it compiles, the compiler has mathematically proven that your code is free of a whole class of memory bugs.
The bugs that Rust eliminates at compile time include:
- Use-after-free errors — accessing memory that has already been deallocated
- Double-free errors — deallocating the same memory twice
- Null pointer dereferences — accessing memory through a null pointer
- Buffer overflows — reading or writing past the end of an array
- Data races — two threads accessing the same memory simultaneously when at least one is writing
These are not obscure edge cases. They are the bugs that have caused the majority of critical security vulnerabilities in systems software for the past forty years.
Rust is also honest about what it cannot prove. When you need to do something that the compiler cannot verify — such as calling a C library or performing low-level pointer arithmetic — you can mark a block of code as unsafe and take responsibility for its correctness yourself. This is a crucial design decision: Rust does not pretend that unsafe operations do not exist. It simply quarantines them so that you always know exactly where to look if something goes wrong.
Where Is Rust Used Today?
Rust has found a home in an impressive range of domains. In systems programming, it is used for operating system components, device drivers, and embedded firmware. In web development, frameworks like Axum, Actix-web, and Rocket allow developers to build web servers that handle enormous loads with minimal resource consumption. In the blockchain world, platforms like Solana and Polkadot are written in Rust because they need both performance and correctness. In data processing, the Polars DataFrame library, written in Rust, is dramatically faster than its Python-native competitors. In artificial intelligence, libraries like Candle (from Hugging Face) and Burn are bringing Rust's performance advantages to deep learning inference and training.
Perhaps most tellingly, Rust is increasingly used in places where software failures have real-world consequences: in automotive systems, in aerospace, in medical devices, and in critical infrastructure. These are domains where a memory bug is not just an annoyance but a potential catastrophe, and where Rust's compile-time guarantees provide a level of confidence that no other language at this performance level can match.
CHAPTER 2: GETTING STARTED — INSTALLING RUST AND MEETING YOUR TOOLS
Before we write a single line of Rust, let us get the toolchain installed and understand what we are working with. Rust's toolchain is one of its great strengths: it is well-designed, well-documented, and genuinely pleasant to use.
The primary tool for managing Rust installations is called Rustup. Think of Rustup as the equivalent of nvm for Node.js or pyenv for Python, except that it is the official, recommended way to install Rust on every platform. Rustup manages multiple versions of the Rust compiler, allows you to switch between stable, beta, and nightly releases, and handles the installation of additional components like the standard library source code and cross-compilation targets.
To install Rustup on Linux or macOS, open a terminal and run:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
On Windows, download and run the rustup-init.exe installer from rustup.rs. The installer will ask you a few questions and then set everything up for you, including adding the Rust tools to your system PATH.
After installation, you will have several tools available in your terminal:
| Tool | Purpose |
|---|---|
rustc | The Rust compiler itself |
cargo | Build system and package manager |
rustup | Toolchain manager |
rust-analyzer | Language server for IDE integration |
rustfmt | Official code formatter |
clippy | Collection of lints and static analysis checks |
Install the optional components with:
rustup component add rust-analyzer clippy rustfmt
Keep everything up to date with:
rustup update
Creating Your First Project
Let us create a new Rust project. Open a terminal and run:
cargo new hello_rust
cd hello_rust
Cargo creates a directory with the following structure:
hello_rust/
├── Cargo.toml
└── src/
└── main.rs
The Cargo.toml file is the project's manifest:
[package]
name = "hello_rust"
version = "0.1.0"
edition = "2024"
[dependencies]
The edition = "2024" field tells the compiler which edition of Rust to use. The 2024 edition is the current one and is what we will use throughout this book. Rust editions are a mechanism for introducing breaking changes in a backward-compatible way: old code continues to work with its original edition, while new code can opt into new language features.
The src/main.rs file contains the entry point:
fn main() {
println!("Hello, world!");
}
To build and run this program:
cargo run
To build a release version with full optimizations:
cargo build --release
The optimized binary appears at target/release/hello_rust. Release builds can be dramatically faster than debug builds for compute-intensive code, because the compiler applies aggressive optimizations that it skips in debug mode to keep compilation fast during development.
CHAPTER 3: A SMALL BUT COMPLETE RUST APPLICATION
Before diving into the language's individual concepts, let us look at a complete, working Rust application that touches many of the language's key features. This will give you a mental map of the territory before we explore each region in detail. Do not worry if you do not understand every line yet — that is exactly what the rest of the book is for.
The application we will build is a simple task manager. It allows the user to add tasks, mark them as complete, and list all tasks. It demonstrates structs, enums, traits, error handling, pattern matching, and basic I/O.
Create a new project with cargo new task_manager and replace the contents of src/main.rs with the following:
use std::fmt;
use std::io::{self, BufRead, Write};
// A task has a title, a description, and a completion status.
// The #[derive] attribute asks the compiler to automatically generate
// implementations of common traits for us.
#[derive(Debug, Clone)]
struct Task {
id: u32,
title: String,
description: String,
completed: bool,
}
// We implement the Display trait so we can print tasks nicely.
// This is similar to overriding ToString() in Java or __str__ in Python.
impl fmt::Display for Task {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let status = if self.completed { "[x]" } else { "[ ]" };
write!(
f,
"{} #{}: {} - {}",
status, self.id, self.title, self.description
)
}
}
// An enum representing the possible errors our application can produce.
// Enums in Rust are far more powerful than in C or Java: each variant
// can carry its own data.
#[derive(Debug)]
enum AppError {
TaskNotFound(u32),
InvalidInput(String),
IoError(io::Error),
}
// We implement Display for our error type so it prints human-readable messages.
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::TaskNotFound(id) => {
write!(f, "Task with ID {} was not found.", id)
}
AppError::InvalidInput(msg) => {
write!(f, "Invalid input: {}", msg)
}
AppError::IoError(e) => {
write!(f, "I/O error: {}", e)
}
}
}
}
// This allows us to convert io::Error into AppError automatically,
// which makes the ? operator work smoothly in functions that return
// Result<_, AppError>.
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self {
AppError::IoError(e)
}
}
// The TaskManager holds all tasks and a counter for generating unique IDs.
struct TaskManager {
tasks: Vec<Task>,
next_id: u32,
}
impl TaskManager {
// Associated functions (like static methods in other languages) are
// called with TaskManager::new(), not on an instance.
fn new() -> Self {
TaskManager {
tasks: Vec::new(),
next_id: 1,
}
}
// &mut self means this method borrows the TaskManager mutably,
// i.e., it can modify the struct's fields.
fn add_task(
&mut self,
title: String,
description: String,
) -> Result<u32, AppError> {
if title.trim().is_empty() {
return Err(AppError::InvalidInput(
"Task title cannot be empty.".to_string(),
));
}
let id = self.next_id;
self.tasks.push(Task {
id,
title,
description,
completed: false,
});
self.next_id += 1;
Ok(id)
}
// &self means this method borrows the TaskManager immutably,
// i.e., it can only read the struct's fields.
fn list_tasks(&self) {
if self.tasks.is_empty() {
println!("No tasks yet. Add one!");
return;
}
for task in &self.tasks {
println!("{}", task);
}
}
fn complete_task(&mut self, id: u32) -> Result<(), AppError> {
// iter_mut() gives us mutable references to each element.
// find() searches for the first element matching a predicate.
match self.tasks.iter_mut().find(|t| t.id == id) {
Some(task) => {
task.completed = true;
println!("Task #{} marked as complete.", id);
Ok(())
}
None => Err(AppError::TaskNotFound(id)),
}
}
}
// A helper function that reads a line from stdin and returns it trimmed.
// The ? operator propagates any I/O error up to the caller.
fn read_line(prompt: &str) -> Result<String, AppError> {
print!("{}", prompt);
// flush() ensures the prompt appears before we wait for input.
io::stdout().flush()?;
let stdin = io::stdin();
let mut line = String::new();
stdin.lock().read_line(&mut line)?;
Ok(line.trim().to_string())
}
fn main() {
println!("=== Rust Task Manager ===");
println!("Commands: add, list, complete, quit");
let mut manager = TaskManager::new();
// The main loop. 'loop' creates an infinite loop; 'break' exits it.
loop {
let command = match read_line("\n> ") {
Ok(cmd) => cmd,
Err(e) => {
eprintln!("Error reading input: {}", e);
continue;
}
};
// Pattern matching on string slices.
match command.as_str() {
"add" => {
let title = match read_line("Title: ") {
Ok(t) => t,
Err(e) => { eprintln!("Error: {}", e); continue; }
};
let desc = match read_line("Description: ") {
Ok(d) => d,
Err(e) => { eprintln!("Error: {}", e); continue; }
};
match manager.add_task(title, desc) {
Ok(id) => println!("Task #{} added.", id),
Err(e) => eprintln!("Error: {}", e),
}
}
"list" => manager.list_tasks(),
"complete" => {
let input = match read_line("Task ID: ") {
Ok(s) => s,
Err(e) => { eprintln!("Error: {}", e); continue; }
};
match input.parse::<u32>() {
Ok(id) => {
if let Err(e) = manager.complete_task(id) {
eprintln!("Error: {}", e);
}
}
Err(_) => eprintln!("Please enter a valid task ID number."),
}
}
"quit" | "exit" | "q" => {
println!("Goodbye!");
break;
}
"" => {} // Ignore empty input.
other => {
println!(
"Unknown command: '{}'. Try add, list, complete, or quit.",
other
);
}
}
}
}
Run this application with cargo run and try adding a few tasks, listing them, and marking them complete. Notice how the program handles errors gracefully: if you enter an invalid task ID, it tells you so without crashing. If you try to add a task with an empty title, it rejects the input with a helpful message.
This small application already demonstrates a remarkable number of Rust concepts: structs to define data types, enums to represent errors and variants, traits to define shared behavior, the Result type for error handling, pattern matching to handle different cases, and the ? operator to propagate errors. Ownership and borrowing are implicit throughout — the TaskManager owns its Vec of Tasks, methods borrow it either mutably or immutably depending on whether they need to modify it, and the compiler verifies all of this at compile time.
CHAPTER 4: VARIABLES, TYPES, AND THE BASICS OF RUST SYNTAX
Variables and Mutability
In Rust, variables are immutable by default. This is one of the first things that surprises developers coming from other languages, and it is one of the most important design decisions in the language. When you declare a variable with let, you cannot change its value unless you explicitly mark it as mutable with mut.
fn main() {
// An immutable variable. Once set, x cannot be changed.
let x = 5;
println!("x is {}", x);
// This would cause a compile error:
// x = 6; // error: cannot assign twice to immutable variable
// A mutable variable. We can change y after it is declared.
let mut y = 10;
println!("y is {}", y);
y = 20;
println!("y is now {}", y);
}
This default immutability is not just a stylistic choice — it is a safety feature. When you look at a piece of code and see a variable declared without mut, you know with certainty that its value will never change. This makes code much easier to reason about, especially in concurrent programs where shared mutable state is a primary source of bugs.
Rust also supports shadowing, which allows you to declare a new variable with the same name as an existing one. The new variable shadows the old one, and the old one is no longer accessible. This is different from mutation because the new variable can have a completely different type.
fn main() {
let x = 5;
// We shadow x with a new variable that has the same name.
let x = x + 1;
println!("x is {}", x); // Prints: x is 6
// We can even shadow with a different type.
let spaces = " "; // spaces is a string
let spaces = spaces.len(); // now spaces is a number
println!("spaces is {}", spaces); // Prints: spaces is 3
}
Scalar Types
Rust has four primary scalar types: integers, floating-point numbers, Booleans, and characters.
Integers come in signed (i) and unsigned (u) variants, and in sizes of 8, 16, 32, 64, and 128 bits. The most commonly used are i32 (the default), i64, u32, u64, and usize (pointer-sized, used for indexing).
Floating-point numbers come in f32 (single precision) and f64 (double precision). The default is f64.
The Boolean type is bool with values true and false.
The character type char represents a Unicode scalar value — always four bytes wide, capable of representing any Unicode character including emoji and mathematical symbols. This is a key difference from C's byte-sized char.
fn main() {
// Integer literals can use underscores as visual separators.
let population: i64 = 8_000_000_000;
// Integer literals can be written in hex, octal, or binary.
let hex = 0xFF; // 255 in hexadecimal
let octal = 0o377; // 255 in octal
let binary = 0b1111_1111; // 255 in binary
let pi: f64 = 3.14159265358979;
let is_rust_awesome: bool = true;
// A char is a full Unicode scalar value, not just a byte.
let letter = 'A';
let emoji = '🦀'; // The Rust mascot: Ferris the crab!
println!("Population: {}", population);
println!("Pi: {}", pi);
println!("Is Rust awesome? {}", is_rust_awesome);
println!("The Rust mascot is: {}", emoji);
}
Compound Types: Tuples and Arrays
Tuples group together values of different types into a single compound value with a fixed length. You access elements by index using dot notation.
fn main() {
// A tuple with three elements of different types.
let point: (f64, f64, f64) = (1.0, 2.5, -3.7);
// Destructuring: binding the tuple's elements to individual variables.
let (x, y, z) = point;
println!("x={}, y={}, z={}", x, y, z);
// Accessing elements by index.
println!("The y coordinate is {}", point.1);
// Functions can return tuples to return multiple values.
let (min, max) = find_min_max(&[3, 7, 1, 9, 4]);
println!("Min: {}, Max: {}", min, max);
}
fn find_min_max(values: &[i32]) -> (i32, i32) {
let mut min = values[0];
let mut max = values[0];
for &v in values {
if v < min { min = v; }
if v > max { max = v; }
}
(min, max)
}
Arrays in Rust have a fixed length known at compile time. Every element must have the same type. For variable-length collections, use Vec.
fn main() {
// An array of five i32 values.
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
// An array of twelve elements, all initialized to zero.
let zeros = [0; 12];
println!("First number: {}", numbers[0]);
println!("Array length: {}", numbers.len());
println!("Zeros length: {}", zeros.len());
// Iterating over an array.
for n in &numbers {
print!("{} ", n);
}
println!();
}
Rust performs bounds checking on array accesses at runtime. If you try to access an element beyond the end of an array, the program panics rather than silently reading garbage memory or corrupting data. A panic is always better than undefined behavior.
Functions
Functions in Rust are declared with the fn keyword. Parameters must have explicit type annotations. The return type is specified after ->.
// A function that takes two i32 parameters and returns their sum.
fn add(a: i32, b: i32) -> i32 {
// The last expression in a function body is the return value.
// Note: no semicolon at the end of the last expression.
a + b
}
// A function that returns nothing. The () type (called "unit") is
// Rust's equivalent of void.
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let result = add(3, 4);
println!("3 + 4 = {}", result);
greet("Ferris");
}
The absence of a semicolon on the last line of a function body is significant. In Rust, expressions evaluate to a value; statements perform an action and do not return a value. Adding a semicolon to the end of an expression turns it into a statement. You can also use the return keyword for early returns.
fn absolute_value(n: i32) -> i32 {
if n < 0 {
return -n; // Early return using the return keyword.
}
n // Implicit return of the last expression.
}
Control Flow
Rust's if is an expression, not just a statement, meaning it can return a value:
fn main() {
let temperature = 22;
// Using if as an expression to assign a value.
// This is similar to the ternary operator in C/Java.
let description = if temperature > 30 {
"hot"
} else if temperature > 20 {
"pleasant"
} else {
"cool"
};
println!("The weather is {}.", description);
}
Rust has three looping constructs. The loop keyword creates an infinite loop and can return a value via break:
fn main() {
// loop can return a value via break.
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // Return 20 from the loop.
}
};
println!("The result is {}", result); // Prints: The result is 20
// while loops work as expected.
let mut n = 1;
while n < 100 {
n *= 2;
}
println!("First power of 2 >= 100: {}", n); // Prints: 128
// for loops iterate over ranges or collections.
// 1..=5 is an inclusive range from 1 to 5.
for i in 1..=5 {
print!("{} ", i);
}
println!(); // Prints: 1 2 3 4 5
}
CHAPTER 5: OWNERSHIP — THE HEART OF RUST
Ownership is the concept that makes Rust unique. It is the mechanism by which Rust achieves memory safety without a garbage collector, and it is the concept that requires the most adjustment if you are coming from another language. Take your time with this chapter. Read it slowly. The effort is worth it, because once ownership clicks, everything else in Rust becomes much clearer.
The Three Rules of Ownership
Rust's ownership system is governed by three rules enforced at compile time:
- Every value in Rust has exactly one owner — the variable that holds the value.
- There can only be one owner at a time. You cannot have two variables that both own the same piece of memory.
- When the owner goes out of scope, the value is automatically dropped — memory is freed and any associated resources (file handles, network connections) are released.
These three rules together eliminate the need for a garbage collector. The compiler can determine exactly when each piece of memory should be freed, and it inserts the necessary deallocation code automatically.
Move Semantics
When you assign a value to another variable, what happens depends on the type. For types stored entirely on the stack that are cheap to copy (like integers, booleans, characters), Rust copies the value. For types involving heap-allocated memory (like String and Vec), Rust moves the value.
fn main() {
// Integers implement the Copy trait.
// Assigning x to y creates a copy; both x and y are valid.
let x = 5;
let y = x;
println!("x={}, y={}", x, y); // Both are valid.
// String is a heap-allocated type. It does NOT implement Copy.
// Assigning s1 to s2 MOVES the ownership of the string data to s2.
// After the move, s1 is no longer valid.
let s1 = String::from("hello");
let s2 = s1; // s1 is moved into s2.
// This would cause a compile error:
// println!("{}", s1); // error: value borrowed here after move
println!("{}", s2); // s2 is the new owner; this is fine.
}
If you want an explicit deep copy, use clone():
fn main() {
let s1 = String::from("hello");
// clone() creates a deep copy, including the heap data.
let s2 = s1.clone();
// Now both s1 and s2 are valid owners of their own copies.
println!("s1={}, s2={}", s1, s2);
}
Move semantics also apply when passing values to functions or returning them. Passing a String to a function moves ownership into the function. When the function returns, if it does not return the String, the String is dropped.
Borrowing and References
A reference allows you to refer to a value without taking ownership of it. Creating a reference is called borrowing. You create a reference using the & operator. A reference is guaranteed to always point to a valid value: the compiler ensures that the referenced value cannot be dropped while the reference exists.
fn main() {
let s = String::from("hello");
// We pass a reference to s, not s itself.
// calculate_length borrows s but does not take ownership.
let len = calculate_length(&s);
// s is still valid here because we only lent it.
println!("The length of '{}' is {}.", s, len);
}
// The parameter type &String means "a reference to a String".
fn calculate_length(s: &String) -> usize {
s.len()
// s goes out of scope here, but since it is a reference,
// the underlying String is NOT dropped.
}
References are immutable by default. If you want to modify a borrowed value, you need a mutable reference, created with &mut:
fn main() {
let mut s = String::from("hello");
append_world(&mut s);
println!("{}", s); // Prints: hello, world
}
fn append_world(s: &mut String) {
s.push_str(", world");
}
Mutable references come with a crucial restriction: you can have only one mutable reference to a particular piece of data at a time. This rule prevents data races at compile time. There is also a rule about mixing mutable and immutable references: you cannot have a mutable reference while immutable references exist.
The Rust compiler is smart enough to track exactly where references are last used, not just where they go out of scope. This feature, called Non-Lexical Lifetimes (NLL), makes the borrow checker less restrictive while maintaining safety.
The Slice Type
Slices are a special kind of reference that refer to a contiguous sequence of elements in a collection, rather than the whole collection. They do not have ownership.
fn main() {
let s = String::from("hello world");
// A string slice refers to a portion of the string.
let hello = &s[0..5];
let world = &s[6..11];
println!("{} {}", hello, world); // Prints: hello world
let numbers = [1, 2, 3, 4, 5];
// An array slice.
let middle: &[i32] = &numbers[1..4];
println!("{:?}", middle); // Prints: [2, 3, 4]
}
Functions that accept string data should generally accept &str rather than &String. A &str can refer to a slice of a Stringor to a string literal, making the function more flexible.
CHAPTER 6: STRUCTS — DEFINING YOUR OWN TYPES
Structs are Rust's primary mechanism for creating custom data types, similar to structs in C, classes in Java and C#, or named tuples in Python.
Defining and Using Structs
use std::fmt;
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
// An associated function (no self parameter) acts like a constructor.
fn new(width: f64, height: f64) -> Self {
Self { width, height }
}
// A method that computes the area. Borrows self immutably.
fn area(&self) -> f64 {
self.width * self.height
}
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
fn can_contain(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
// Needs &mut self because it modifies the struct's fields.
fn scale(&mut self, factor: f64) {
self.width *= factor;
self.height *= factor;
}
}
impl fmt::Display for Rectangle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Rectangle({}x{})", self.width, self.height)
}
}
fn main() {
let mut rect1 = Rectangle::new(10.0, 5.0);
let rect2 = Rectangle::new(3.0, 2.0);
println!("{}", rect1);
println!("Area: {}", rect1.area());
println!("Perimeter: {}", rect1.perimeter());
println!("Can contain rect2? {}", rect1.can_contain(&rect2));
rect1.scale(2.0);
println!("After scaling: {}", rect1);
}
Tuple Structs
Tuple structs are structs with unnamed fields, useful for giving a tuple a meaningful name without naming each field. They enable the newtype pattern: even though Color and Point2D both contain the same underlying types, they are different types in Rust's type system, preventing accidental type confusion bugs.
struct Color(u8, u8, u8);
struct Point2D(f64, f64);
fn main() {
let red = Color(255, 0, 0);
let origin = Point2D(0.0, 0.0);
println!("Red: ({}, {}, {})", red.0, red.1, red.2);
println!("Origin: ({}, {})", origin.0, origin.1);
}
Struct Update Syntax
When you want to create a new struct that is mostly the same as an existing one but with a few fields changed, Rust provides struct update syntax:
#[derive(Debug)]
struct User {
username: String,
email: String,
active: bool,
login_count: u32,
}
fn main() {
let user1 = User {
username: String::from("alice"),
email: String::from("alice@example.com"),
active: true,
login_count: 1,
};
// Create user2 with the same values as user1, except for email.
// The ..user1 syntax fills in the remaining fields from user1.
let user2 = User {
email: String::from("bob@example.com"),
..user1
};
println!("{:?}", user2);
}
CHAPTER 7: ENUMS AND PATTERN MATCHING — RUST'S SUPERPOWER
If ownership is Rust's most distinctive feature, enums and pattern matching are its most expressive. Rust's enums are what computer scientists call algebraic data types or sum types, and they allow you to model data in ways that are both precise and safe.
Enums with Data
In C and Java, an enum is just a named integer constant. In Rust, each variant of an enum can carry its own data of any type:
#[derive(Debug)]
enum Message {
Quit, // A variant with no data.
Move { x: i32, y: i32 }, // A variant with named fields.
Write(String), // A variant with a single String.
ChangeColor(u8, u8, u8), // A variant with three u8 values.
}
impl Message {
fn process(&self) {
match self {
Message::Quit => println!("Quitting."),
Message::Move { x, y } => println!("Moving to ({}, {}).", x, y),
Message::Write(text) => println!("Writing: {}", text),
Message::ChangeColor(r, g, b) => {
println!("Changing color to ({}, {}, {}).", r, g, b)
}
}
}
}
fn main() {
let messages = vec![
Message::Move { x: 10, y: 20 },
Message::Write(String::from("hello")),
Message::ChangeColor(255, 128, 0),
Message::Quit,
];
for msg in &messages {
msg.process();
}
}
The match expression in Rust is exhaustive: the compiler requires you to handle every possible variant of an enum. If you forget a case, the code will not compile. This is a profound safety guarantee that Java's switch or C's switch cannot provide.
Option: The Safe Alternative to Null
One of the most famous bugs in programming history is the null pointer, which its inventor Tony Hoare called his "billion-dollar mistake." Rust eliminates null pointers entirely. Instead, Rust uses the Option enum:
enum Option<T> {
Some(T),
None,
}
The compiler forces you to handle both cases before you can use the value inside a Some:
fn find_user(id: u32) -> Option<String> {
if id == 1 {
Some(String::from("Alice"))
} else {
None
}
}
fn main() {
// Pattern matching to handle Option.
match find_user(1) {
Some(name) => println!("Found user: {}", name),
None => println!("User not found."),
}
// The if let syntax is a convenient shorthand.
if let Some(name) = find_user(2) {
println!("Found: {}", name);
} else {
println!("User 2 not found.");
}
// Option has many useful methods.
let name = find_user(1).unwrap_or(String::from("Unknown"));
println!("Name: {}", name);
// map transforms the value inside Some, leaving None unchanged.
let name_length = find_user(1).map(|n| n.len());
println!("Name length: {:?}", name_length); // Some(5)
}
Result: Explicit Error Handling
Rust does not have exceptions. Instead, it uses the Result enum for operations that can fail:
enum Result<T, E> {
Ok(T),
Err(E),
}
The compiler forces you to handle both the success and failure cases. You cannot accidentally ignore an error in Rust the way you can ignore an exception in Java by not catching it.
use std::num::ParseIntError;
fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
// parse() returns a Result. The ? operator unwraps the Ok value
// or returns the Err value from the enclosing function.
let n: i32 = s.trim().parse()?;
Ok(n * 2)
}
fn main() {
match parse_and_double("21") {
Ok(result) => println!("Result: {}", result), // Prints: 42
Err(e) => println!("Error: {}", e),
}
match parse_and_double("not a number") {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
}
The ? operator is one of Rust's most useful ergonomic features. When used in a function that returns a Result, ? unwraps the Ok value if the expression succeeded, or immediately returns the Err value from the current function if it failed.
Advanced Pattern Matching
Pattern matching in Rust goes far beyond simple enum matching. You can match on integers, strings, tuples, structs, and combinations thereof, using guards (additional conditions) and binding matched values to variables:
fn classify_temperature(temp: f64) -> &'static str {
match temp {
t if t < 0.0 => "freezing",
t if t < 10.0 => "cold",
t if t < 20.0 => "cool",
t if t < 30.0 => "warm",
_ => "hot",
}
}
fn describe_pair(pair: (i32, i32)) -> String {
match pair {
(0, 0) => String::from("origin"),
(x, 0) => format!("on the x-axis at {}", x),
(0, y) => format!("on the y-axis at {}", y),
(x, y) if x == y => format!("on the diagonal at {}", x),
(x, y) => format!("at ({}, {})", x, y),
}
}
fn main() {
println!("{}", classify_temperature(-5.0)); // freezing
println!("{}", classify_temperature(25.0)); // warm
println!("{}", describe_pair((0, 0))); // origin
println!("{}", describe_pair((3, 0))); // on the x-axis at 3
println!("{}", describe_pair((4, 4))); // on the diagonal at 4
println!("{}", describe_pair((2, 7))); // at (2, 7)
}
Rust 1.96.0 also stabilized the assert_matches! macro, which is particularly useful in tests for asserting that a value matches a specific pattern with improved diagnostics:
use std::assert_matches::assert_matches;
fn main() {
let value: Option<i32> = Some(42);
assert_matches!(value, Some(x) if x > 0);
println!("Assertion passed!");
}
CHAPTER 8: TRAITS — DEFINING SHARED BEHAVIOR
Traits are Rust's mechanism for defining shared behavior across different types. They are similar to interfaces in Java and C#, protocols in Swift, or abstract base classes in Python — but with important differences that make them more powerful and flexible.
Defining and Implementing Traits
trait Animal {
// A required method: every type implementing Animal must provide this.
fn name(&self) -> &str;
fn sound(&self) -> &str;
// A default method: types can use this or override it.
fn describe(&self) -> String {
format!("The {} says {}!", self.name(), self.sound())
}
}
struct Dog { name: String }
struct Cat { name: String }
impl Animal for Dog {
fn name(&self) -> &str { &self.name }
fn sound(&self) -> &str { "woof" }
// Dog overrides the default describe() method.
fn describe(&self) -> String {
format!("{} is a good dog who says {}!", self.name(), self.sound())
}
}
impl Animal for Cat {
fn name(&self) -> &str { &self.name }
fn sound(&self) -> &str { "meow" }
// Cat uses the default describe() implementation.
}
// The impl Trait syntax is shorthand for a generic type bound.
fn make_noise(animal: &impl Animal) {
println!("{}", animal.describe());
}
fn main() {
let dog = Dog { name: String::from("Rex") };
let cat = Cat { name: String::from("Whiskers") };
make_noise(&dog);
make_noise(&cat);
}
Generics and Trait Bounds
Generics allow you to write functions and types that work with multiple different types without duplicating code. Trait bounds constrain which types a generic function or type can work with:
use std::fmt::Display;
// T can be any type that implements PartialOrd (values can be compared).
fn find_largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
struct Pair<T> {
first: T,
second: T,
}
impl<T> Pair<T> {
fn new(first: T, second: T) -> Self {
Self { first, second }
}
}
// This impl block only applies when T implements both Display and PartialOrd.
impl<T: Display + PartialOrd> Pair<T> {
fn print_larger(&self) {
if self.first >= self.second {
println!("The larger value is {}", self.first);
} else {
println!("The larger value is {}", self.second);
}
}
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("Largest number: {}", find_largest(&numbers));
let chars = vec!['y', 'm', 'a', 'q'];
println!("Largest char: {}", find_largest(&chars));
let pair = Pair::new(5, 10);
pair.print_larger(); // Prints: The larger value is 10
}
Trait Objects and Dynamic Dispatch
Sometimes you need a collection of different types that all implement the same trait, and you do not know at compile time which specific types will be in the collection. For this, Rust provides trait objects, written as dyn Trait, which use dynamic dispatch:
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &str;
}
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn name(&self) -> &str { "Circle" }
}
impl Shape for Square {
fn area(&self) -> f64 { self.side * self.side }
fn name(&self) -> &str { "Square" }
}
fn main() {
// Box<dyn Shape> means "a heap-allocated value of any type
// that implements Shape".
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 3.0 }),
Box::new(Square { side: 4.0 }),
Box::new(Circle { radius: 1.5 }),
];
let total_area: f64 = shapes.iter().map(|s| s.area()).sum();
for shape in &shapes {
println!("{}: area = {:.2}", shape.name(), shape.area());
}
println!("Total area: {:.2}", total_area);
}
The difference between generics (static dispatch) and trait objects (dynamic dispatch) is a performance trade-off. With generics, the compiler generates a separate version of the function for each concrete type — faster at runtime but larger binaries. With trait objects, there is a single version of the code that calls methods through a vtable — slightly slower but more flexible and producing smaller code.
CHAPTER 9: COLLECTIONS — MANAGING GROUPS OF DATA
Vec: The Growable Array
Vec<T> is the workhorse collection type in Rust — a heap-allocated, dynamically sized array similar to ArrayList in Java or list in Python:
fn main() {
let mut numbers: Vec<i32> = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);
// The vec! macro is a convenient shorthand.
let fruits = vec!["apple", "banana", "cherry"];
// Accessing elements by index. This panics if out of bounds.
println!("First fruit: {}", fruits[0]);
// get() returns an Option, which is safer for untrusted indices.
match fruits.get(10) {
Some(f) => println!("Found: {}", f),
None => println!("Index out of bounds."),
}
// Transforming a Vec using iterators.
let doubled: Vec<i32> = numbers.iter().map(|&n| n * 2).collect();
println!("{:?}", doubled); // [2, 4, 6]
// Filtering a Vec.
let evens: Vec<i32> = numbers.iter().filter(|&&n| n % 2 == 0).cloned().collect();
println!("{:?}", evens); // [2]
// Sorting.
let mut data = vec![3, 1, 4, 1, 5, 9, 2, 6];
data.sort();
println!("{:?}", data); // [1, 1, 2, 3, 4, 5, 6, 9]
}
HashMap: Key-Value Storage
HashMap<K, V> stores key-value pairs and allows lookup by key in constant time on average:
use std::collections::HashMap;
fn main() {
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Alice"), 95);
scores.insert(String::from("Bob"), 87);
scores.insert(String::from("Charlie"), 92);
// get() returns Option<&V>.
if let Some(score) = scores.get("Alice") {
println!("Alice's score: {}", score);
}
// The entry API: insert a value only if the key does not exist.
scores.entry(String::from("Dave")).or_insert(75);
scores.entry(String::from("Alice")).or_insert(0); // No change; Alice exists.
// Count word frequencies.
let text = "hello world hello rust world hello";
let mut word_count: HashMap<&str, u32> = HashMap::new();
for word in text.split_whitespace() {
let count = word_count.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", word_count);
}
Iterators: The Functional Heart of Rust Collections
Rust's iterator system is one of its most elegant features. Iterators are lazy: they do not compute their results until you ask for them. This allows you to chain multiple operations together without creating intermediate collections, which is both efficient and expressive:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Chain multiple iterator operations.
let sum_of_squared_evens: i32 = numbers
.iter()
.filter(|&&n| n % 2 == 0) // Keep only even numbers.
.map(|&n| n * n) // Square each one.
.sum(); // Add them all up.
println!("Sum of squared evens: {}", sum_of_squared_evens); // 220
// take(n) takes only the first n elements.
let first_three_evens: Vec<i32> = numbers
.iter()
.filter(|&&n| n % 2 == 0)
.take(3)
.cloned()
.collect();
println!("{:?}", first_three_evens); // [2, 4, 6]
// any() and all() check predicates.
let has_large = numbers.iter().any(|&n| n > 8);
let all_positive = numbers.iter().all(|&n| n > 0);
println!("Has element > 8: {}", has_large); // true
println!("All positive: {}", all_positive); // true
// fold() is a general reduction operation.
let product: i32 = numbers.iter().fold(1, |acc, &n| acc * n);
println!("Product: {}", product); // 3628800
}
The entire chain filter → map → sum compiles down to a single loop with no intermediate allocations. The Rust compiler is extremely good at optimizing iterator chains, often producing code as fast as a hand-written loop.
CHAPTER 10: LIFETIMES — TEACHING THE COMPILER ABOUT TIME
Lifetimes are the most conceptually challenging part of Rust for most newcomers. They are also the part that the compiler handles automatically in the vast majority of cases through lifetime elision. But when you do need them, understanding what they mean is essential.
A lifetime is the scope for which a reference is valid. The purpose of lifetimes is to prevent dangling references: references that point to memory that has already been freed.
When Lifetime Annotations Are Needed
Lifetime annotations become necessary when a function takes multiple references as parameters and returns a reference, and the compiler cannot determine which input reference the output is derived from:
// This function takes two string slices and returns the longer one.
// The lifetime annotation 'a says: "the returned reference will be
// valid for as long as BOTH x and y are valid."
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("Longest: {}", result); // This is fine.
}
// string2 has been dropped here. Using result here would be a
// compile error because result might refer to string2.
}
Lifetime Annotations in Structs
When a struct holds a reference, it needs a lifetime annotation:
// 'a means: "an instance of ImportantExcerpt cannot outlive
// the reference it holds in the part field."
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn announce(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel
.split('.')
.next()
.expect("Could not find a '.'");
let excerpt = ImportantExcerpt { part: first_sentence };
println!("Excerpt: {}", excerpt.part);
}
In practice, you will find that most Rust code does not need explicit lifetime annotations, because the compiler applies lifetime elision rules that handle the common cases automatically. You will mainly encounter explicit lifetimes in library code, in structs that hold references, and in complex functions with multiple reference parameters.
CHAPTER 11: ERROR HANDLING IN DEPTH
Defining Custom Error Types
In real applications, you will often want to define your own error types that can represent all the different ways your application can fail:
use std::fmt;
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum DataError {
Io(io::Error),
Parse(ParseIntError),
InvalidData(String),
OutOfRange { value: i64, min: i64, max: i64 },
}
impl fmt::Display for DataError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DataError::Io(e) => write!(f, "I/O error: {}", e),
DataError::Parse(e) => write!(f, "Parse error: {}", e),
DataError::InvalidData(msg) => write!(f, "Invalid data: {}", msg),
DataError::OutOfRange { value, min, max } => write!(
f, "Value {} is out of range [{}, {}]", value, min, max
),
}
}
}
impl From<io::Error> for DataError {
fn from(e: io::Error) -> Self { DataError::Io(e) }
}
impl From<ParseIntError> for DataError {
fn from(e: ParseIntError) -> Self { DataError::Parse(e) }
}
fn parse_and_validate(s: &str) -> Result<i64, DataError> {
let value: i64 = s.trim().parse()?;
if value < 0 || value > 1000 {
return Err(DataError::OutOfRange { value, min: 0, max: 1000 });
}
Ok(value)
}
fn main() {
let inputs = vec!["42", " 500 ", "abc", "-5", "9999"];
for input in inputs {
match parse_and_validate(input) {
Ok(v) => println!("'{}' -> {}", input.trim(), v),
Err(e) => println!("'{}' -> Error: {}", input.trim(), e),
}
}
}
The anyhow and thiserror Crates
For library code, the thiserror crate (version 2.0.18 as of June 2026) provides a derive macro that reduces the boilerplate of implementing Display and From for custom error types. For application code, anyhow (version 1.0.102) provides a convenient error type that can hold any error.
Add them to Cargo.toml:
[dependencies]
anyhow = "1.0.102"
thiserror = "2.0.18"
With thiserror, the DataError example above becomes much more concise:
use thiserror::Error;
use std::io;
use std::num::ParseIntError;
#[derive(Debug, Error)]
enum DataError {
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("Parse error: {0}")]
Parse(#[from] ParseIntError),
#[error("Invalid data: {0}")]
InvalidData(String),
#[error("Value {value} is out of range [{min}, {max}]")]
OutOfRange { value: i64, min: i64, max: i64 },
}
With anyhow, the ? operator works with any error type without defining custom From implementations:
use anyhow::{Context, Result};
use std::fs;
fn read_config(path: &str) -> Result<String> {
let contents = fs::read_to_string(path)
.with_context(|| format!("Failed to read config file: {}", path))?;
Ok(contents)
}
// fn main() -> Result<()> allows using ? in main itself.
fn main() -> Result<()> {
let config = read_config("config.toml")?;
println!("Config: {}", config);
Ok(())
}
CHAPTER 12: CLOSURES AND FUNCTIONAL PROGRAMMING
Closures are anonymous functions that can capture values from their surrounding environment. They are central to Rust's iterator system and are used extensively throughout the language.
Closure Syntax and Capturing
fn main() {
let x = 5;
// A closure that captures x from the surrounding scope.
let add_x = |n| n + x;
println!("{}", add_x(10)); // 15
// A closure with explicit type annotations.
let multiply = |a: i32, b: i32| -> i32 { a * b };
println!("{}", multiply(3, 4)); // 12
// A closure that captures a mutable variable.
let mut count = 0;
let mut increment = || {
count += 1;
count
};
println!("{}", increment()); // 1
println!("{}", increment()); // 2
// The move keyword forces the closure to take ownership of
// captured values, rather than borrowing them.
let greeting = String::from("Hello");
let greet = move |name: &str| {
println!("{}, {}!", greeting, name)
};
greet("Alice");
// greeting has been moved into the closure; we cannot use it here.
}
When you want to store a closure in a struct or pass it to a function, you use one of three traits:
Fn— closures that can be called multiple times and only borrow captured values immutablyFnMut— closures that can be called multiple times and may borrow captured values mutablyFnOnce— closures that can only be called once, because they consume their captured values
fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(f(x))
}
fn main() {
let double = |n| n * 2;
let add_five = |n| n + 5;
println!("{}", apply_twice(double, 3)); // 12 (3 -> 6 -> 12)
println!("{}", apply_twice(add_five, 10)); // 20 (10 -> 15 -> 20)
}
Iterators and Closures Together
#[derive(Debug)]
struct Student {
name: String,
grade: f64,
subject: String,
}
fn main() {
let students = vec![
Student { name: "Alice".into(), grade: 92.5, subject: "Math".into() },
Student { name: "Bob".into(), grade: 78.0, subject: "Science".into() },
Student { name: "Charlie".into(), grade: 88.5, subject: "Math".into() },
Student { name: "Diana".into(), grade: 95.0, subject: "Science".into() },
Student { name: "Eve".into(), grade: 65.0, subject: "Math".into() },
];
// Find all Math students with a grade above 80.
let high_math_students: Vec<&Student> = students
.iter()
.filter(|s| s.subject == "Math" && s.grade > 80.0)
.collect();
println!("High-performing Math students:");
for s in &high_math_students {
println!(" {} - {:.1}", s.name, s.grade);
}
// Calculate the average grade.
let total: f64 = students.iter().map(|s| s.grade).sum();
let average = total / students.len() as f64;
println!("Class average: {:.2}", average);
// Find the top student.
if let Some(top) = students.iter().max_by(|a, b| {
a.grade.partial_cmp(&b.grade).unwrap()
}) {
println!("Top student: {} with {:.1}", top.name, top.grade);
}
}
CHAPTER 13: CONCURRENCY — FEARLESS PARALLELISM
One of Rust's most celebrated features is what the community calls "fearless concurrency." Rust's ownership system and type system prevent entire categories of concurrency bugs at compile time.
Threads
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..=5 {
println!("Thread: count {}", i);
thread::sleep(Duration::from_millis(50));
}
});
for i in 1..=3 {
println!("Main: count {}", i);
thread::sleep(Duration::from_millis(80));
}
handle.join().unwrap();
println!("Both threads finished.");
}
Sharing Data Between Threads
To share data between threads, Rust requires thread-safe types. Arc<Mutex<T>> is the standard pattern for shared mutable state:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..10 {
// Clone the Arc to give each thread its own reference.
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
// lock() acquires the mutex. MutexGuard automatically releases
// the lock when it goes out of scope (RAII pattern).
let mut num = counter_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final counter: {}", *counter.lock().unwrap()); // 10
}
Message Passing with Channels
use std::sync::mpsc; // mpsc = multiple producer, single consumer
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
let tx2 = tx.clone();
thread::spawn(move || {
let messages = vec!["Hello", "from", "thread", "one"];
for msg in messages {
tx.send(msg).unwrap();
thread::sleep(std::time::Duration::from_millis(100));
}
});
thread::spawn(move || {
let messages = vec!["And", "greetings", "from", "thread", "two"];
for msg in messages {
tx2.send(msg).unwrap();
thread::sleep(std::time::Duration::from_millis(150));
}
});
for received in rx {
println!("Received: {}", received);
}
}
Async/Await with Tokio
For I/O-bound concurrency, Rust provides async/await syntax. The most popular async runtime is Tokio (version 1.52.3as of June 2026). Add it to Cargo.toml:
[dependencies]
tokio = { version = "1.52", features = ["full"] }
use tokio::time::{sleep, Duration};
// An async function returns a Future.
async fn fetch_data(id: u32) -> String {
// In a real application, this would make a network request.
sleep(Duration::from_millis(100 * id as u64)).await;
format!("Data for item {}", id)
}
// The #[tokio::main] attribute sets up the Tokio runtime.
#[tokio::main]
async fn main() {
println!("Starting concurrent tasks...");
// tokio::join! runs multiple futures concurrently and waits for
// all of them to complete.
let (result1, result2, result3) = tokio::join!(
fetch_data(1),
fetch_data(2),
fetch_data(3),
);
println!("{}", result1);
println!("{}", result2);
println!("{}", result3);
println!("All tasks complete.");
}
The key insight about async/await is that it allows you to write concurrent code that looks sequential. The .awaitkeyword suspends the current task and gives the runtime a chance to run other tasks, but the code reads as if it were a simple sequential series of function calls. This is far easier to reason about than callback-based or thread-based concurrency.
CHAPTER 14: MODULES, CRATES, AND THE CARGO ECOSYSTEM
Modules
Modules allow you to organize code into logical namespaces and control what is public (visible to other modules) and what is private:
pub mod geometry {
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
pub fn distance(&self, other: &Point) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
}
pub mod shapes {
use super::Point;
pub struct Triangle {
pub a: Point,
pub b: Point,
pub c: Point,
}
impl Triangle {
pub fn perimeter(&self) -> f64 {
self.a.distance(&self.b)
+ self.b.distance(&self.c)
+ self.c.distance(&self.a)
}
}
}
}
fn main() {
use geometry::Point;
use geometry::shapes::Triangle;
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(3.0, 0.0);
let p3 = Point::new(0.0, 4.0);
let triangle = Triangle { a: p1, b: p2, c: p3 };
println!("Perimeter: {:.2}", triangle.perimeter()); // 12.00
}
Using External Crates
To use an external crate, add it to the [dependencies] section of Cargo.toml. Here is an example using serde (version 1.0.228) and serde_json for JSON serialization:
[dependencies]
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1"
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Config {
host: String,
port: u16,
max_connections: u32,
debug_mode: bool,
}
fn main() {
let config = Config {
host: String::from("localhost"),
port: 8080,
max_connections: 100,
debug_mode: false,
};
// Serialize to JSON.
let json = serde_json::to_string_pretty(&config).unwrap();
println!("JSON:\n{}", json);
// Deserialize from JSON.
let json_str = r#"
{
"host": "example.com",
"port": 443,
"max_connections": 500,
"debug_mode": false
}
"#;
let loaded: Config = serde_json::from_str(json_str).unwrap();
println!("Loaded: {:?}", loaded);
}
Key Crates in the Ecosystem (June 2026)
| Crate | Version | Purpose |
|---|---|---|
serde | 1.0.228 | Serialization framework |
tokio | 1.52.3 | Async runtime |
anyhow | 1.0.102 | Application error handling |
thiserror | 2.0.18 | Library error type derivation |
clap | 4.6.1 | Command-line argument parsing |
rayon | 1.12.0 | Data parallelism |
ndarray | 0.17.2 | N-dimensional arrays (NumPy equivalent) |
pyo3 | 0.28.3 | Python bindings |
log | 0.4.32 | Logging facade |
env_logger | 0.11.10 | Environment-variable-driven logger |
CHAPTER 15: TESTING IN RUST
Rust has first-class support for testing built directly into the language and toolchain. Tests are written in the same files as the code they test, in a special module annotated with #[cfg(test)].
Unit Tests
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_positive_numbers() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative_numbers() {
assert_eq!(add(-2, -3), -5);
}
#[test]
fn test_divide_normal() {
assert_eq!(divide(10.0, 2.0), Some(5.0));
}
#[test]
fn test_divide_by_zero() {
assert_eq!(divide(10.0, 0.0), None);
}
#[test]
fn test_add_is_commutative() {
assert!(add(3, 4) == add(4, 3));
}
// Rust 1.96+ stabilized assert_matches! for pattern assertions.
#[test]
fn test_divide_result_is_some() {
use std::assert_matches::assert_matches;
assert_matches!(divide(10.0, 2.0), Some(x) if x > 0.0);
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn test_out_of_bounds() {
let v = vec![1, 2, 3];
let _ = v[10];
}
}
Run the tests with cargo test.
Documentation Tests
Code examples in documentation comments are automatically compiled and run as tests, ensuring your documentation is always accurate:
/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// let result = my_crate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
CHAPTER 16: SMART POINTERS AND MEMORY MANAGEMENT IN DEPTH
Box: Heap Allocation
Box<T> allocates a value on the heap. It is useful for recursive data structures, large values you want to move without copying, and trait objects:
#[derive(Debug)]
enum List {
Cons(i32, Box<List>), // A node with a value and a pointer to the next node.
Nil, // The end of the list.
}
fn main() {
let list = List::Cons(
1,
Box::new(List::Cons(
2,
Box::new(List::Cons(
3,
Box::new(List::Nil),
)),
)),
);
println!("{:?}", list);
let boxed_value = Box::new(42);
println!("Boxed value: {}", *boxed_value);
}
Rc and RefCell: Shared Ownership and Interior Mutability
Rc<T> (Reference Counted) enables multiple parts of your program to own the same data. RefCell<T> provides interior mutability — it allows you to mutate data even when you only have an immutable reference to it, with borrow rules enforced at runtime:
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
impl Node {
fn new(value: i32) -> Rc<Self> {
Rc::new(Self {
value,
children: RefCell::new(Vec::new()),
})
}
fn add_child(&self, child: Rc<Node>) {
self.children.borrow_mut().push(child);
}
}
fn main() {
let root = Node::new(1);
let child1 = Node::new(2);
let child2 = Node::new(3);
let shared_grandchild = Node::new(4);
child1.add_child(Rc::clone(&shared_grandchild));
child2.add_child(Rc::clone(&shared_grandchild));
root.add_child(Rc::clone(&child1));
root.add_child(Rc::clone(&child2));
println!("Root value: {}", root.value);
println!(
"Shared grandchild reference count: {}",
Rc::strong_count(&shared_grandchild) // 3
);
}
Note:
Rcis not thread-safe. For multi-threaded code, useArc(Atomic Reference Counted) instead.
CHAPTER 17: UNSAFE RUST — WHEN YOU NEED TO BREAK THE RULES
The unsafe keyword tells the compiler: "I am taking responsibility for the correctness of this code." Inside an unsafeblock, you can do five things not allowed in safe Rust: dereference raw pointers, call unsafe functions, access or modify mutable static variables, implement unsafe traits, and access fields of unions.
fn main() {
let mut num = 5;
// Creating raw pointers is safe; dereferencing them is not.
let raw_ptr: *mut i32 = &mut num;
unsafe {
*raw_ptr = 10;
println!("Value via raw pointer: {}", *raw_ptr);
}
println!("num is now: {}", num);
}
The most common use of unsafe Rust is calling C functions through Rust's Foreign Function Interface (FFI):
extern "C" {
fn abs(x: i32) -> i32;
}
fn main() {
let result = unsafe { abs(-42) };
println!("abs(-42) = {}", result); // 42
}
The important thing to understand about unsafe Rust is that it does not disable the borrow checker or turn Rust into C. The ownership rules still apply everywhere. Unsafe code should always be encapsulated behind safe abstractions: write a small amount of unsafe code inside a function, and expose a completely safe public API. This is exactly how the standard library is implemented.
CHAPTER 18: RUST FOR ARTIFICIAL INTELLIGENCE APPLICATIONS
Artificial intelligence and machine learning have been dominated by Python for the past decade, and for good reason: Python's ecosystem is unmatched in breadth and maturity. But Python has significant limitations in production: it is slow, its Global Interpreter Lock (GIL) limits true parallelism, and its dynamic typing makes it difficult to catch errors before deployment.
Rust is increasingly being used in AI applications where Python's limitations matter most: in inference engines that need to be fast and resource-efficient, in edge AI applications running on devices with limited memory, in data processing pipelines that handle enormous volumes of data, and in AI infrastructure where performance and reliability are critical.
The Rust AI Ecosystem (June 2026)
ndarray (version 0.17.2) is Rust's equivalent of NumPy, providing N-dimensional arrays and a comprehensive set of mathematical operations. It is the foundation that many other Rust AI libraries build on.
Candle is a minimalist deep learning framework developed by Hugging Face, designed primarily for inference. It supports CPU and NVIDIA GPU (via cuTENSOR and cuDNNv8) and Apple Silicon (Metal) execution, and can run models like LLaMA, Mistral, and Whisper. Benchmarks show Candle outperforming PyTorch in inference tasks.
Burn is a comprehensive deep learning framework supporting both training and inference. It is backend-agnostic, supporting LibTorch, WGPU (cross-platform GPU), Metal, Vulkan, and NdArray for CPU-only environments. Burn also supports importing ONNX models, allowing models trained in PyTorch or TensorFlow to be deployed in Rust.
Linfa is a classical machine learning toolkit in the spirit of scikit-learn, providing algorithms for classification, regression, clustering, and dimensionality reduction.
ort provides Rust bindings for ONNX Runtime, the industry-standard runtime for deploying models trained in any framework in a production environment.
polars is a DataFrame library written in Rust that is dramatically faster than pandas for large datasets, widely used in data preprocessing pipelines for AI applications.
A Practical Example: Linear Regression from Scratch
Let us implement a simple linear regression model from scratch to illustrate how numerical computing works in Rust. This example uses only the standard library:
/// A simple linear regression model using gradient descent.
///
/// The model learns the relationship y = weight * x + bias
/// from a set of training examples.
struct LinearRegression {
weight: f64,
bias: f64,
learning_rate: f64,
}
impl LinearRegression {
fn new(learning_rate: f64) -> Self {
Self { weight: 0.0, bias: 0.0, learning_rate }
}
fn predict(&self, x: f64) -> f64 {
self.weight * x + self.bias
}
fn compute_loss(&self, xs: &[f64], ys: &[f64]) -> f64 {
let n = xs.len() as f64;
xs.iter()
.zip(ys.iter())
.map(|(&x, &y)| {
let error = self.predict(x) - y;
error * error
})
.sum::<f64>()
/ n
}
fn train_step(&mut self, xs: &[f64], ys: &[f64]) {
let n = xs.len() as f64;
let mut d_weight = 0.0;
let mut d_bias = 0.0;
for (&x, &y) in xs.iter().zip(ys.iter()) {
let error = self.predict(x) - y;
d_weight += error * x;
d_bias += error;
}
self.weight -= self.learning_rate * (2.0 / n) * d_weight;
self.bias -= self.learning_rate * (2.0 / n) * d_bias;
}
fn fit(&mut self, xs: &[f64], ys: &[f64], epochs: u32) {
for epoch in 0..epochs {
self.train_step(xs, ys);
if epoch % 100 == 0 {
let loss = self.compute_loss(xs, ys);
println!(
"Epoch {:4}: loss={:.6}, w={:.4}, b={:.4}",
epoch, loss, self.weight, self.bias
);
}
}
}
}
fn main() {
// Training data: y ≈ 2x + 1
let xs = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
let ys = vec![3.1, 5.0, 6.9, 9.1, 11.0, 13.2, 15.1, 16.9, 19.0, 21.1];
let mut model = LinearRegression::new(0.01);
model.fit(&xs, &ys, 1000);
println!("\nFinal model: y = {:.4} * x + {:.4}", model.weight, model.bias);
println!("Prediction for x=11: {:.4}", model.predict(11.0));
println!("Expected (approx): {:.4}", 2.0 * 11.0 + 1.0);
}
Using ndarray for Numerical Computing
[dependencies]
ndarray = "0.17.2"
use ndarray::{array, Array1, Array2};
fn main() {
let v: Array1<f64> = array![1.0, 2.0, 3.0, 4.0, 5.0];
let m: Array2<f64> = array![
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0]
];
let doubled = &v * 2.0;
println!("Doubled: {:?}", doubled);
println!("Sum: {}", v.sum());
println!("Mean: {}", v.mean().unwrap());
println!("Matrix shape: {:?}", m.shape());
let row = m.row(0);
println!("First row: {:?}", row);
}
Integrating with Python: The PyO3 Bridge
One of the most practical patterns for AI development is to use Python for model training and experimentation, and Rust for performance-critical production code. The pyo3 crate (version 0.28.3) allows you to write Python extension modules in Rust.
[lib]
name = "my_rust_module"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.28.3", features = ["extension-module"] }
use pyo3::prelude::*;
/// A fast Rust function exposed to Python.
#[pyfunction]
fn fast_sum(numbers: Vec<f64>) -> f64 {
numbers.iter().sum()
}
/// A Python module implemented in Rust.
#[pymodule]
fn my_rust_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(fast_sum, m)?)?;
Ok(())
}
After building with cargo build --release, you can import this module in Python and call fast_sum just like any other Python function — but with the performance of native Rust code.
Parallel Data Processing with Rayon
The rayon crate (version 1.12.0) provides data parallelism through parallel iterators. You can convert a sequential iterator into a parallel one with a single method call, and rayon automatically distributes the work across all available CPU cores:
[dependencies]
rayon = "1.12.0"
use rayon::prelude::*;
fn main() {
let data: Vec<f64> = (0..10_000_000)
.map(|i| i as f64 * 0.001)
.collect();
// Sequential processing.
let start = std::time::Instant::now();
let sequential_sum: f64 = data.iter().map(|&x| x.sin() * x.cos()).sum();
println!("Sequential: {:.6} in {:?}", sequential_sum, start.elapsed());
// Parallel processing with rayon. Just change .iter() to .par_iter()!
let start = std::time::Instant::now();
let parallel_sum: f64 = data.par_iter().map(|&x| x.sin() * x.cos()).sum();
println!("Parallel: {:.6} in {:?}", parallel_sum, start.elapsed());
}
On a multi-core machine, the parallel version will be several times faster than the sequential version, with no changes to the logic of the computation.
CHAPTER 19: DEBUGGING AND PROFILING RUST APPLICATIONS
Debugging with rust-lldb and rust-gdb
Rust compiles to native machine code, so you can use standard debuggers. Rust provides wrappers called rust-lldb and rust-gdb that add Rust-aware pretty-printing. To debug a program:
cargo build
rust-lldb target/debug/my_program
For a more integrated experience, VS Code with the CodeLLDB extension provides a graphical debugger with full Rust support.
The dbg! Macro
For quick debugging, the dbg! macro prints the file name, line number, and value of an expression to stderr, then returns the value:
fn main() {
let a = 2;
let b = dbg!(a * 2) + 1; // Prints: [src/main.rs:3] a * 2 = 4
println!("b = {}", b); // Prints: b = 5
}
Logging with log and env_logger
[dependencies]
log = "0.4.32"
env_logger = "0.11.10"
use log::{debug, error, info, warn};
fn process_data(data: &[i32]) -> i32 {
info!("Processing {} items", data.len());
let result: i32 = data.iter().sum();
debug!("Computed sum: {}", result);
if result < 0 {
warn!("Result is negative: {}", result);
}
result
}
fn main() {
env_logger::init();
info!("Application starting");
let data = vec![1, 2, 3, 4, 5];
let result = process_data(&data);
info!("Result: {}", result);
}
Run with RUST_LOG=debug cargo run to see all log messages, or RUST_LOG=info cargo run to see only info-level and above.
Profiling with cargo-flamegraph
To find performance bottlenecks, use cargo-flamegraph, which generates a flame graph showing where your program spends its time:
cargo install flamegraph
cargo flamegraph
The resulting flame graph shows a visual representation of the call stack, with wider bars indicating functions that consume more CPU time.
CHAPTER 20: BUILDING A COMPLETE COMMAND-LINE APPLICATION
Let us put everything together and build a complete, polished command-line application: a word frequency counter that reads text from files or stdin, counts word frequencies, and displays the results. This application uses clap version 4.6.1for argument parsing.
cargo new word_freq
cd word_freq
Edit Cargo.toml:
[package]
name = "word_freq"
version = "1.0.0"
edition = "2024"
[dependencies]
clap = { version = "4.6.1", features = ["derive"] }
Replace src/main.rs with the following complete application:
use clap::Parser;
use std::collections::HashMap;
use std::fs;
use std::io::{self, BufRead};
use std::path::PathBuf;
/// A command-line tool for counting word frequencies in text.
///
/// Reads from one or more files, or from standard input if no files
/// are specified. Outputs the most frequent words.
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
/// Input files to process. Reads from stdin if none provided.
files: Vec<PathBuf>,
/// Number of top words to display.
#[arg(short, long, default_value_t = 10)]
top: usize,
/// Minimum word length to consider.
#[arg(short, long, default_value_t = 1)]
min_length: usize,
/// Show results in descending order of frequency.
#[arg(short, long)]
descending: bool,
}
/// Counts word frequencies in a given text.
///
/// Words are normalized to lowercase and stripped of punctuation.
fn count_words(text: &str, min_length: usize) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for word in text.split_whitespace() {
let clean: String = word
.chars()
.filter(|c| c.is_alphabetic())
.collect::<String>()
.to_lowercase();
if clean.len() >= min_length {
*counts.entry(clean).or_insert(0) += 1;
}
}
counts
}
fn read_file(path: &PathBuf) -> Result<String, String> {
fs::read_to_string(path)
.map_err(|e| format!("Cannot read '{}': {}", path.display(), e))
}
fn read_stdin() -> String {
let stdin = io::stdin();
let mut text = String::new();
for line in stdin.lock().lines() {
match line {
Ok(l) => { text.push_str(&l); text.push('\n'); }
Err(e) => { eprintln!("Error reading stdin: {}", e); break; }
}
}
text
}
fn main() {
let args = Args::parse();
let mut all_text = String::new();
if args.files.is_empty() {
eprintln!("Reading from stdin (press Ctrl+D when done)...");
all_text = read_stdin();
} else {
for path in &args.files {
match read_file(path) {
Ok(text) => all_text.push_str(&text),
Err(e) => eprintln!("Warning: {}", e),
}
}
}
if all_text.is_empty() {
eprintln!("No text to process.");
std::process::exit(1);
}
let counts = count_words(&all_text, args.min_length);
let mut sorted: Vec<(String, usize)> = counts.into_iter().collect();
if args.descending {
sorted.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
} else {
sorted.sort_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0)));
}
let top_n = args.top.min(sorted.len());
println!("{:<20} {:>10}", "Word", "Frequency");
println!("{}", "-".repeat(32));
for (word, count) in sorted.iter().rev().take(top_n) {
println!("{:<20} {:>10}", word, count);
}
println!("{}", "-".repeat(32));
println!("Total unique words: {}", sorted.len());
}
Build and run:
cargo build --release
echo "the quick brown fox jumps over the lazy dog the fox" \
| ./target/release/word_freq --top 5 --descending
The application automatically generates help text from the documentation comments and argument definitions. Run ./target/release/word_freq --help to see it.
CHAPTER 21: NEXT STEPS AND THE RUST ECOSYSTEM
You have now covered the core of the Rust language. You understand ownership, borrowing, and lifetimes. You can define structs and enums, implement traits, write generic code, handle errors with Result and Option, use closures and iterators, write concurrent code, and build complete applications. You are ready to write real Rust programs.
Advanced Language Features to Explore
Macros are a powerful metaprogramming system that allows you to write code that generates code. The derive macros we used throughout this book (Debug, Clone, Serialize, etc.) are examples of procedural macros. You can write your own to eliminate repetitive boilerplate.
Async Rust has much more depth than we covered. Understanding how Futures work internally, how to write your own async combinators, and how to choose between different async runtimes (Tokio, async-std, smol) will make you a more effective async programmer.
The type system has features we have not fully covered: associated types in traits, higher-ranked trait bounds (HRTB), const generics (which allow you to write generic code parameterized by constant values, like array sizes), and the async traits that have been progressively stabilized in recent Rust versions.
The Rust 2025 Edition (available since Rust 1.85) brings better syntax, more expressive macros, improved async ergonomics, and enhanced pattern matching. Make sure your Cargo.toml specifies edition = "2024" (or the upcoming 2025 edition when it stabilizes) to take advantage of the latest language improvements.
Essential Resources
| Resource | URL |
|---|---|
| The Rust Book | doc.rust-lang.org/book |
| Rust by Example | doc.rust-lang.org/rust-by-example |
| The Rustonomicon | doc.rust-lang.org/nomicon |
| Crate documentation | docs.rs |
| Package registry | crates.io |
| Rust Users Forum | users.rust-lang.org |
The Rust Community
One of Rust's greatest assets is its community. The Rust community has a reputation for being welcoming, patient, and genuinely helpful to newcomers. The language's complexity means that everyone has been a beginner at some point, and experienced Rust developers generally remember that and are generous with their time.
The Rust project itself is governed by a set of working groups and teams that manage different aspects of the language and ecosystem. The development process is open: you can follow the progress of new features on the Rust RFC repository on GitHub, participate in discussions, and even contribute code.
APPENDIX A: RUST QUICK REFERENCE
| Concept | Syntax |
|---|---|
| Immutable variable | let x: i32 = 5; |
| Mutable variable | let mut x: i32 = 5; |
| Function | fn name(param: Type) -> ReturnType { body } |
| Struct definition | struct Name { field: Type } |
| Struct method | impl Name { fn method(&self) -> T { ... } } |
| Enum definition | enum Name { Variant1, Variant2(Type) } |
| Pattern matching | match value { Pattern => expr, _ => expr } |
| Immutable reference | &value |
| Mutable reference | &mut value |
| Option type | Some(value) or None |
| Result type | Ok(value) or Err(error) |
| Error propagation | expression? |
| Closure | |param| expression or |param| { body } |
| Move closure | move |param| expression |
| Generic function | fn name<T: Trait>(x: T) -> T { ... } |
| Trait definition | trait Name { fn method(&self) -> T; } |
| Trait implementation | impl TraitName for TypeName { ... } |
| Heap allocation | Box::new(value) |
| Shared ownership | Rc::new(value) / Arc::new(value) |
| Interior mutability | RefCell::new(value) |
Cargo commands:
cargo new name # Create a new project
cargo build # Compile in debug mode
cargo build --release # Compile with optimizations
cargo run # Compile and run
cargo test # Run all tests
cargo doc --open # Generate and open documentation
cargo fmt # Format code
cargo clippy # Run lints
cargo update # Update dependencies
rustup update # Update the Rust toolchain
CLOSING THOUGHTS
Learning Rust is an investment. The borrow checker will frustrate you. The compiler's error messages, while excellent, will sometimes feel like a lecture from a very strict professor. You will write code that you are sure is correct, and the compiler will disagree with you, and you will spend twenty minutes understanding why the compiler is right.
But then something will click. You will understand why the rules exist. You will start to see memory bugs in other languages that you would never have noticed before. You will write concurrent code and realize, with a small shock of delight, that you are not afraid of it. You will look at your Rust code and know, with a confidence that no other language gives you, that it will not corrupt memory, will not have data races, and will not crash due to a null pointer.
That feeling is worth the investment. Welcome to Rust.
Rust version covered: 1.96.0 (stable, released May 28, 2026), 2024 Edition
Key library versions: tokio 1.52.3 · serde 1.0.228 · clap 4.6.1 · rayon 1.12.0 · ndarray 0.17.2 · pyo3 0.28.3 · anyhow 1.0.102 · thiserror 2.0.18 · log 0.4.32 · env_logger 0.11.10
No comments:
Post a Comment