Use --release when you care about performance (benchmarks, production builds).
Add Dependencies to Your Project
[dependencies]
rand = "0.8"
Add crates under [dependencies] in Cargo.toml.
Then use them in your code:
use rand::Rng;
fn main() {
let mut rng = rand::thread_rng();
let n: u8 = rng.gen_range(0..=9);
println!("Random number: {n}");
}
On next cargo build or cargo run, Cargo will:
download rand and its dependencies
compile them and your project
Useful Cargo Commands for a New Project
Command
Description
cargo new <name>
Create a new project directory with Cargo files
cargo init
Initialize Cargo in the current directory
cargo build
Compile the project (debug mode)
cargo run
Build and run the binary
cargo check
Type-check without producing binaries (faster feedback)
cargo test
Run tests (if any exist in tests/ or src)
cargo clean
Remove the target/ directory
For everyday development, you'll mostly use: cargo run, cargo check, and cargo test.
Comments in Rust
Introduction
Rust supports several kinds of comments, each serving a different purpose:
Line comments — for normal explanations
Block comments — for multi-line notes or temporarily disabling code
Documentation comments — generate HTML docs and appear in rustdoc
Comments are ignored by the compiler (except documentation comments, which are processed by rustdoc).
Line Comments (//)
// This is a single-line comment
let x = 10; // you can also put comments at the end of a line
Starts with // and continues until the end of the line.
Most common form of comments in Rust.
Great for explaining logic or adding short descriptions.
Block Comments (/* ... */)
/*
This is a block comment.
It can span multiple lines.
*/
let x = 5;
Useful for larger explanations or temporarily disabling code.
Can contain nested block comments — Rust supports nesting!
/*
Outer comment
/*
Nested comment — valid!
*/
*/
This makes large-scale commenting-out safer compared to languages like C/C++.
Documentation Comments (/// and //! )
Rust has two styles of documentation comments:
/// — for documenting items that follow.
//! — for documenting containers (crate/module) from within.
They support Markdown formatting and are used by rustdoc to generate HTML documentation.
Documentation for Functions (///)
/// Adds two numbers together.
///
/// # Examples
/// ```
/// let sum = add(2, 3);
/// assert_eq!(sum, 5);
/// ```
fn add(a: i32, b: i32) -> i32 {
a + b
}
Supports Markdown sections like:
# Examples
# Panics
# Safety
# Errors
The examples are actually compiled and tested when running cargo test!
Documentation for Modules or Crates (//! )
//! This is documentation for the entire module or crate.
//! It describes the purpose and structure at a high level.
//! You typically put this at the top of `lib.rs` or inside a module.
//! comments apply to the enclosing item (crate or module).
Ideal for explaining architecture or high-level concepts.
Inner vs Outer Documentation Comments
Kind
Syntax
Applies To
Outer doc
///
Item that follows
Inner doc
//!
Enclosing module or crate
Doc Comments Support Markdown
/// # Title
/// - Bullet point
/// - Another
///
/// `inline code`
///
/// ```rust
/// let x = 5;
/// println!("{}", x);
/// ```
fn demo() {}
This Markdown is rendered into HTML when running cargo doc.
You can use bold, italic, links, headings, code blocks, and tables.
Generate Docs and Open Them
$ cargo doc --open
Builds documentation using rustdoc and opens it in your browser.
Includes documentation for your dependencies too.
Temporary Disabling Code with Block Comments
/*
fn calculate() {
// temporarily disabled
}
*/
Because Rust supports nested block comments, disabling large sections is safe:
/*
fn a() {}
/*
fn b() {}
*/
fn c() {}
*/
Unlike C/C++, nested comments won't break the code.
Choosing the Right Comment Style
Use Case
Style
Example
Explain a line of code
//
// explanation
Explain a block or disable code
/* ... */
/* long comment */
Document a function/struct/enum
///
/// Adds two numbers
Document a module/crate
//!
//! Top-level docs
Formatted Print
Introduction
Rust provides a powerful and type-safe formatting system through println!, print!, format!, and related macros.
The formatting engine is part of the std::fmt module and uses traits like Display and Debug to render values.
Mutation affects only the binding, not necessarily the value's ownership rules.
Use mutation only when necessary.
Type Annotations
let x: i32 = 42;
let name: &str = "Alice";
Rust usually infers types automatically.
Type annotations are needed when:
Type inference is ambiguous.
You want to guide or constrain the type.
Working with generics or traits.
Shadowing
let x = 5;
let x = x + 1; // shadows the previous x
{
let x = x * 2;
println!("x inside block = {}", x); // 12
}
println!("x outside block = {}", x); // 6
Shadowing allows creating a new binding with the same name.
The new binding replaces the previous one within that scope.
Unlike mut, shadowing can change:
the type
the mutability
the structure of the value
Shadowing is useful for transformations:
let spaces = " ";
let spaces = spaces.len();
Here, a &str becomes a usize.
Destructuring Bindings
let (a, b, c) = (1, 2, 3);
println!("a = {}, b = {}, c = {}", a, b, c);
Rust allows pattern matching in variable bindings.
Patterns include:
Tuples
Structs
Enums
Nested patterns
struct Point { x: i32, y: i32 }
let p = Point { x: 10, y: 20 };
let Point { x, y } = p;
println!("x = {}, y = {}", x, y);
Use _ to ignore values:
let (x, _) = (10, 99);
Binding by Reference
let x = 10;
let ref_x = &x;
println!("ref_x = {}", ref_x);
Bindings can store references.
You can also destructure references:
let value = 5;
let &ref_to_value = &value;
// let ref_to_value = value; // same end result for Copy types
println!("{}", ref_to_value); // 5
Mutable References as Bindings
let mut x = 10;
let y = &mut x;
*y += 5;
println!("x = {}", x);
Mutable references require the original binding to be mut.
The borrowchecker enforces rules: only one mutable reference allowed at a time.
Binding with match Patterns
let numbers = vec![1, 2, 3];
match numbers.as_slice() {
[first, .., last] => println!("first = {}, last = {}", first, last),
_ => println!("Too short"),
}
Using let in if and while let
if let Some(n) = Some(5) {
println!("n = {}", n);
}
let mut v = vec![1, 2, 3];
while let Some(n) = v.pop() {
println!("popped {}", n);
}
if let is for matching a single pattern.
while let runs repeatedly as long as the pattern matches.
Both rely on variable binding through patterns.
Freezing via Immutable Binding
let mut x = 10;
let y = &x;
println!("y = {}", y);
// x += 1; // ERROR: x frozen while y exists
An immutable reference "freezes" the original variable: it cannot be mutated while the reference exists.
Rules enforced by the borrowchecker guarantee safe access.
Summary of Variable Binding Rules
Binding Feature
Description
Example
Immutable by default
Cannot change value
let x = 5;
Mutable binding
Explicit mut required
let mut x = 5;
Shadowing
Create new binding with same name
let x = x + 1;
Pattern binding
Destructure tuples, structs, enums
let (a, b) = pair;
Move semantics
Non-Copy values move on assignment
let t = s;
Binding references
Store & or &mut
let r = &x;
Types
Introduction
Rust is a statically typed language, meaning every value has a known type at compile time.
Most types fall into these categories:
Primitive types
Compound types
User-defined types (structs, enums)
Reference types
Function and closure types
Generic and trait object types
Type Inference
let x = 10; // inferred as i32
let name = "Alice"; // inferred as &str
Annotating Types Explicitly
let x: i64 = 100;
let flag: bool = true;
let scores: [i32; 3] = [10, 20, 30];
Type annotations follow the pattern: let name: Type.
Annotations improve clarity and integration in complex codebases.
Type Aliases
type Kilometers = i32;
let distance: Kilometers = 50;
Aliases introduce new names for existing types.
This does not create a new type — only a new label.
Useful for readability, domain-specific naming, or complex types.
type Result<T> = std::result::Result<T, String>;
Primitive Types (Built-in)
Primitive types include:
Integers: i8.. i128, u8.. u128, isize, usize
Floating: f32, f64
Boolean: bool
Character: char
Tuples: (T1, T2, ...)
Arrays: [T; N]
Slices: &[T]
String slice: &str
String: String
Unit: ()
Never: !
Primitive types are covered in detail in the “Primitives” chapter.
Compound Types
let tup: (i32, &str) = (10, "Hi");
let arr: [u8; 4] = [1, 2, 3, 4];
Compound types group multiple values together.
Rust supports tuples and arrays as built-in compound types.
More expressive types can be created with structs and enums.
User-Defined Types
struct User {
id: u32,
name: String,
}
enum Direction {
Up,
Down,
Left,
Right,
}
type Id = u64;
Reference Types
let x = 10;
let r1: &i32 = &x;
let mut y = 20;
let r2: &mut i32 = &mut y;
References allow borrowing values without taking ownership.
Two main reference types:
&T — shared reference (immutable)
&mut T — mutable reference
Rules enforced by borrowchecker ensure memory safety:
Any number of immutable borrows
Exactly one mutable borrow
Mutable and immutable borrows cannot coexist
Function Types
fn add(a: i32, b: i32) -> i32 {
a + b
}
Function items have a unique type based on their signature.
If From<A> for B is implemented, then Into<B> for A is automatically implemented.
Use Into in function parameters to accept flexible input types.
Fallible Conversions: TryFrom and TryInto
use std::convert::TryFrom;
let x: u8 = 200;
let result = i8::try_from(x);
match result {
Ok(n) => println!("n = {}", n),
Err(e) => println!("Error: {}", e),
}
use std::convert::TryInto;
let val: i16 = (-5).try_into().unwrap(); // will panic!
Use these traits when conversion may fail.
Returns Result<T, E>.
Never use as for fallible situations unless you want truncation.
String Conversions
A. ToString
let n = 123;
let s1 = n.to_string();
let s2 = format!("Number: {}", n);
let s3: String = "hello".into();
Any type implementing Display or Debug also implements ToString.
format! is the most flexible method.
B. FromString(Parsing)
let s = "42";
let n: i32 = s.parse().unwrap();
parse() uses the FromStr trait.
Returns Result<T, E>.
Valid for:
numbers
booleans
IPs
custom types implementing FromStr
C. String Slice <-> String
let s: &str = "hello";
let owned: String = s.to_string();
let another: &str = &owned; // &String → &str
&String automatically coerces to &str (one of Rust’s few implicit conversions).
Often used with network or file system operations.
Summary of loop
Feature
Description
Example
Basic loop
Repeats forever
loop { ... }
Break
Stops the loop
break;
Break with value
Returns value from loop-expression
break x;
Continue
Skip to next iteration
continue;
Loop labels
Control nested loops
'label: loop { ... }
State machines
Ideal for multi-step logic
match state { ... }
loop is simple, powerful, and often the foundation of more complex looping constructs.
Mastering loop helps build systems-level code, state machines, servers, and embedded applications in Rust.
while
Introduction
The while loop runs as long as its condition evaluates to true.
Unlike loop, while ends automatically once its condition becomes false.
The condition must be a boolean (bool).
while and for do not return values with break like what loop could do.
Basic while loop
let mut n = 1;
while n <= 5 {
println!("{}", n);
n += 1;
}
This prints numbers from 1 to 5.
Once n becomes 6, the condition becomes false and iteration stops.
Using break in a while loop
let mut x = 0;
while x < 10 {
if x == 4 {
break; // stop the loop early
}
println!("{}", x);
x += 1;
}
break exits the loop immediately.
Using continue in while
let mut x = 0;
while x < 5 {
x += 1;
if x % 2 == 0 {
continue; // skip even numbers
}
println!("{}", x);
}
continue skips the current iteration and jumps to the next one.
Countdown example
let mut count = 3;
while count > 0 {
println!("{}", count);
count -= 1;
}
println!("Lift off!");
Classic countdown pattern.
while with conditions that change inside the loop
let mut temperature = 20;
while temperature < 25 {
println!("Heating... {}", temperature);
temperature += 1;
}
Conditions often depend on values updated inside the loop body.
while as an alternative to some for loops
let arr = [10, 20, 30, 40];
let mut i = 0;
while i < arr.len() {
println!("{}", arr[i]);
i += 1;
}
This mimics classical for-loops from languages like C or Java.
But in Rust, for is usually preferred for iteration.
while let (pattern-based iteration)
let mut opt = Some(3);
while let Some(x) = opt {
println!("{}", x);
opt = if x > 1 { Some(x - 1) } else { None };
}
while let continues looping as long as the pattern matches.
Very useful for popping or consuming items gradually.
Using while let with collections
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("Popped: {}", top);
}
Vec::pop() returns Some(value) until the vector is empty.
When it becomes None, the loop stops.
This is idiomatic Rust.
Labelled while loops
'outer: while true {
let mut x = 0;
while x < 5 {
if x == 2 {
break 'outer; // exit the outer loop
}
x += 1;
}
}
Labels allow you to control which loop you break from.
Useful in nested loops with complex logic.
Infinite while loop (not recommended)
while true {
println!("Running...");
}
Works, but using loop is more idiomatic for deliberate infinite loops.
while for waiting on conditions
let mut ready = false;
while !ready {
println!("Waiting...");
// check some condition...
ready = true; // simulate becoming ready
}
println!("Ready!");
Often used with hardware events, threads, or async tasks (though async uses await instead).
for and Range
Introduction
The for loop in Rust is used to iterate over items in a sequence.
Unlike C/Java style loops, Rust does not use index-based counting automatically.
Instead, for iterates over anything that implements the IntoIterator trait, including:
ranges
arrays
vectors
strings
custom iterator types
It is safe, avoids indexing errors, and is the idiomatic way to loop in Rust.
Basic for loop with a range
for i in 0..5 {
println!("{}", i);
}
0..5 is a range from 0 to 4 (end is exclusive).
This prints: 0 1 2 3 4.
Inclusive range
for i in 0..=5 {
println!("{}", i);
}
0..=5 includes the end value.
This prints: 0 1 2 3 4 5.
Reverse iteration using rev()
for i in (1..5).rev() {
println!("{}", i);
}
Prints: 4 3 2 1
Ranges must be in parentheses when using methods like rev().
Iterating over an array
let arr = [10, 20, 30];
for value in arr {
println!("{}", value);
}
Rust copies each element for simple types (like i32).
For non-Copy types, values are moved (ownership transferred).
Iterating over a vector by reference
let v = vec![10, 20, 30];
for value in &v {
println!("{}", value);
}
&v borrows the vector instead of moving it.
Inside the loop, value is of type &i32.
The vector remains usable after the loop.
Mutable iteration over a vector
let mut v = vec![1, 2, 3];
for value in &mut v {
*value *= 2;
}
println!("{:?}", v);
&mut v gives mutable references to elements.
You must dereference *value to modify.
Result: [2, 4, 6]
Enumerating index and value
let arr = ["a", "b", "c"];
for (index, value) in arr.iter().enumerate() {
println!("{}: {}", index, value);
}
.iter() borrows elements.
.enumerate() gives (index, value) pairs.
Iterating over characters in a string
let s = "Hello";
for c in s.chars() {
println!("{}", c);
}
.chars() iterates Unicode scalar values (char).
Each char is 4 bytes.
Iterating over bytes in a string
let s = "Hello";
for b in s.bytes() {
println!("{}", b);
}
.bytes() iterates raw UTF-8 bytes (u8).
Useful for parsing low-level binary data.
Using for with patterns
let pairs = vec![(1, 2), (3, 4)];
for (a, b) in pairs {
println!("{} + {} = {}", a, b, a + b);
}
Rust destructures tuples directly in the loop head.
Looping multiple ranges (Cartesian product)
for x in 0..3 {
for y in 0..3 {
println!("({}, {})", x, y);
}
}
Nesting for loops is straightforward.
Breaking from a for loop
for i in 0..10 {
if i == 4 {
break;
}
println!("{}", i);
}
break stops the loop early.
Continuing to next iteration
for i in 0..10 {
if i % 2 == 0 {
continue; // skip even numbers
}
println!("{}", i);
}
continue skips the remain of the current iteration.
Labelled for loops
'outer: for x in 0..3 {
for y in 0..3 {
if x == 1 && y == 1 {
break 'outer;
}
println!("({}, {})", x, y);
}
}
Labels allow breaking out of outer loops.
Range types: from and to
// exclusive end
let r1 = 1..5; // 1,2,3,4
// inclusive end
let r2 = 1..=5; // 1,2,3,4,5
All range types implement Iterator or IntoIterator.
Ranges used in slices
let arr = [10, 20, 30, 40, 50];
let slice = &arr[1..4]; // elements 1 through 3
println!("{:?}", slice);
Ranges can index collections.
In slices, the upper bound is always exclusive.
Summary of for and range
Feature
Description
Example
Exclusive range
End value is excluded
0..5
Inclusive range
End value included
0..=5
Reverse iteration
Iterate backwards
(1..5).rev()
Vector iteration
Borrow or move elements
for v in &vec
Enumerate
Index + value
.enumerate()
Characters
Iterate Unicode chars
s.chars()
Pattern matching
Destructure in loop head
for (a,b) in pairs
Labels
Control nested loops
'outer: for...
for is the most idiomatic looping construct in Rust.
Ranges are powerful tools for index-free iteration.
Together, they provide safe, expressive, and concise looping patterns for everyday Rust programming.
match
Introduction
match is Rust’s powerful pattern-matching expression.
It compares a value against multiple patterns and executes the code of the first matching arm.
match must be exhaustive — all possible cases must be handled.
match is an expression and can return a value.
Basic match
let n = 3;
match n {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("something else"),
}
_ is the wildcard pattern: matches anything.
The match is exhaustive because _ handles all remaining cases.
match is an expression
let n = 5;
let description = match n {
1 => "one",
2 => "two",
3 => "three",
_ => "other",
};
println!("{}", description);
Both arms must produce the same type (&str here).
No semicolon inside arms unless you want to return ().
Matching ranges
let age = 20;
match age {
0..=12 => println!("child"),
13..=19 => println!("teenager"),
_ => println!("adult"),
}
Range patterns are inclusive: 0..=12.
Great for numerical classification.
Multiple patterns with |
let c = 'a';
match c {
'a' | 'e' | 'i' | 'o' | 'u' => println!("vowel"),
_ => println!("consonant or other"),
}
| acts like logical OR for matching.
Matching Option<T>
let x: Option<i32> = Some(10);
match x {
Some(n) => println!("value = {}", n),
None => println!("no value"),
}
Option is the standard way Rust handles nullability.
The match destructures the enum and extracts its data.
Matching Result<T, E>
let r: Result<i32, &str> = Ok(42);
match r {
Ok(v) => println!("Success: {}", v),
Err(e) => println!("Error: {}", e),
}
Used extensively in Rust error handling.
Patterns extract inner values.
Binding matched values
let n = 7;
match n {
1 => println!("one"),
2 => println!("two"),
other => println!("something else: {}", other),
}
other binds whatever value is matched.
Equivalent to _ but binds the value for use.
Pattern guards
let n = 10;
match n {
x if x % 2 == 0 => println!("even"),
_ => println!("odd"),
}
Pattern guards add an extra boolean condition.
Useful for additional filtering beyond pattern structure.
Destructuring tuples
let pair = (3, -3);
match pair {
(0, y) => println!("x is zero, y = {}", y),
(x, 0) => println!("y is zero, x = {}", x),
(x, y) => println!("x = {}, y = {}", x, y),
}
Matches structure and extracts values at the same time.
Destructuring structs
struct Point { x: i32, y: i32 }
let p = Point { x: 3, y: 7 };
match p {
Point { x: 0, y } => println!("on y-axis at {}", y),
Point { x, y: 0 } => println!("on x-axis at {}", x),
Point { x, y } => println!("({}, {})", x, y),
}
Allows destructuring only some fields or matching specific ones.
// Traditional match:
let value = match maybe_value {
Some(v) => v,
None => return,
};
// Cleaner with let else:
let Some(value) = maybe_value else {
return;
};
Avoids indentation and nested matches.
Improves readability when only one pattern matters.
Using let else with multiple variables
let Ok((x, y)) = do_work() else {
eprintln!("Work failed");
return;
};
println!("x = {}, y = {}", x, y);
Perfect for destructuring tuples, structs, or enum variants.
let else in loops
while let Some(line) = lines.next() {
let Ok(num) = line.parse::<i32>() else {
continue; // skip malformed input
};
println!("Parsed number: {}", num);
}
continue is a valid early exit inside loops.
Useful for parsing streams or validating input.
let else with structs
struct Point { x: i32, y: i32 }
let Point { x, y } = p else {
panic!("Not a point!");
};
println!("({}, {})", x, y);
Struct destructuring works the same as in match patterns.
Pattern guards are allowed
let Some(v) = input else {
panic!("Not an Option");
};
let n = v;
let Some(x) = n.filter(|x| *x > 10) else {
println!("Too small!");
return;
};
You can combine let else with preconditions using methods like filter.
The else block must break or return
// ERROR: compiler does not allow falling through
let Some(x) = maybe else {
println!("No value");
// nothing here — ERROR
};
The compiler requires that the else block ends control flow:
return
break
continue
panic!
Otherwise, the pattern failure would result in uninitialized variables.
Supports destructuring of references via & in the pattern.
Chaining multiple let elses
let Some(a) = opt_a else { return; };
let Some(b) = opt_b else { return; };
let Ok(sum) = add(a, b) else { return; };
println!("Sum = {}", sum);
Useful for step-by-step validation pipelines.
while let
Introduction
while let is a control-flow construct that repeatedly matches a pattern as long as it continues to succeed.
It is especially useful for:
Iterators
Processing Option values
Handling Result types in loops
State machines or stream parsing
It is syntactic sugar for a loop + if let + break combination.
Basic while let Example
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("Popped: {}", top);
}
stack.pop() returns Some(value) until the vector is empty.
When it returns None, the loop stops automatically.
No need for a manual break.
Equivalent to loop + if let
let mut stack = vec![1, 2, 3];
loop {
if let Some(top) = stack.pop() {
println!("Popped: {}", top);
} else {
break;
}
}
while let is more concise and idiomatic for this pattern.
Matching tuples with while let
let mut iter = (0..5).enumerate();
while let Some((index, value)) = iter.next() {
println!("#{} = {}", index, value);
}
You can destructure complex values inside the loop header.
while let with Result<T, E>
let mut input = vec!["10", "x", "20"].into_iter();
while let Some(s) = input.next() {
let Ok(n) = s.parse::<i32>() else {
println!("Skipping invalid number: {}", s);
continue;
};
println!("Parsed: {}", n);
}
Here while let drives iteration, while let else handles parsing.
You may combine both constructs effectively.
while let with state machines
enum State {
Start,
Number(i32),
End,
}
let mut state = Some(State::Start);
while let Some(s) = state {
match s {
State::Start => {
println!("Starting...");
state = Some(State::Number(42));
}
State::Number(n) => {
println!("Number: {}", n);
state = Some(State::End);
}
State::End => {
println!("Finished.");
state = None;
}
}
}
while let loops until state becomes None.
Extremely useful for designing simple state machines.
Processing I/O lines
use std::io::{self, BufRead};
let stdin = io::stdin();
let mut lines = stdin.lock().lines();
while let Some(Ok(line)) = lines.next() {
println!("Input: {}", line);
}
Real-world example: reading input until EOF.
Combines pattern matching with loop control.
while let with references
let numbers = vec![10, 20, 30];
let mut iter = numbers.iter();
while let Some(&n) = iter.next() {
println!("n = {}", n);
}
&n destructures a &i32 reference into an i32 value.
while let vs for loop
Use while let when...
Use for when...
You must destructure Option or Result.
You have a simple iterator.
You want to stop upon None.
You want to iterate exactly once over each element.
You have stateful logic controlling next().
Your iterations are independent and clean.
You need flexible custom loop breaking.
Standard iteration is sufficient.
Summary of while let
while let repeatedly matches a pattern until it fails.
Perfect for iterators, stacks, parsing, and state machines.
A more concise alternative to:
loop + if let + break
or a match inside a loop
Supports full pattern matching, destructuring, references, and tuple patterns.
Methods
Introduction
A method in Rust is a function defined inside an impl block and associated with a specific type (struct, enum, or trait).
Unlike a normal function, a method always has:
a receiver parameter (self, &self, or &mut self)
access to the instance it is called on
Methods allow types to encapsulate behavior, similar to OOP languages, but without inheritance.
let p = Point { x: 3.0, y: 4.0 };
println!("{}", p.distance_from_origin());
The Receiver: self, &self, &mut self
Methods may take the following receivers:
Receiver
Description
Meaning
self
takes ownership
method consumes the instance
&self
immutable borrow
method reads but cannot modify
&mut self
mutable borrow
method can modify the instance
Methods with &mut self
impl Point {
fn move_by(&mut self, dx: f64, dy: f64) {
self.x += dx;
self.y += dy;
}
}
let mut p = Point { x: 1.0, y: 2.0 };
p.move_by(5.0, -1.0);
Only mutable instances can call methods with &mut self.
Rust enforces borrowing rules strictly.
Methods with self (consuming)
impl Point {
fn into_tuple(self) -> (f64, f64) {
(self.x, self.y)
}
}
let p = Point { x: 3.0, y: 4.0 };
let t = p.into_tuple(); // p is moved and cannot be used again
Use self when a method should take ownership.
Common for builder patterns and transformation methods.
mod a {
mod b {
pub(super) fn f() {
println!("visible to parent (mod a)");
}
}
pub fn call() {
b::f(); // OK
}
}
fn main() {}
pub(super) exposes an item to the parent module only.
Siblings and outside modules cannot access it.
pub(in path): Visible Only Within a Specific Module
mod a {
pub mod b {
pub(in crate::a) fn f() {
println!("visible only inside mod a");
}
}
fn call() {
b::f(); // OK
}
}
fn main() {
// a::b::f(); // ERROR
}
pub(in path) gives fine-grained control.
You can expose functions to a specific module subtree.
pub(self): Visible Only Inside the Current Module
mod a {
pub(self) fn f() {
println!("only inside a");
}
}
This is identical to the default private visibility.
Useful for clarity in API design.
pub(restricted) Summary
Keyword
Visibility
pub
Visible everywhere
pub(crate)
Visible in the current crate
pub(super)
Visible to parent module
pub(in path)
Visible in a specific module
pub(self)
Visible only in the current module (default)
Re-exporting with pub use
mod backend {
pub fn init() {
println!("backend init");
}
}
pub use backend::init; // re-export
fn main() {
init(); // OK
}
pub use re-exports items at a different location.
Useful for creating clean public APIs.
The use Keyword in Rust
Introduction
The use keyword in Rust brings names into the current scope.
It reduces long paths and makes code cleaner.
You can use it for:
Your own modules
Crates and external libraries
Standard library items
Structs, enums, traits, and functions
Selective imports and renaming
Re-exports (public API design)
Basic Usage
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
}
Without use, you would need:
let mut map = std::collections::HashMap::new();
Using Standard Library Items
use std::fs::File;
use std::io::{self, Read};
fn main() -> io::Result<()> {
let mut file = File::open("data.txt")?;
let mut buffer = String::new();
file.read_to_string(&mut buffer)?;
Ok(())
}
use std::io::{self, Read}; imports:
io module
Read trait
Using 3rd-Party Crates
Declare dependencies in Cargo.toml:
[dependencies]
rand = "0.8"
serde = "1.0"
use rand::Rng;
fn main() {
let n = rand::thread_rng().gen_range(1..=10);
println!("Random: {}", n);
}
After adding a crate in Cargo.toml, you use use to bring items into scope.
Using Your Own Modules
// src/math.rs
pub fn add(a: i32, b: i32) -> i32 { a + b }
// src/main.rs
mod math;
use math::add;
fn main() {
println!("{}", add(5, 3));
}
This means that when you write a new attribute, you are actually writing a macro.
Beginner-level rule:
You can only define new attributes inside a library crate using procedural macros.
Three Kinds of Procedural Macros that Create Attributes
Derive macro (creates attributes like #[derive(MyTrait)])
Attribute macro (creates attributes like #[my_attribute])
Function-like macro (not an attribute, but part of procedural macros)
At beginner level, we focus mainly on derive macros and attribute macros.
Project Setup for Custom Attributes
To define custom attributes, you must create a crate of type proc-macro.
$ cargo new my_macros --lib
$ cd my_macros
# Edit Cargo.toml to enable procedural macros
[lib]
proc-macro = true
This declares the crate as a “macro crate”, allowing it to define custom attributes.
Example 1: Creating a Simple Derive Macro (easiest form)
// my_macros/src/lib.rs
use proc_macro::TokenStream;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro(_input: TokenStream) -> TokenStream {
// At beginner level, return nothing or simple message
// A real macro would inspect and modify the input code.
TokenStream::new()
}
This lets you write:
// in another crate:
use my_macros::HelloMacro;
#[derive(HelloMacro)]
struct User;
This compiles but does nothing yet — good for beginners to test setup.
Example 2: A Very Simple Attribute Macro
// inside my_macros/src/lib.rs
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn debug_message(_attr: TokenStream, item: TokenStream) -> TokenStream {
println!("debug_message attribute was used!");
item // return the original item unchanged
}
Now you can write:
// in a binary or library crate
use my_macros::debug_message;
#[debug_message]
fn greet() {
println!("Hello!");
}
This macro prints a message at compile-time when used.
It does nothing to the function itself (beginner-friendly example).
Example 3: Attribute Macro That Modifies Code (Still Beginner Level)
// my_macros/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_attribute]
pub fn make_public(_attr: TokenStream, item: TokenStream) -> TokenStream {
let mut ast = syn::parse_macro_input!(item as syn::ItemFn);
ast.vis = syn::Visibility::Public;
let expanded = quote! { #ast };
expanded.into()
}
Usage:
use my_macros::make_public;
#[make_public]
fn secret() {
println!("This is now public!");
}
This macro turns fn secret() into pub fn secret().
This is a gentle example of modifying syntax trees.
How Custom Attributes Work Internally (Beginner Explanation)
Rust passes the item (function, struct, etc.) to your macro as a TokenStream, which is a sequence of tokens: identifiers, keywords, literals, braces
Your macro:
reads the input tokens
may modify them (optional)
returns new code back to the compiler
Popular crates that help:
syn — parses Rust code into a syntax tree
quote — converts syntax tree back to tokens
Limitations of Custom Attributes
You cannot arbitrarily attach attributes unless they correspond to actual procedural macros.
You must put custom macros in a proc-macro crate.
Beginner-level macros often:
modify visibility
auto-generate trivial functions
add print/debug statements
derive simple traits
Summary
Custom attributes in Rust are created using procedural macros.
You must use a proc-macro crate.
Two forms of attribute macros:
#[derive(MyTrait)] — derive macros
#[my_attribute] — attribute macros
They operate on Rust code represented as token streams.
Beginner-level macros can modify visibility or add simple behaviors.
Popular crates syn and quote make macros easier to write.
The dead_code Attribute in Rust
Introduction
Rust warns you when you define a function, variable, struct, or module that is never used.
The compiler issues this warning under the lint named dead_code.
You can suppress this warning using the attribute:
#[allow(dead_code)]
#![allow(dead_code)] (crate-level)
Why Does Rust Warn About Dead Code?
Unused code may indicate mistakes:
a function you forgot to call
an outdated variable
an abandoned struct or enum
The warning encourages:
clean codebases
smaller binaries
better maintainability
Basic Usage of #[allow(dead_code)]
#[allow(dead_code)]
fn unused_function() {
println!("This function is never called");
}
#[cfg(feature = "gui")]
mod gui;
#[cfg(feature = "cli")]
mod cli;
Only the selected module is compiled.
Conditional Blocks Using cfg!
cfg! does not remove code from compilation.
It simply evaluates to true or false.
fn main() {
if cfg!(windows) {
println!("This binary was compiled for Windows");
}
if cfg!(target_arch = "aarch64") {
println!("Compiled for ARM64");
}
}
Conditional Use Statements
#[cfg(windows)]
use winapi::um::winuser::MessageBoxA;
#[cfg(unix)]
use libc::printf;
Here <T>, <T, E> are declarations: "this item is generic over these type parameters".
Inside the body, T and E can be used like normal types.
Declaring Generics on impl Blocks
The impl keyword can also introduce generics:
struct Point<T> {
x: T,
y: T,
}
// Declare T on the impl:
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
fn x(&self) -> &T {
&self.x
}
}
impl<T> declares that this whole block is generic over T.
Point<T> after that is a use: we apply the same T to the type.
This means: "for every T, these methods exist for Point<T>".
Using Generics: Applying Concrete Types
When you see something like Point<i32> or Storage<String>, you are choosing a concrete type for the generic:
let p_int: Point<i32> = Point::new(1, 2);
let p_str: Point<&str> = Point::new("x", "y");
Here <i32> and <&str> are applications, not declarations.
Same idea appears in trait implementations:
trait Storage<T> {
fn store(&self, value: T);
}
struct IntStorage;
// Implement Storage for the concrete type T = i32
impl Storage<i32> for IntStorage {
fn store(&self, value: i32) {
println!("storing number: {}", value);
}
}
Storage<i32> means: "this impl is for the trait Storage specialized to i32."
Notice: we did not write impl<T> here, so this impl is not generic.
Generic impl vs Concrete impl
Compare a generic implementation and a concrete one side by side:
struct Boxed<T> {
value: T,
}
// 1. Generic impl: applies to all T
impl<T> Boxed<T> {
fn value(&self) -> &T {
&self.value
}
}
// 2. Concrete impl: only for Boxed<i32>
impl Boxed<i32> {
fn is_positive(&self) -> bool {
self.value > 0
}
}
impl<T> Boxed<T>:
<T> after impl = declare T.
Methods work for any Boxed<T>.
impl Boxed<i32>:
No generics after impl => this block is not generic.
We are using the concrete type Boxed<i32>.
Methods only exist for Boxed<i32>, not for other Boxed<T>.
Traits + Generics: Where to Put <T>?
When combining traits and generics, angle brackets appear in both declaration and usage positions:
trait ToPair<A, B> {
fn to_pair(&self) -> (A, B);
}
struct PairHolder<T> {
left: T,
right: T,
}
// Declare T on impl, use T, A, B on the trait and type
impl<T: Clone> ToPair<T, T> for PairHolder<T> {
fn to_pair(&self) -> (T, T) {
(self.left.clone(), self.right.clone())
}
}
impl<T: Clone> declares the generic parameter T for this impl block.
ToPair<T, T> is using the trait with concrete parameters A = T, B = T.
PairHolder<T> is using the generic struct with the same T.
Generic Methods on Non-Generic Types
A type does not have to be generic to have generic methods. The method declares its own <T>:
struct Logger;
// Logger itself is not generic
impl Logger {
// Method is generic
fn log_any<T: std::fmt::Debug>(&self, value: T) {
println!("{:?}", value);
}
}
Here:
No <T> after struct Logger or after impl.
<T: Debug> after the method name log_any declares a method-level generic.
This pattern is common for "utility" methods that work for many types.
Traits With Default Generic Parameters
Traits can have generic parameters with defaults. The generic goes on the trait name; the default type goes in = ...:
trait Storage<T = String> {
fn store(&self, value: T);
}
struct Logger;
// Uses the default: T = String
impl Storage for Logger {
fn store(&self, value: String) {
println!("{}", value);
}
}
struct IntLogger;
// Overrides the default: T = i32
impl Storage<i32> for IntLogger {
fn store(&self, value: i32) {
println!("number: {}", value);
}
}
trait Storage<T = String> declares a generic with a default type.
impl Storage for Logger uses the default (String), so we omit <T>.
impl Storage<i32> for IntLogger chooses a specific type and overrides the default.
Associated Types vs Generic Parameters
Sometimes a trait uses an associated type instead of a generic parameter, so the angle brackets move away from the trait name:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
struct Counter {
current: u32,
max: u32,
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.max {
let v = self.current;
self.current += 1;
Some(v)
} else {
None
}
}
}
Here Iterator has no <T> at all.
Instead, type Item inside the trait and type Item = u32; in the impl tie the concrete type to the implementing type.
This is another way Rust connects traits with types, without angle brackets on the trait name.
Cheat Sheet: Where Do the Angle Brackets Go?
Pattern
Location of <...>
Meaning
struct Foo<T>
After struct
Declare a generic type parameter on the struct.
fn foo<T>(...)
After function name
Declare method/function generics.
trait Trait<T>
After trait
Declare generic parameters on a trait.
impl<T> Foo<T>
After impl
Declare generics for the impl block.
impl Foo<i32>
After type name
Use a concrete type for a non-generic impl.
impl Trait for Foo
No angle brackets
Non-generic trait impl using defaults or no generics.
impl Trait<i32> for Foo
After trait name
Implement a trait specialized to a concrete type.
Vec<i32>, Option<String>
After type name
Apply a concrete type to a generic type.
Generics Bounds in Rust
What Are Generic Bounds?
Generics let you write functions and types that work with many different concrete types.
Bounds restrict which concrete types are allowed by requiring that they implement one or more traits.
In other words: "T can be anything" becomes "T can be anything that implements these traits".
Basic Trait Bound Syntax on Functions
A simple generic function without bounds looks like this:
fn identity<T>(value: T) -> T {
value
}
If you want to println! the value, you need a bound so that T is printable:
use std::fmt::Display;
fn print_value<T: Display>(value: T) {
println!("value = {}", value);
}
fn main() {
print_value(42); // i32 implements Display
print_value("hello"); // &str implements Display
// print_value(vec![1, 2, 3]); // <-- does not compile: Vec<i32> does not implement Display
}
Pattern: fn name<T: SomeTrait>(arg: T) { ... }
The compiler enforces: any T used here must implement SomeTrait.
Multiple Trait Bounds on a Type Parameter
You can require that a single type implements several traits using +:
use std::fmt::Display;
fn print_and_clone<T: Display + Clone>(value: T) {
println!("value = {}", value);
let another = value.clone();
println!("cloned = {}", another);
}
Here T must implement both Display and Clone.
In more complex signatures, this inline syntax can get noisy. Then you can use a where-clause.
use std::fmt::Display;
fn compare_and_print<T, U>(x: T, y: U)
where
T: Display + PartialOrd,
U: Display + PartialOrd,
{
if x >= y {
println!("x ("{}") is greater or equal to y ("{}")", x, y);
} else {
println!("x ("{}") is less than y ("{}")", x, y);
}
}
where-clauses are purely syntactic sugar, they do not change semantics, just readability.
Bounds on Structs and Enums
You can also put bounds on types (structs, enums), not only on functions.
This is equivalent to a generic function with a single type parameter and a bound.
You can use impl Trait:
in argument position: fn f(x: impl Trait)
in return position: fn f() -> impl Trait (returning some type that implements the trait)
use std::fmt::Display;
fn make_message() -> impl Display {
// Compiler picks a concrete type (here, String),
// but callers only know "it implements Display".
String::from("Hello from impl Trait!")
}
Trait Bounds vs. Lifetimes (Short Overview)
Type parameters and lifetimes are separate kinds of generics, but you often see them together.
use std::fmt::Display;
fn print_ref<'a, T>(x: &'a T)
where
T: Display + 'a,
{
println!("x = {}", x);
}
Here:
'a is a generic lifetime parameter.
T: Display + 'a means: T must implement Display, and T must live at least as long as lifetime 'a.
This is useful when your generic types contain references and you need to express their relationships.
Blanket Implementations and the Power of Bounds
A blanket implementation is an impl for all types that satisfy some bound.
This is heavily used in the standard library to extend behavior to many types at once.
Now only legal indices (< 10) can be constructed via SafeIndex::new().
The newtype acts as a validation guard.
Newtype for Deref-like Behavior
You can give the newtype pointer-like or wrapper-like behavior via Deref:
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn main() {
let x = MyBox(10);
println!("{}", *x); // acts like &T
}
The standard library uses this heavily (e.g., String is a newtype over Vec<u8>).
Newtype Pattern for Nominal Typing
Rust is structurally typed for generics but nominally typed for structs:
Two structs with identical fields are still distinct types.
Newtypes take advantage of this to make semantic types.
struct Celsius(f64);
struct Fahrenheit(f64);
fn main() {
let c = Celsius(30.0);
let f = Fahrenheit(86.0);
// They are not interchangeable even though both wrap f64.
}
This prevents errors like mixing temperature units.
Tuple Newtypes vs. Braced Newtypes
Most newtypes use tuple syntax:
struct Age(u8);
But you can also use named fields — useful when you want clarity or multiple fields later:
struct Age {
value: u8,
}
For a pure newtype, tuple structs are idiomatic.
Zero-Cost Abstraction
Newtypes add no runtime overhead.
The compiler optimizes them away; they exist mainly at compile time.
This makes them ideal for safety and clarity without performance cost.
The `From` / `Into` Convenience
Newtypes often implement From to convert from the underlying type:
Important for patterns where the type is relevant but no runtime representation is needed.
Scoping Rules in Rust
What Is a Scope in Rust?
A scope is a region of code where:
variables are valid,
names are visible,
resources are owned,
and lifetimes are enforced.
Rust's scoping rules determine:
when a value comes into existence (creates/begins its lifetime),
when it is dropped (end of lifetime),
what is accessible from where (visibility),
how ownership and borrowing rules behave.
Scopes are introduced by:
{ ... } blocks,
functions,
loops / match arms / if-blocks,
closures,
modules.
Variable Lifetime and Scope
A variable’s lifetime begins when it is initialized and ends when its scope ends.
The variable is destroyed automatically at scope end (RAII-style behavior).
fn main() {
let x = 10; // x comes into scope
{
let y = 20; // y is valid only in this inner block
println!("x = {}, y = {}", x, y);
} // y is dropped here
// println!("{}", y); // ERROR: y does not exist anymore
} // x is dropped here
Shadowing: Redeclaring a Variable Name Inside the Same Scope
Shadowing means a new variable with the same name overrides the previous one in a narrower scope.
It creates a new binding, rather than mutating the old one.
let x = 10;
let x = x + 5; // shadow old x
let x = "hello"; // shadow again with new type
Each x has its own scope and lifetime.
Shadowing is strongly tied to scoping rules.
Inner scopes can also shadow outer variables:
let x = 1;
{
let x = 2; // shadows outer x
println!("inner x = {}", x);
}
println!("outer x = {}", x);
Ownership and Borrowing Are Scope-Based
Ownership transfers and borrow behavior depend on the scope.
A borrowed value must not outlive the owner’s scope.
let mut s = String::from("hello");
let r = &s; // immutable borrow in scope
println!("{}", r);
// r is no longer used after here, borrow ends
let mut_ref = &mut s; // OK: previous borrow ended
mut_ref.push_str(" world");
Borrow must remain within scope of the owner, but must also not overlap incorrectly with mutating borrows.
Temporary Scopes: Limiting the Lifetime of Borrows
You can create mini-scopes to limit the lifetime of a borrow:
let mut s = String::from("hello");
{
let r = &s; // borrow active only inside this block
println!("{}", r);
} // borrow ends here
let r2 = &mut s; // now allowed
r2.push_str(" world");
Function Scope and Parameter Lifetimes
Each function call creates its own new scope.
Parameters are scoped to the function body.
fn foo(x: i32) {
println!("{}", x);
} // x is dropped here
Return values are moved out of function scope.
If you return a reference, its lifetime must be tied to an input reference.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Scopes and Loops
Each iteration of a loop has its own temporary scope.
Variables declared inside a loop iteration do not survive to the next iteration.
for i in 0..3 {
let val = i * 2;
println!("{}", val);
}
// val is not accessible here
You can also introduce scopes within loops:
while condition() {
{
let temp = compute_something();
println!("{}", temp);
} // temp dropped each iteration
}
Match Arms Have Independent Scopes
Each match arm is its own separate scope.
match n {
0 => {
let x = 10;
println!("{}", x);
}
1 => {
// x does not exist here
}
_ => {}
}
This prevents accidental name collisions between branches.
Scopes in Closures
Closures capture variables from the scope in which they are created.
Captured values remain valid as long as the closure's scope allows.
let mut num = 5;
let add = |x| num + x; // borrow of num
println!("{}", add(3));
Once the closure is moved or stored, the borrowed/moved values follow closure’s own scope rules.
Scopes and Drop Order
Variables are dropped in reverse order of declaration when the scope ends.
let a = String::from("A");
let b = String::from("B");
let c = String::from("C");
// When leaving scope, drop order: c, b, a
This matters for:
RAII resource management,
locks,
temporary references,
interactions between smart pointers.
Nested scopes are dropped bottom-up:
{
let a = String::from("outer");
{
let b = String::from("inner");
} // b dropped here
} // a dropped here
Tight Scopes Help the Borrow Checker
Rust encourages making scopes small and precise.
Reducing the lifetime of borrows often resolves borrow checker errors.
let mut data = vec![1, 2, 3];
{
let first = &data[0];
println!("{}", first);
} // borrow ends earlier
data.push(4); // OK now
Lifetimes Tied to Scopes (Very Important!)
Lifetimes describe how long references are valid.
But references do not own data; ownership and lifetimes depend on scope.
let r; // r has no value yet
{
let x = 5;
r = &x; // ERROR: x does not live long enough
} // x dropped here
println!("{}", r);
The borrow checker rejects references that might outlive their owners.
This is why lifetimes and scopes are inseparable concepts.
Lifetimes in Rust
What Are Lifetimes in Rust?
A lifetime is a compile-time annotation that describes how long a reference is valid.
Every reference in Rust has a lifetime, even if you do not write it explicitly.
Rust uses lifetimes to ensure:
no dangling references,
no double-free / use-after-free,
borrows never outlive owners.
Lifetimes do not exist at runtime, they are entirely a static compile-time concept.
Why Are Lifetimes Needed?
If Rust only had &T without lifetimes, the compiler would have no way to know whether a reference remains valid.
Consider this invalid scenario:
let r;
{
let x = 10;
r = &x; // ERROR: x does not live long enough
} // x dropped here
println!("{}", r);
r would reference dropped memory — Rust prevents this via lifetime analysis.
How Lifetimes Work: The Basic Rules
Rule 1: A reference cannot outlive its owner.
Rule 2: A borrowed reference cannot outlive the scope in which it is valid.
Rule 3: Lifetimes are inferred most of the time.
Rule 4: Lifetime annotations do not change lifetimes—they only express constraints.
fn foo<'a>(x: &'a i32) -> &'a i32 {
x
}
This means: the returned reference must not outlive the input reference.
Lifetime Syntax and Meaning
A lifetime parameter starts with ', e.g. 'a, 'b, 'static.
Lifetimes appear in:
&'a T
functions (fn foo<'a>)
struct definitions
trait definitions and impl blocks
Interpretation:
&'a T = reference valid for at least lifetime 'a.
fn borrow<'a>(x: &'a i32) -> &'a i32 {
x
}
Here 'a connects the input reference with the output reference.
Multiple Input Lifetimes
Sometimes different inputs have different lifetimes. Example:
You cannot return a reference to a local variable because the variable will be dropped at the end of the function.
fn bad() -> &i32 {
let x = 10;
&x // ERROR: x does not live long enough
}
This is where lifetimes come in to ensure references never outlive data.
Borrowing with Mutable Iteration
Iteration borrows each element immutably or mutably depending on the iterator type.
let mut data = vec![1, 2, 3];
for x in &data { // immutable borrow
println!("{}", x);
}
for x in &mut data { // mutable borrow
*x *= 2;
}
After each loop, the borrow ends automatically at the end of the loop.
Borrowing and Slices
Slices (&[T], &str) are just borrowed views into contiguous memory.
let nums = vec![1, 2, 3, 4, 5];
let slice = &nums[1..4]; // borrow a view
println!("{:?}", slice);
Slices obey the same borrow rules as references.
Borrowing in Structs
Structs can store borrowed references, but require lifetime annotations.
struct Holder<'a> {
value: &'a str,
}
fn main() {
let s = String::from("hello");
let h = Holder { value: &s };
println!("{}", h.value);
}
Non-Lexical Lifetimes (NLL)
Rust 2018 introduced Non-Lexical Lifetimes:
borrows end when the reference is last used, not when the scope ends,
reduces borrow checker errors,
allows more flexible code.
let mut s = String::from("hello");
let r = &s;
println!("{}", r); // r ends here automatically
let r2 = &mut s; // OK now
r2.push_str("!");
Derive in Rust
What Is Derive?
#[derive(...)] is an attribute that automatically implements traits for your types.
It saves you from writing repetitive boilerplate code.
Derive works by generating trait implementations at compile time using procedural macros.
Basic Syntax
Place #[derive(...)] directly above a struct or enum definition.
List one or more traits inside the parentheses.
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone();
println!("{:?}", p1); // Debug
println!("{}", p1 == p2); // PartialEq
}
Common Derivable Traits (Standard Library)
Trait
Purpose
Debug
Enables {:?} formatting for printing
Clone
Allows explicit duplication via .clone()
Copy
Enables implicit bitwise copy (requires Clone)
PartialEq
Enables == and != comparison
Eq
Marks full equivalence (requires PartialEq)
PartialOrd
Enables <, >, <=, >= comparison
Ord
Total ordering (requires Eq + PartialOrd)
Hash
Allows use as key in HashMap / HashSet
Default
Provides a default value via Default::default()
The Debug Trait
Enables debug formatting with {:?} or pretty-print with {:#?}.
Essential for development and debugging.
#[derive(Debug)]
struct User {
name: String,
age: u32,
}
fn main() {
let user = User {
name: String::from("Alice"),
age: 30,
};
println!("{:?}", user); // User { name: "Alice", age: 30 }
println!("{:#?}", user); // Pretty-printed
}
The Clone and Copy Traits
Clone allows explicit duplication via .clone().
Copy enables implicit copying (no move semantics).
Copy requires Clone and only works if all fields are Copy.
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // Copy, not move
println!("{:?} {:?}", p1, p2); // Both valid
}
Types containing String, Vec, or other heap data cannot derive Copy.
The PartialEq and Eq Traits
PartialEq enables equality comparison with == and !=.
Eq is a marker trait indicating reflexive equality (every value equals itself).
Floating-point types implement PartialEq but not Eq (because NaN != NaN).
#[derive(Debug, PartialEq, Eq)]
struct Id(u64);
fn main() {
let a = Id(42);
let b = Id(42);
let c = Id(99);
println!("{}", a == b); // true
println!("{}", a == c); // false
}