12 Rust
12.1 Introduction
So far in this course we have worked with high-level, garbage-collected languages like OCaml and Ruby. These are type-safe and easy to use, but they give up low-level control and rely on a runtime garbage collector for memory management. On the other end of the spectrum, C and C++ give programmers manual memory management and direct hardware control, but at the cost of type safety.
Is there something in between?
12.1.1 What Choice Do Programmers Have?
Consider two families of languages:
- C/C++:
Type-unsafe — pointer arithmetic, unchecked casts
Low-level control over memory layout and hardware
Performance over safety and ease of use
Manual memory management, e.g., with malloc/free
- Java, OCaml, Go, Ruby, …:
Type-safe — runtime type errors are prevented statically
High-level, less control over memory
Ease-of-use and safety over performance
Automatic memory management via garbage collection — no explicit malloc/free
Rust occupies the space in between: it offers low-level control and manual memory management without a garbage collector, while still being type-safe and thread-safe.
12.2 Rust
Rust is a systems programming language designed for type safety, thread safety, and high performance.
A public, Mozilla-sponsored project since 2010, Rust was originally started in 2006 by Graydon Hoare while he was at Mozilla.
Voted the most loved programming language in Stack Overflow’s annual developer surveys every year from 2016 through 2023.
Key properties: type safety and no data races, despite using concurrency and manual memory management.
Rust achieves these properties through a novel ownership and borrowing system enforced entirely at compile time — there is no garbage collector and no runtime overhead for safety.
12.2.1 Rust in the Real World
Rust is used in production by many organizations:
Firefox Quantum and the Servo browser engine
REmacs — a port of Emacs to Rust
Amethyst game engine
Magic Pocket filesystem from Dropbox
OpenDNS malware detection components
12.2.2 Features of Rust
Rust takes ideas from both functional and object-oriented languages, as well as recent programming-languages research:
Lifetimes and Ownership — the key feature for ensuring memory and thread safety at compile time
Traits as the core of the object(-like) system
Variables are immutable by default
Data types and pattern matching (like OCaml)
Type inference — no need to write types for local variables
Generics (aka parametric polymorphism)
First-class functions
Efficient C bindings
12.3 Getting Started with Rust
12.3.1 Installing Rust
Rust can be installed via rustup, the official toolchain installer:
On macOS or Linux, open a terminal and run:
curl https://sh.rustup.rs -sSf | shOn Windows, download and run rustup-init.exe.
12.3.2 Rust Compiler and Build System
Rust programs can be compiled directly using rustc:
Source files use the .rs suffix.
Compilation produces an executable by default (no -c option).
The preferred workflow is to use cargo, Rust’s package manager and build system:
Invokes rustc as needed to build files.
Downloads and builds dependencies automatically.
Based on a .toml configuration file and a .lock file.
Analogous to ocamlbuild or dune in OCaml.
12.3.3 Using cargo
shell
> cargo new hello_cargo --bin Creating binary (application) `hello_cargo` package error: destination `/Users/anwar/git/umd/cmsc330-notes/notes/code/hello_cargo` already exists Use `cargo init` to initialize the directory
shell
> tree hello_cargo /bin/sh: tree: command not found
% cd hello_cargo
% cargo build
Compiling hello_cargo v0.1.0 (file:///...)
Finished dev [unoptimized + debuginfo] ...
% ./target/debug/hello_cargo
Hello, world!The default src/main.rs contains:
Rust REPL
fn main() { println!("Hello, world!"); } [Output] Hello, world!
cargo can also run tests; we will discuss that later in the course.
12.3.4 Rust Interactively
Unlike OCaml or Ruby, Rust has no top-level read-eval-print loop (REPL). For quick experiments, use the in-browser Rust Playground at https://play.rust-lang.org/.
12.3.5 Rust Documentation
The official documentation is an excellent reference and learning resource:
The Rust Book (The Rust Programming Language by Klabnik and Nichols) — most of our slides are based on this.
The Rust Book With Quizzes (The Rust Programming Language With Quizzes by Klabnik and Nichols) — most of our slides are based on this.
The reference manual.
Short manuals on rustc, cargo, and more.
All of these are linked from https://doc.rust-lang.org/stable/.
12.4 Rust Basics
12.4.1 Functions
Functions are declared with the fn keyword. The entry point of every Rust program is main. println! is a macro (note the !):
Rust REPL
// comment fn main() { println!("Hello, world!"); } [Output] Hello, world!
12.4.2 Let Statements
Local variables are introduced with let. By default, variables are immutable:
{
let x = 37;
let y = x + 5;
y
} //42Attempting to assign to an immutable variable is a compile-time error:
{
let x = 37;
x = x + 5; //err
x
}To allow reassignment, declare the variable with mut:
{
let mut x = 37;
x = x + 5;
x
} //42Shadowing: Rust allows redefining a variable with a new let binding, which shadows the previous binding (similar to OCaml). This is different from mutation and should generally be avoided:
{
let x = 37;
let x = x + 5; // shadows previous x
x
} //42Type annotations: Types are inferred by default. Optional annotations must be consistent with the inferred type (and may override the default numeric type):
{
let x:i16 = -1;
let y:i16 = x + 5;
y
} //4Trying to assign a value that is out of range for the declared type is an error:
{ //err:
let x:u32 = -1; // u32 cannot hold negative values
let y = x + 5;
y
}12.4.3 Conditionals
Rust conditionals look like C but the condition does not require parentheses. Braces { } around each branch are required:
Rust REPL
fn main() { let n = 5; if n < 0 { print!("{} is negative", n); } else if n > 0 { print!("{} is positive", n); } else { print!("{} is zero", n); } } [Output] 5 is positive
12.4.4 Conditionals Are Expressions
Like OCaml, if/else in Rust is an expression, not just a statement. You can use it on the right-hand side of a let:
Rust REPL
fn main() { let n = 5; let x = if n < 0 { 10 } else { 20 }; print!("x={:?}", x); } [Output] x=20
Because if is an expression, both branches must have the same type. The following is a type error — the if branch has type i32 and the else branch has type &str:
let x = if n < 0 {
10 // i32
} else {
"a" // &str <-- type error
};if x > 5 {
println!("x is greater than 5");
}if x > 5 {
1 // i32, but else branch implicitly returns ()
}12.4.5 Functions with Arguments and Return Types
Functions take typed parameters and declare a return type after ->. The last expression in the body is the return value (no return keyword needed):
Rust REPL
fn fact(n:i32) -> i32 { if n == 0 { 1 } else { n * fact(n-1) } } fn fact2(n:u32)->u32{ let mut i = n; let mut a = 1; loop{ //infinite if i < 1 {break;} a = a * i; i = i - 1; } a } fn fact3(n:u32)->u32{ let mut a = 1; for i in 1..n+1{ a = a * i; } a } fn main() { let res1 = fact(6); let res2 = fact2(6); let res3 = fact3(6); println!("fact(6) = {},{},{}", res1,res2,res3); } [Output] fact(6) = 720,720,720
Quiz: What does the following expression evaluate to?
{
let x = 6;
let y = "hi";
if x == 5 { y } else { 5 };
7
}
Quiz: What does the following expression evaluate to?
{
let x = 6;
let y = "hi";
if x == 5 { y } else { 5 };
7
}Answer: type error. The if branches have incompatible types (&str vs i32), so Rust rejects this at compile time.
Quiz: What does the following expression evaluate to?
{
let x = 6;
let y = 4;
y = x;
x == y
}
Quiz: What does the following expression evaluate to?
{
let x = 6;
let y = 4;
y = x;
x == y
}Answer: compile-time error. y is declared without mut, so the assignment y = x is rejected. y is immutable.
12.4.6 Mutation and Iteration
Mutation is useful when performing iteration, just as in C and Java. Rust provides an infinite loop construct that runs forever until you break out of it:
fn fact(n: u32) -> u32 {
let mut x = n;
let mut a = 1;
loop {
if x <= 1 { break; } // infinite loop, break out
a = a * x;
x = x - 1;
}
a
}12.4.7 Other Looping Constructs
Rust also has while loops and for loops:
while e block — loop while condition is true
for pat in e block — iterate over a collection
for x in 0..10 {
println!("{}", x); // x: i32
}The range 0..10 produces integers from 0 up to (but not including) 10.
12.4.8 Loops Are Expressions
Like if, all three looping constructs (loop, while, for) are expressions. loop returns the final computed value, or () if none. while and for always evaluate to (). A break may carry a value, which becomes the loop’s result:
let mut x = 5;
let y = loop {
x += x - 3;
println!("{}", x); // 7 11 19 35
if x % 5 == 0 { break x; }
};
print!("{}", y); // 35Quiz: What does the following expression evaluate to?
let mut x = 1;
for i in 1..6 {
let x = x + 1;
}
x
Quiz: What does the following expression evaluate to?
let mut x = 1;
for i in 1..6 {
let x = x + 1;
}
xAnswer: 1. Inside the loop, let x = x + 1 creates a new shadowed binding scoped to each iteration — it does not mutate the outer x. The outer x remains 1.
12.4.9 Scalar Types
Rust’s primitive scalar types are:
Integers — signed: i8, i16, i32, i64, isize; unsigned: u8, u16, u32, u64, usize. isize/usize are the machine word size. Default inferred type is i32.
Characters — char, Unicode scalar values.
Booleans — bool = {true, false}.
Floating point — f32, f64 (default inferred is f64).
Arithmetic operators (+, -, etc.) are overloaded across numeric types, but operands must have the same type.
12.4.10 Compound Data: Tuples
An n-tuple groups n values of possibly different types. The type of an n-tuple is written (t1, …, tn). unit () is simply the 0-tuple.
Tuple fields are accessed by index (.0, .1, …) or via pattern matching:
let tuple = ("hello", 5, 'c');
assert_eq!(tuple.0, "hello");
let (x, y, z) = tuple;Patterns may appear directly in function parameters:
fn dist(s:(f64,f64), e:(f64,f64)) -> f64 {
let (sx, sy) = s;
let ex = e.0;
let ey = e.1;
let dx = ex - sx;
let dy = ey - sy;
(dx*dx + dy*dy).sqrt()
}Equivalently, you can destructure in the parameter list:
fn dist2((sx,sy):(f64,f64), (ex,ey):(f64,f64)) -> f64 {
let dx = ex - sx;
let dy = ey - sy;
(dx*dx + dy*dy).sqrt()
}Rust structs (covered later) generalize tuples with named fields.
12.4.11 Arrays
Arrays in Rust have a fixed length known at compile time. The type of an array of n elements of type T is [T;n]. Arrays can be mutable or immutable:
let nums = [1,2,3]; // type is [i32;3]
let strs = ["Monday","Tuesday","Wednesday"]; // [&str;3]
let x = nums[0]; // 1
let s = strs[1]; // "Tuesday"
let mut xs = [1,2,3];
xs[0] = 1; // OK, xs is mutable
let i = 4;
let y = nums[i]; // fails (panics) at run-timeOut-of-bounds indexing compiles successfully but causes a runtime panic.
To iterate over an array, use .iter(), which produces an iterator (similar to Java’s Iterator). The for loop calls .next() repeatedly until no elements remain — with no possibility of going out of bounds:
let a = [10,20,30,40,50];
for element in a.iter() {
println!("the value is: {}", element);
}Quiz: Will this function type check?
fn f(n:[u32]) -> u32 {
n[0]
}
Quiz: Will this function type check?
fn f(n:[u32]) -> u32 {
n[0]
}Answer: No. Array types must include a fixed length, e.g. [u32;4]. Without a length, the size is unknown at compile time and the type is incomplete.
12.4.12 Testing
Testing in Rust is a first-class citizen — the testing framework is built into cargo, unlike most languages which require extra libraries (e.g., Minitest in Ruby, OUnit in OCaml, JUnit in Java).
Unit testing is for local or private functions. Place unit tests in the same file as your code, inside a module annotated with #[cfg(test)]. Each test function is marked #[test]:
fn bad_add(a: i32, b: i32) -> i32 {
a - b
}
#[cfg(test)] // this module contains tests
mod tests {
#[test] // this function is a test
fn test_bad_add() {
assert_eq!(bad_add(1,2), 3);
}
}Use assert! to check that a condition holds, and assert_eq! to check equality between two values that implement the PartialEq trait (e.g., integers, booleans). Traits are covered later.
Integration testing is for APIs and whole programs. Create a tests/ directory at the project root, with separate files for each major area of functionality. Integration test files do not need #[cfg(test)] or a special module, but each test function still needs #[test]. Tests treat the crate as an external library: import it with extern crate and bring items into scope with use.
12.5 Ownership, References, and Lifetimes
Rust’s most distinctive feature is its ownership system: a set of compile-time rules that eliminate entire classes of memory bugs with no runtime cost.
12.5.1 GC-less Memory Management, Safely
Rust manages heap memory without a garbage collector. The type checker ensures:
No dangling pointers and no buffer overflows — unsafe idioms are rejected at compile time.
- Safety is enforced through two key concepts: ownership and lifetimes.
Every piece of data has a single owner. Immutable aliases are permitted, but mutation is only allowed through the owner or a single mutable reference.
How long data is alive is determined by its lifetime, tracked statically by the compiler.
12.5.2 Memory: the Stack and the Heap
Recall the two memory regions:
The stack — constant-time, automatic (de)allocation. Data size and lifetime must be known at compile time (function parameters and locals of fixed size).
- The heap — dynamically sized data with non-fixed lifetime. Slightly slower to access (via a pointer). Three strategies:
GC: automatic deallocation, but adds space/time overhead.
Manual (C/C++): low overhead, but non-trivial opportunity for devastating bugs — dangling pointers, double free, and other forms of memory corruption.
Rust: automatic deallocation triggered by scope exit, no GC.
In C, the programmer manually allocates and frees heap data:
// C
char *p = malloc(10);
...
free(p); // p deleted from stack when function returnsIn Java, objects live on the heap and are collected by the GC when no longer reachable:
// Java
String p = new String("rust");
...
p = null; //GC will collect laterIn Rust, heap data is freed automatically when its owner goes out of scope — no manual free, no GC:
// Rust
let p = String::from("hello");
...
// p deleted from stack when function terminates;
// heap data freed automatically12.5.3 Rules of Ownership
Rust’s ownership system rests on three rules, enforced at compile time:
Each value in Rust has a variable that is its owner.
There can only be one owner at a time.
When the owner goes out of scope, the value is dropped (freed).
String is Rust’s dynamically-sized, heap-allocated, mutable string type. When s goes out of scope, Rust automatically calls s.drop():
{
let mut s = String::from("hello"); //s is the owner
s.push_str(", world!");
println!("{}", s);
} //s's data is freed by calling s.drop()12.5.4 Assignment Transfers Ownership
By default, assigning a heap value moves it — ownership transfers to the new variable, leaving the old variable invalid:
let x = String::from("hello");
let y = x; //x moved to yAfter the move, only y is the owner; using x is a compile-time error:
println!("{}, world!", y); //ok
println!("{}, world!", x); //fails — x was movedWhy? Both x and y could otherwise point to the same underlying heap data. Allowing both to exist would risk a double free or use-after-free when either goes out of scope. The move ensures there is always exactly one owner.
12.5.5 The Copy Trait
Primitive types — i32, char, bool, f32, tuples of these types, etc. — do not transfer ownership on assignment. Instead, assignment copies the entire value:
let x = 5;
let y = x;
println!("{} = 5!", y); //ok
println!("{} = 5!", x); //okThis works because these types implement the Copy trait. Assigning a Copy type duplicates the object rather than moving it.
Traits
A trait is a way of saying that a type has a particular property:
Copy — objects with this trait are copied on assignment rather than moved. Primitive types derive Copy automatically.
- Traits also specify functions a type must implement, similar to Java interfaces. For example:
Deref — indicates that an object can be dereferenced via the * operator; the compiler calls the object’s deref() method.
We will see more traits later in the course.
Cloning
If you need two independent copies of a heap value without losing ownership of the original, call .clone():
let x = String::from("hello");
let y = x.clone(); //x ownership not moved
println!("{}, world!", y); //ok
println!("{}, world!", x); //okCloning avoids the loss of ownership but performs a full heap copy — use it deliberately, not as a default.
12.5.6 Ownership Transfer in Function Calls
Ownership follows the same move semantics across function calls:
Passing an argument moves ownership from the caller to the called function’s parameter.
Returning a value moves ownership from the called function to the caller’s receiver.
fn main() {
let s1 = String::from("hello");
let s2 = id(s1); //s1 moved to arg
println!("{}", s2); //id's result moved to s2
println!("{}", s1); //fails — s1 was moved
}
fn id(s: String) -> String {
s // s moved to caller on return
}Passing s1 to id moves it; s1 cannot be used afterwards. The returned value is moved into s2.
12.5.7 References and Borrowing
Moving ownership into every function that needs to read a value would be cumbersome. Instead, you can lend a reference to a value without transferring ownership. This is called borrowing.
A reference is created with the & operator and is immutable by default:
Rust REPL
fn main() { let s1 = String::from("hello"); let len = calc_len(&s1); //lends reference println!("the length of '{}' is {}", s1, len); } fn calc_len(s: &String) -> usize { // s.push_str("hi"); //fails! refs are immutable s.len() // s dropped; but not its referent } [Output] the length of 'hello' is 5
When s (the reference) is dropped at the end of calc_len, only the reference is dropped — the underlying s1 in main is unaffected.
12.5.8 Rules of References
Rust enforces two rules about references at compile time:
- At any given time you can have either — but not both — of:
One mutable reference, or
Any number of immutable references.
References must always be valid (the pointed-to value must not have been dropped).
These rules prevent data races and dangling pointers statically.
12.5.9 Borrowing and Mutation
You can make an immutable reference to a mutable value. While the immutable reference is live, mutation of the original is disallowed:
{
let mut s1 = String::from("hello");
{
let s2 = &s1;
println!("String is {} and {}", s1, s2); //ok
s1.push_str(" world!"); //disallowed while s2 alive
println!("{}", s2); //otherwise, NLL (Non-Lexical Lifetimes) may drop s2 early
} //drops s2
s1.push_str(" world!"); //ok
println!("String is {}", s1); //prints updated s1
}12.5.10 Mutable References
To allow mutation through a reference, use &mut instead of &. This is only permitted on mut variables:
let mut s1 = String::from("hello");
{
let s2 = &s1;
s2.push_str(" there"); //disallowed; s2 immut
} //s2 dropped
let s3 = &mut s1; //ok since s1 mutable
s3.push_str(" there"); //ok since s3 mutable
println!("String is {}", s3); //okQuiz 1: Who is the owner of str’s data at HERE?
fn foo(str: String) -> usize {
let x = str;
let y = &x;
let w = &y;
// HERE
}
Quiz 1: Who is the owner of str’s data at HERE?
fn foo(str: String) -> usize {
let x = str;
let y = &x;
let w = &y;
// HERE
}Answer: x. When str is assigned to x, ownership moves to x. y and w are references (borrows), not owners. Immutable references implement Copy, so let w = &y copies the reference rather than moving it.
Quiz 2: What does this evaluate to?
{
let mut s1 = String::from("Hello!");
{
let s2 = &s1;
s2.push_str("World!");
println!("{}", s2)
}
}
Quiz 2: What does this evaluate to?
{
let mut s1 = String::from("Hello!");
{
let s2 = &s1;
s2.push_str("World!");
println!("{}", s2)
}
}Answer: Error — s2 is not mut. s2 is an immutable reference (&s1), so calling push_str through it is disallowed.
Quiz 3: What is printed?
fn foo(s: &mut String) -> usize {
s.push_str("Bob");
s.len()
}
fn main() {
let mut s1 = String::from("Alice");
println!("{}", foo(&mut s1))
}
Quiz 3: What is printed?
fn foo(s: &mut String) -> usize {
s.push_str("Bob");
s.len()
}
fn main() {
let mut s1 = String::from("Alice");
println!("{}", foo(&mut s1))
}Answer: 8. foo receives a mutable reference to s1, appends "Bob" to "Alice" giving "AliceBob" (length 8), and returns that length.
12.5.11 Non-lexical lifetimes (NLL)
Before (lexical lifetimes): a borrow lived until the end of its scope, even if it wasn’t used anymore.
With NLL: a borrow ends at its last use, allowing more flexibility.
let mut s = String::from("hello");
let r = &s;
println!("{}", r); // last use of r
s.push('!'); // OK with NLLNLL is enabled by default in modern Rust.
Older toolchains (e.g., some embedded setups) may lack full NLL support.
The borrow checker is still conservative: complex control flow (loops, branches, aliasing) may be rejected if safety can’t be proven.
In practice, assume borrows last to the end of scope. Use inner scopes or drop() to end them early.
12.6 Strings, Slices, Vectors, and HashMaps
12.6.1 String Representation
Rust’s String type is represented internally as a 3-tuple on the stack:
A pointer to a heap-allocated byte array (interpreted as UTF-8)
A current length (number of bytes used)
A maximum capacity (number of bytes allocated)
The invariant length ≤ capacity always holds. When the capacity is exhausted and more data is pushed, Rust reallocates a larger buffer. When the owner is dropped, the pointed-to heap data is freed automatically.
let mut s = String::new();
println!("{}", s.capacity()); // 0
for _ in 0..5 {
s.push_str("hello");
println!("{},{}", s.len(), s.capacity());
}
// prints: 0 / 5,5 / 10,10 / 15,20 / 20,20 / 25,40Capacity doubles when exhausted, so reallocation is amortized O(1) per push.
12.6.2 UTF-8 and Rust Strings
Rust strings are UTF-8 encoded. UTF-8 is a variable-length encoding:
The first 128 characters (US-ASCII) use one byte each.
The next 1,920 characters use two bytes, covering most Latin-script alphabets.
Up to four bytes for all Unicode code points.
Because a character may span multiple bytes, direct indexing of a String is rejected by the compiler — you could land in the middle of a multi-byte character:
let s1 = String::from("hello");
let h = s1[0]; // rejectedUse slices or character iterators instead.
12.6.3 Slices
A slice is a reference to a contiguous subsequence of a collection’s data — it shares the underlying bytes without copying them. A slice stores a pointer and a length but no capacity.
For a String s, the notation &s[range] creates a string slice of type &str:
&s[i..j] — bytes from index i up to (but not including) j
&s[i..] — bytes from i to the end
&s[..j] — bytes from 0 to j
&s[..] — the entire string as a slice
&str is the type of a String slice (and of string literals).
12.6.3.1 String Slice Example
pub fn first_word(s: &String) -> &str {
for (i, item) in s.char_indices() {
if item == ' ' {
return &s[0..i];
}
}
s.as_str()
}Note: char_indices() iterates over characters, not bytes. Using as_bytes() instead could split a multi-byte UTF-8 character.
12.6.3.2 Slices and Ownership
A &str slice borrows from the original String — it behaves like an immutable reference. This means the borrowing rules apply: while word (an immutable borrow) is live, you cannot mutate s:
let mut s = String::from("hello world");
let word = first_word(&s); //borrow
s.clear(); // Error! Can't take mut ref while word alive
println!("{word}");Multiple immutable slices are fine; a mutable slice conflicts with any other borrow:
// ok: two immutable slices
let b = &s[..];
let c = &s[..];
print!("{}{}", b, c);
// error: two mutable slices
let b = &mut s[..];
let c = &mut s[..]; //error
print!("{}{}", b, c);12.6.3.3 String Literals Are Slices
String literals have type &str — they are slices into read-only program memory. The variable is not the owner:
let s: &str = "hello world";The compiler establishes a static owner to permit free immutable sharing. Use String when you need to own and modify the data; use &str when you only need to read it. Functions should prefer &str parameters over &String because a String can always be coerced to &str (via the Deref trait):
fn first_word(s: &str) -> &str { ... }Quiz 1: What is the output?
let s = String::from("Rust is fun!");
let h = &s[0..4];
println!("{}", h);
Quiz 1: What is the output?
let s = String::from("Rust is fun!");
let h = &s[0..4];
println!("{}", h);Answer: Rust. &s[0..4] yields bytes 0–3, which are ’R’, ’u’, ’s’, ’t’.
Quiz 2: What is the output?
let mut s1 = String::from("Hello");
let s2 = " World";
s1.push_str(s2);
print!("{}", s2);
Quiz 2: What is the output?
let mut s1 = String::from("Hello");
let s2 = " World";
s1.push_str(s2);
print!("{}", s2);Answer: World. push_str takes a &str, so it borrows s2 rather than taking ownership. s2 remains valid after the call.
Quiz 3: What is the output?
let s1 = String::from("CMSC");
let s3; //deferred init
{
let s2 = String::from("330");
s3 = s1 + &s2;
}
print!("{}", s3);
print!("{}", s1);
Quiz 3: What is the output?
let s1 = String::from("CMSC");
let s3; //deferred init
{
let s2 = String::from("330");
s3 = s1 + &s2;
}
print!("{}", s3);
print!("{}", s1);Answer: Error — s1 lost ownership. The + operator on String consumes its left-hand operand (takes ownership of s1), so s1 cannot be used after s3 = s1 + &s2.
12.6.4 Vectors
Vec<T> is Rust’s dynamically-sized, heap-allocated array — analogous to ArrayList<T> in Java. All elements must have the same type.
{
let mut v: Vec<i32> = Vec::new();
v.push(1); // adds 1 to v
v.push("hi"); // error — v contains i32s
let w = vec![1, 2, 3]; // vec! is a macro
} // v, w and their elements droppedIndexing can be done in two ways:
&v[i] — returns a reference; panics at runtime if out of bounds.
v.get(i) — returns Option<&T>; returns None if out of bounds instead of panicking.
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2]; //panics if OOB
let third: Option<&i32> = v.get(2); //None if OOB12.6.4.1 Aside: Options
Option<T> is an enumerated type (like an OCaml variant) with two constructors:
Some(v) — contains a value v
None — no value
Use match to deconstruct it:
let v = vec![1, 2, 3, 4, 5];
let third: Option<&i32> = v.get(2);
let z = match third {
Some(i) => Some(i + 1), //matches here
None => None
};We will cover enumerated types in more detail later.
12.6.4.2 Updates and Iteration
Mutating a vector element via a mutable borrow requires the borrow to be dropped before accessing the vector again:
let mut a = vec![10, 20, 30, 40, 50];
let p = &mut a[1]; //mutable borrow
*p = 2; //updates a[1]
println!("vector contains {:?}", &a);
println!("{p}");let mut a = vec![10, 20, 30, 40, 50];
let p = &mut a[1]; //mutable borrow
*p = 2; //updates a[1]
println!("vector contains {:?}", &a);Iteration can be immutable or mutable:
let mut v = vec![100, 32, 57];
for i in &v { println!("{}", i); } // immutable
for i in &mut v { *i += 50; } // mutable12.6.4.3 Vector Slices
Like strings, vectors support slices:
let a = vec![10, 20, 30, 40, 50];
let b = &a[1..3]; //[20, 30]
let c = &b[1]; //30
println!("{}", c); //prints 30Internally, String is implemented as Vec<u8> — but you should not manipulate a String’s byte-level representation directly, as this can violate UTF-8 validity.
12.6.5 HashMaps
HashMap<K,V> is Rust’s generic hash table, mapping keys of type K to values of type V. Key methods:
new() : () -> HashMap<K,V> — create an empty map
insert(k, v) : (K,V) -> Option<V> — insert a key-value pair; returns the old value wrapped in Some, or None if the key was absent
get(&k) : (&K) -> Option<&V> — look up a key; returns a reference to the value or None
Rust REPL
use std::collections::HashMap; fn main() { // Create a new HashMap let mut scores = HashMap::new(); // Insert key-value pairs scores.insert(String::from("Alice"), 10); scores.insert(String::from("Bob"), 20); // Access a value let name = String::from("Alice"); match scores.get(&name) { Some(score) => println!("{}'s score: {}", name, score), None => println!("No score found for {}", name), } // Iterate over key-value pairs for (key, value) in &scores { println!("{}: {}", key, value); } // Update a value scores.insert(String::from("Alice"), 15); // Entry API (insert if not exists) scores.entry(String::from("Charlie")).or_insert(30); println!("Final map: {:?}", scores); } [Output] Alice's score: 10 Alice: 10 Bob: 20 Final map: {"Alice": 15, "Charlie": 30, "Bob": 20}
12.6.6 Primitive Data Conversion with as
Unlike C, Rust performs no implicit numeric conversions. To convert between primitive types use the as keyword:
Rust REPL
fn main() { let decimal = 65.4321_f32; //floating point number //let integer: u8 = decimal; //error: no auto-convert // Explicit conversion let integer = decimal as u8; // explicit conversion let character = integer as char; println!("Casting: {} -> {} -> {}", decimal, integer, character); } [Output] Casting: 65.4321 -> 65 -> A
Conversion truncates towards zero; wrapping and saturation rules apply for out-of-range values.
12.7 Structs, Enums, and Methods
So far we have seen scalar types, tuples, arrays, and collections. To build richer data structures Rust provides:
Structs — named, record-like types with support for methods (like objects)
Enums — tagged-union types, like OCaml datatypes
Traits — interfaces that specify behaviour, like Java interfaces (more later)
12.7.1 Structs
A struct groups named fields under a single type. By convention, struct names use CamelCase.
12.7.1.1 Definition and Construction
Rust REPL
#[allow(dead_code)] struct Rectangle { width: u32, height: u32, } fn main() { // construction let rect1 = Rectangle { width: 30, height: 50 }; // accessing fields println!("rect1's width is {}", rect1.width); } [Output] rect1's width is 30
Fields are accessed with dot notation. If the binding is mut, all fields are mutable; Rust does not allow marking individual fields as mut in the struct definition — mutability is a property of the binding, not the type.
12.7.1.2 Construction via Associated Methods
A common pattern is to provide a new associated method as a constructor:
Rust REPL
#[allow(dead_code)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn new(width: u32, height: u32) -> Rectangle { return Rectangle { width, height }; //name match } } fn main() { let rect1 = Rectangle::new(30, 50); println!("rect1's width is {}", rect1.width); } [Output] rect1's width is 30
When a field name and the local variable name match, Rust allows the shorthand Rectangle { width, height } instead of Rectangle { width: width, height: height }.
12.7.1.3 Printing Structs
Attempting to print a struct directly with "{}" fails unless the Display trait is implemented:
println!("rect1 is {}", rect1);
// error[E0277]: the trait bound `Rectangle: std::fmt::Display`
// is not satisfiedThe easiest fix is to derive the Debug trait and use the "{:?}" format specifier:
Rust REPL
#[allow(dead_code)] #[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn new(width: u32, height: u32) -> Rectangle { return Rectangle { width, height }; //name match } } fn main() { let rect1 = Rectangle::new(30, 50); println!("rect1 is {:?}", rect1); } [Output] rect1 is Rectangle { width: 30, height: 50 }
12.7.1.4 A Note on Mutability
Mutability cannot be attached to individual fields in a struct definition:
struct MutablePoint {
x: mut i32, // error: expected type, found keyword `mut`
y: mut i32,
}Instead, declare the binding as mut:
let mut point = Point { x: 0, y: 0 };
point.x = 5; // ok — the binding is mutQuiz 1: Is point immutable at HERE?
struct Point { x: i32, y: i32 }
let mut point = Point { x: 0, y: 0 };
point.x = 5;
let point = point; // shadow with immutable binding
// HERE
Quiz 1: Is point immutable at HERE?
struct Point { x: i32, y: i32 }
let mut point = Point { x: 0, y: 0 };
point.x = 5;
let point = point; // shadow with immutable binding
// HEREAnswer: True. The second let point = point creates a new immutable binding that shadows the mutable one. Mutability is a property of the binding; the struct’s contents are copied into the new binding.
12.7.2 Methods
Methods are defined in an impl block associated with a struct. The first parameter is always self, which refers to the instance. The ownership rules determine how self is passed:
&self — read-only borrowed reference (preferred for non-mutating methods)
&mut self — mutable borrowed reference (needed to modify fields)
self — takes full ownership (rare; consumes the instance)
Rust REPL
struct Rectangle { width: u32, height: u32, } impl Rectangle { fn new(width: u32, height: u32) -> Rectangle { return Rectangle { width, height }; //name match } fn area(&self) -> u32 { self.width * self.height } } fn main() { let rect1 = Rectangle::new(30, 50); println!("The area is {} pixels.", rect1.area()); } [Output] The area is 1500 pixels.
Methods are called with dot syntax: rect1.area(). If the method takes additional arguments, pass them inside the parentheses: rect1.area(3).
12.7.2.1 Multiple Arguments and Associated Methods
A method can take additional parameters beyond self. A function inside impl with no self parameter is called an associated method (similar to a static method in Java):
Rust REPL
#[allow(dead_code)] #[allow(unused_variables)] #[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } fn square(size: u32) -> Rectangle { // associated method Rectangle { width: size, height: size } } } fn main(){ let s1 = Rectangle::square(3);// called with :: syntax let s2 = Rectangle::square(3); let b = s1.can_hold(&s2); println!("{b}"); } [Output] false
Quiz 2: What is the output?
#[derive(Debug)]
struct Point { x: i32, y: i32 }
impl Point {
fn m(&mut self) {
self.x += 1;
self.y += 1;
}
}
fn main() {
let mut p = Point { x: 0, y: 0 };
p.m();
println!("{:?}", p);
}
Quiz 2: What is the output?
#[derive(Debug)]
struct Point { x: i32, y: i32 }
impl Point {
fn m(&mut self) {
self.x += 1;
self.y += 1;
}
}
fn main() {
let mut p = Point { x: 0, y: 0 };
p.m();
println!("{:?}", p);
}Answer: Point \{ x: 1, y: 1 \}. m takes &mut self, mutates both fields, and p is declared mut, so the call succeeds and the updated values are printed.
12.7.3 Generic Lifetimes in Structs
When a struct holds a reference rather than an owned value, Rust requires a lifetime annotation on the reference field. The annotation tells the compiler how long the referenced data must remain valid relative to the struct:
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Generic Lifetime");
let i = ImportantExcerpt { part: &novel };
}Here ’a is a lifetime parameter. It says: an ImportantExcerpt cannot outlive the string slice stored in part. The lifetime of i is inferred by the compiler (lifetime elision) — you rarely need to write it manually at call sites.
The lifetime parameter must also appear on the impl block:
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}The <’a> after impl is a lifetime parameter — analogous to a generic type parameter in Java. The compiler can often infer it.
12.7.4 Enums
Enums define a type with a fixed set of variants, each of which can carry data. They are equivalent to OCaml datatypes (algebraic data types).
12.7.4.1 Definition and Construction
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));The OCaml equivalent is:
type ip_addr = V4 of string | V6 of string
let home = V4 "127.0.0.1"
let loopback = V6 "::1"Each variant is namespaced under the enum with ::. Variants can carry any type — primitives, Strings, tuples, or structs.
12.7.4.2 Methods on Enums
Like structs, enums can have impl blocks:
impl IpAddr {
fn call(&self) {
// method body defined here
}
}
let m = IpAddr::V6(String::from("::1"));
m.call();Rust REPL
#![allow(dead_code)] enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn process_message(msg: Message) { match msg { Message::Quit => {println!("Quit message received");} Message::Move { x, y } => {println!("Move to coordinates: ({}, {})", x, y);} Message::Write(text) => {println!("Text message: {}", text);} Message::ChangeColor(r, g, b) => {println!("Change color to RGB({}, {}, {})", r, g, b);} } } fn main() { let m1 = Message::Write(String::from("Hello, Rust!")); let m2 = Message::Move { x: 10, y: 20 }; let m3 = Message::ChangeColor(255, 0, 0); process_message(m1); process_message(m2); process_message(m3); } [Output] Text message: Hello, Rust! Move to coordinates: (10, 20) Change color to RGB(255, 0, 0)
12.7.4.3 Enums with Struct Variants
Variant payloads can be full structs, allowing rich nested types:
struct Ipv4Addr { /* details elided */ }
struct Ipv6Addr { /* details elided */ }
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}Rust REPL
#![allow(dead_code)] struct Quit; struct Move { x: i32, y: i32, } struct Write { text: String, } struct ChangeColor { r: i32, g: i32, b: i32, } enum Message { Quit(Quit), Move(Move), Write(Write), ChangeColor(ChangeColor), } fn process_message(msg: Message) { match msg { Message::Quit(_) => { println!("Quit message received"); } Message::Move(m) => { println!("Move to ({}, {})", m.x, m.y); } Message::Write(w) => { println!("Text: {}", w.text); } Message::ChangeColor(c) => { println!("RGB({}, {}, {})", c.r, c.g, c.b); } } } fn main() { let m1 = Message::Write(Write { text: String::from("Hello"), }); let m2 = Message::Move(Move { x: 5, y: 10 }); let m3 = Message::ChangeColor(ChangeColor { r: 255, g: 0, b: 0 }); process_message(m1); process_message(m2); process_message(m3); } [Output] Text: Hello Move to (5, 10) RGB(255, 0, 0)
12.8 Traits
Traits abstract over behaviour that types can share. A trait declares a set of method signatures; any type that provides those methods is said to implement the trait.
Traits are similar to Java interfaces — they specify a contract without providing data fields.
Unlike Java, a trait can be implemented for any type, anywhere in the code — not only at the point the type is defined.
Trait bounds constrain generic type variables: T: Foo means T must implement trait Foo. This is analogous to Java’s bounded type parameters (<T extends Foo>).
Defining a Trait
A trait is declared with the trait keyword and contains method signatures. The pub keyword makes the trait visible to other modules:
pub trait Summarizable {
fn summary(&self) -> String;
}This is equivalent to the Java interface:
public interface Summarizable {
public String summary();
}Methods declared with &self are instance methods. A trait can also declare associated methods (no self), which behave like static methods in Java. Note: pub makes the entire trait public, not individual members.
Implementing a Trait on a Type
Use impl Trait for Type to provide a concrete implementation:
impl Summarizable for (i32, i32) {
fn summary(&self) -> String {
let &(x, y) = self;
format!("{}", x + y)
}
}
fn foo() {
let y = (1, 2).summary(); //"3"
let z = (1, 2, 3).summary(); //fails — not implemented
}The impl block names the trait, then for, then the type being implemented. Any type — primitive, tuple, struct, enum — can implement a trait as long as at least one of the trait or the type is defined in the current crate.
Default Implementations
A trait can supply a default method body. Types that implement the trait may override it or accept the default by providing an empty impl block ({} ):
pub trait Summarizable {
fn summary(&self) -> String {
String::from("none") // default impl
}
}
impl Summarizable for (i32, i32, i32) {} // uses default
fn foo() {
let y = (1, 2).summary(); //"3" (custom impl)
let z = (1, 2, 3).summary(); //"none" (default)
}Trait Bounds
Generic functions can require that type parameters implement particular traits. The syntax <T: Trait> is a trait bound:
pub fn notify<T: Summarizable>(item: T) {
println!("Breaking news! {}",
item.summary());
}This function accepts any item whose type T implements Summarizable. At the call site, the compiler verifies the bound. The equivalent Java generic method is:
<T extends Summarizable>
void notify(T item) {
System.out.println("Breaking news! " +
item.summary());
}Trait bounds are a form of subtyping: T may have many methods, but must at minimum implement those required by Summarizable.
Generics and Multiple Bounds
Trait implementations can themselves be generic. Here Queue<T> is implemented for Vec<T> for any T:
pub trait Queue<T> {
fn enqueue(&mut self, ele: T) -> (); //...
}
impl <T> Queue<T> for Vec<T> {
fn enqueue(&mut self, ele: T) -> () { /*...*/ } //...
}Multiple bounds are combined with +. There are two equivalent syntaxes:
fn foo<T: Clone + Summarizable>(...) -> i32 { ... }
fn foo<T>(...) -> i32 where T: Clone + Summarizable { ... }The where clause is preferred when bounds become long.
12.8.1 Standard Traits
Several traits appear throughout the standard library and have been mentioned in earlier sections:
Clone — the type has a clone() method that produces a deep copy
Copy — assignment copies the value instead of moving it (no ownership transfer); holds for primitive types and types whose fields are all Copy
Deref — the type can be dereferenced with *; the compiler calls deref() automatically
Display — the type can be converted to a human-readable string via "{}" in format macros
PartialOrd — the type supports comparison operators (<, >, etc.)
Rust REPL
use std::fmt; struct Point { x: i32, y: i32, } impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } fn main(){ let origin = Point { x: 0, y: 0 }; println!("The origin is: {}", origin); } [Output] The origin is: (0, 0)
Rust REPL
#![allow(unused_variables)] #![allow(dead_code)] trait HasArea { fn area(&self) -> f64; } struct Circle { x: f64, y: f64, radius: f64, } impl HasArea for Circle { fn area(&self) -> f64 { std::f64::consts::PI * (self.radius * self.radius) } } struct Square { x: f64, y: f64, side: f64, } impl HasArea for Square { fn area(&self) -> f64 { self.side * self.side } } // This example shows how Rust uses traits + generics to achieve // polymorphism (different types sharing the same behavior). fn print_area<T: HasArea>(shape: T) { println!("This shape has an area of {}", shape.area()); } fn main() { let c = Circle { x: 0.0f64, y: 0.0f64, radius: 1.0f64, }; let s = Square { x: 0.0f64, y: 0.0f64, side: 1.0f64, }; print_area(c); print_area(s); } [Output] This shape has an area of 3.141592653589793 This shape has an area of 1
The following function finds the largest element in a slice of any type T, provided T implements both PartialOrd (needed for >) and Copy (needed so that assigning item to largest copies rather than moves):
Rust REPL
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!("The largest number is {}", result); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest(&char_list); println!("The largest char is {}", result); } [Output] The largest number is 100 The largest char is y
Output:
The largest number is 100
The largest char is yPartialOrd is required to use >; Copy is required because largest = item inside the loop would otherwise move item out of the slice, which is not permitted.
Quiz: What is the output?
trait Trait {
fn p(&self);
}
impl Trait for u32 {
fn p(&self) { print!("1"); }
}
let x = 100; // inferred as u32
x.p();
Quiz: What is the output?
trait Trait {
fn p(&self);
}
impl Trait for u32 {
fn p(&self) { print!("1"); }
}
let x = 100; // inferred as u32
x.p();Answer: 1. x has type u32 (inferred from the literal 100), and Trait is implemented for u32, so x.p() dispatches to that implementation and prints "1". Implementing traits on primitive types is perfectly valid in Rust — unlike Java, where you cannot add methods to built-in types.
12.9 Closures and Iterators
12.9.1 Closures and Local Functions
Rust supports both local functions and closures. The key difference is that closures may capture variables from the surrounding environment, while local functions cannot:
fn moveit(l: bool, x: i32) -> i32 {
let left = |x| x - 1; // closure: may capture env
fn right(x: i32) -> i32 { x+1 } // local fn: no environment
if l { left(x) }
else { right(x) }
}The OCaml equivalent uses anonymous functions for both:
let moveit l x =
let left = fun x -> x - 1 in
let right = fun x -> x + 1 in
if l then left x
else right xClosure syntax in Rust uses vertical bars for the parameter list: |x| body.
12.9.2 Limits of Type Inference for Closures
Rust infers monomorphic types for closures — a closure is fixed to one concrete type once it is first used:
let id = |x| x;
let x = id(1); // infers id : i32 -> i32
let y = id("hi"); // fails: &str ≠i32This contrasts with OCaml, which infers polymorphic types:
let f = fun x -> x (* 'a -> 'a *)
let x = f 1 (* OK *)
let y = f "hi" (* OK *)Because Rust closures are monomorphic, a closure cannot be reused at different types. Use a generic function with a trait bound (see below) to achieve polymorphic behaviour.
The following complete program shows both ideas together. moveit uses local fn definitions (not closures) for left and right; the commented line shows the closure alternative. Then main demonstrates monomorphism: after id(1) fixes the type to i32, calling id("hi") fails at compile time:
fn moveit(l: bool, x: i32) -> i32 {
// let left = |x| x - 1; // closure alternative
fn left(x: i32) -> i32 { x - 1 }
fn right(x: i32) -> i32 { x + 1 }
if l { left(x) }
else { right(x) }
}
fn main() {
let t = moveit(false, 100);
println!("{}", t); // 101
let id = |x| x;
let x = id(1); // infers id: i32 -> i32
let y = id("hi"); // fails: &str ≠i32
}12.9.3 The Iterator Trait
Iteration in Rust is built on the Iterator trait. Calling .iter() on a collection returns a value that implements Iterator:
let a = vec![10, 20, 30, 40, 50];
for e in a.iter() {
println!("the value is: {}", e);
}The trait is defined as:
trait Iterator {
type Item; // associated type
fn next(&mut self) -> Option<Self::Item>;
// ... default method implementations
}type Item is an associated type — a type placeholder that each implementation fills in concretely (e.g., type Item = &i32 for a Vec<i32> iterator).
12.9.4 Unpacking for
A for loop is syntactic sugar for repeatedly calling next on a mut iterator until it returns None:
let a = vec![10, 20];
let mut iter = a.iter();
assert_eq!(iter.next(), Some(&10));
assert_eq!(iter.next(), Some(&20));
assert_eq!(iter.next(), None);Each call to next advances the iterator, so the iterator itself must be mut. a.iter() yields immutable references to the elements. Two alternatives exist:
a.into_iter() — yields owned values (moves elements out of a)
a.iter_mut() — yields mutable references
12.9.5 Iterator Adaptors
Iterator adaptors transform one iterator into another. They are lazy: no work is done until the iterator is consumed (e.g., by collect or a for loop).
i.map(f) — applies f to each element, producing a new iterator
i.filter(f) — keeps only elements e where f(e) == true
i.collect() — consumes the iterator into a Vec (or other collection)
i.fold(a, f) — like OCaml’s fold_left: reduces the iterator with accumulator a and function f
i.rfold(a, f) — like OCaml’s fold_right: reduces the iterator with accumulator a and function f
zip, sum, take, skip, and many more
12.9.5.1 Examples
let a = vec![10, 20];
let i = a.iter();
let j: Vec<i32> = i.map(|x| x+1).collect(); //[11, 21]
let k = a.iter().fold(0, |a, x| x - a); //10
for e in a.iter().filter(|&&x| x == 10) {
println!("{}", e); //prints 10
}Here is a more complete example combining fold, filter, and map on the same vector. fold accumulates a running sum; filter returns a new iterator of only the even elements; map scales each element by 10 and the result is consumed by a for loop:
fn main(){
let v = vec![1,2,3,4,5];
let s = v.iter().fold(0, |acc, num| acc + num);
println!("Sum= {s}");
//filter returns a iterator
let v2 = v.iter().filter(|n| (*n) % 2 == 0);
print!("Filter: ");
for i in v2{print!("{i},"); }
print!("\nMap:");
let v3 = v.iter().map(|n| n * 10);
for i in v3{print!("{i},"); }
println!();
}Sum= 15 |
Filter: 2,4, |
Map:10,20,30,40,50, |
Quiz 1: What is the output?
fn main() {
let a = [0, 1, 2, 3, 4, 5];
let mut iter2 = a.iter().map(|x| 2 * x);
iter2.next();
let t2 = iter2.next();
println!("{:?}", t2)
}
Quiz 1: What is the output?
fn main() {
let a = [0, 1, 2, 3, 4, 5];
let mut iter2 = a.iter().map(|x| 2 * x);
iter2.next();
let t2 = iter2.next();
println!("{:?}", t2)
}Answer: Some(2). The iterator produces 0, 2, 4, 6, 8, 10 (each element doubled). The first next() consumes Some(0); the second call returns Some(2).
12.9.5.2 Lazy Evaluation in Practice
Because iterator adaptors are lazy, the mapping function is never called until a consuming operation (like collect) triggers evaluation. Furthermore, combining an adaptor with take means only the requested elements are ever computed — the rest are never touched:
fn main() {
let v = vec![1, 2, 3, 4, 5];
let iter = v.iter().map(|x| {
println!("mapping {}", x);
x * 2
});
println!("Iterator created, still nothing executed...");
let result: Vec<_> = iter.take(2).collect();
println!("Final result: {:?}", result);
}Iterator created, still nothing executed... |
mapping 1 |
mapping 2 |
Final result: [2, 4] |
Only elements 1 and 2 are mapped — take(2) stops the iterator after two elements, so the remaining three values are never processed.
12.9.6 Custom Iterators
You can implement Iterator on your own types by providing type Item and fn next. Here is a Fibonacci iterator that yields values below 1000:
Rust REPL
struct Fibonacci { curr: u32, next: u32 } impl Iterator for Fibonacci { type Item = u32; fn next(&mut self) -> Option<Self::Item> { let new_next = self.curr + self.next; self.curr = self.next; self.next = new_next; if self.curr < 1000 { Some(self.curr) } else { None } } } fn fibonacci() -> Fibonacci { Fibonacci { curr: 0, next: 1 } } fn main() { println!("The first 15 terms of the Fibonacci seq:"); for i in fibonacci().take(15) { print!("{}, ", i); } println!("\nFrom 5th, the next 3 terms of the Fibonacci seq:"); for i in fibonacci().skip(4).take(3) { print!("{}, ", i); } println!() } [Output] The first 15 terms of the Fibonacci seq: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, From 5th, the next 3 terms of the Fibonacci seq: 5, 8, 13,
Iterator adaptors like take and skip work on any custom iterator because they are default methods provided by the Iterator trait.
12.9.7 Iterator Performance
Iterators in Rust perform at least as well as explicit index-based for loops. The compiler aggressively optimises iterator chains — for example, by unrolling the loop at compile time. Prefer map, fold, zip, etc. over manual loops.
12.9.8 Closures as Arguments
Each closure has a distinct, anonymous type. Even two closures with identical signatures have different types (these are called generative types). To accept a closure as a function parameter, use a generic type variable with a function trait bound:
Fn(A) -> B — borrows captured variables immutably (or copies them); can be called any number of times
FnMut(A) -> B — borrows captured variables mutably; can be called multiple times
FnOnce(A) -> B — moves (or copies) captured variables; can only be called once
Named functions defined with fn also implement all three traits. You cannot write a closure type directly as a parameter type annotation — you must use a generic with a trait bound:
Rust REPL
// correct: generic with trait bound fn app_int<T>(f: T, x: i32) -> i32 where T: Fn(i32) -> i32 { f(x) } fn main() { println!("{}", app_int(|x| x-1, 1)); } // incorrect: cannot write closure type directly // fn app_int(f: (i32) -> i32, x: i32) -> i32 { f(x) } [Output] 0
Here is a minimal complete program using the same pattern — app_int accepts any Fn(i32) -> i32 and applies it to an argument. The closure |x| x*2 doubles the value 10:
fn app_int<T>(f: T, x: i32) -> i32
where T: Fn(i32) -> i32
{
f(x)
}
fn main() {
println!("{}", app_int(|x| x * 2, 10)); // 20
}Function trait bounds can also appear in struct and enum definitions.
12.9.8.1 Polymorphic Higher-Order Functions
Using multiple type parameters gives fully polymorphic higher-order functions:
Rust REPL
fn app<T, U, W>(f: T, x: U) -> W where T: Fn(U) -> W { f(x) } fn main() { println!("{}", app(|x| x-1, 1)); //i32 let s = String::from("hi "); println!("{}", app(|x| x+"there", s)); //String } [Output] 0 hi there
12.9.9 Capturing Free Variables
A closure captures variables from its enclosing scope. A local function defined with fn cannot do this — it has no environment:
fn main() {
let x = 4;
let equal_to_x = |z| z == x; // captures x
let y = 4;
assert!(equal_to_x(y)) // true
}A local function defined with fn inside another function cannot capture the enclosing environment — only a closure can. The following example contrasts a commented-out local function (which would be rejected by the compiler) with a mutable closure that borrows s and appends to it on each call:
#![allow(dead_code)]
#![allow(unused_variables)]
fn main() {
let mut s = String::from("hello");
// fn foo1(x: &str) -> String { // error: cannot capture s
// let v = s;
// v + x
// }
let mut foo2 = |x: &str| {
let v = &mut s; // capture mutable reference
v.push_str("world!");
v.clone() + x
};
let v1 = foo2("123");
println!("{}", s);
foo2("abc");
println!("{}", v1);
}Because foo2 borrows s mutably on each call it can be called multiple times, making it FnMut.
The FnX trait used for a closure depends on how it handles captured owned values:
FnOnce — moves (or copies) captured variables into the closure. The closure can only be called once because the call consumes the captured values.
FnMut — borrows captured variables mutably. The closure can be called multiple times.
Fn — borrows captured variables immutably, or copies them (if Copy). Can be called any number of times. Try this bound first; follow the compiler’s advice if it does not work.
12.9.9.1 Example: Fn
A closure that only borrows captured variables immutably implements Fn and can be called any number of times without restrictions. Here next borrows outer as &String, so outer is still accessible after both calls:
fn main() {
let outer = String::from("Hello");
let next = || {
let m = &outer; // immutable borrow
println!("{}", m);
};
next();
println!("{}", outer); // still accessible — only borrowed
next();
}12.9.9.2 Example: FnOnce
When a closure captures an owned, non-Copy value, it becomes FnOnce:
let x = String::from("hi");
let add_x = |z| x + z; //captures x; is FnOnce
println!("x = {}", x); //fails — x moved into closure
let s = add_x(" there"); //consumes closure
let t = add_x(" joe"); //fails — add_x already consumedAfter add_x is created, x has been moved into it. Calling add_x once more would require the already-consumed x, so the second call fails. println!("{}", x) on the preceding line is also an error because x was moved into the closure.
12.9.9.3 Example: FnMut
A closure that borrows captured variables mutably implements FnMut and can be called multiple times as long as each call has exclusive access. To pass it to a higher-order function, the parameter must be declared mut and the argument must be passed as &mut. Here double mutably borrows s (appending " world" each call) while doubling its integer argument:
fn apply<T>(mut f: T, x: i32) -> i32
where T: FnMut(i32) -> i32 {
f(x)
}
fn main() {
let mut s = String::from("Hello");
let mut double = |x: i32| {
s.push_str(" world");
let m = &mut s;
2 * x
};
let m = apply(&mut double, 10);
println!("{}", m); // 20
let m2 = apply(&mut double, 10);
println!("{}", m2); // 20
println!("{}", s); // Hello world world
}Because double only borrows s mutably (never moves it) it remains valid after both calls, and s shows two appended " world" suffixes.
12.10 Reference Counting and Interior Mutability
12.10.1 Motivation: Relaxing Rust’s Restrictions
Rust’s ownership system is strict by design:
Every value has exactly one owner; when the owner goes out of scope, the value is dropped.
Mutation can occur only through mutable variables or mutable references.
Only one mutable reference (and no immutable ones) may exist at a time.
These rules are enforced statically at compile time, but architecturally it can be awkward to designate one owner through which all accesses must pass — we might need shared mutable access. Rust provides APIs to relax these restrictions:
Rc<T> — reference counting for shared ownership (multiple owners, dynamically managed lifetimes).
Cell<T> / RefCell<T> — interior mutability: track borrows at run time instead of compile time.
The trade-off: extra space and time overhead, and some compile-time errors become run-time panics.
12.10.2 Rc<T>: Shared Ownership via Reference Counting
12.10.2.1 The Problem
Box<T> gives a single owner. Consider trying to share a list between two bindings:
enum List { Nil, Cons(i32, Box<List>) }
use List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a)); // ERROR: a was moved into b
}Box::new(a) moves a, so the second use fails. We need a way to share a without transferring ownership.
12.10.2.2 Rc<T> Basics
Rc<T> (reference-counted pointer) associates a counter with the heap allocation. Multiple Rc handles to the same data can coexist:
Rc::new(val) — allocates val on the heap and creates a handle with count 1.
Rc::clone(&r) — copies the pointer (not the data) and increments the count. By convention, prefer Rc::clone(&r) over r.clone() as a visual marker that this is a cheap clone (not a deep copy).
Moving an Rc (let t = s;) does not increment the count; s is no longer valid.
When an Rc handle goes out of scope, drop is called automatically, decrementing the count. When the count reaches zero, the heap allocation is freed.
use std::rc::Rc;
let x = Rc::new(42); // refcount = 1
let y = Rc::clone(&x); // refcount = 2
let z = Rc::clone(&x); // refcount = 3
// all three point to the same heap valueRc::strong_count(&r) returns the current reference count.
12.10.2.3 Lists with Sharing
Rust REPL
#![allow(unused_variables)] #![allow(dead_code)] use std::rc::Rc; enum List { Nil, Cons(i32, Rc<List>) } use List::{Cons, Nil}; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); let b = Cons(3, Rc::clone(&a)); // a's count -> 2 let c = Cons(4, Rc::clone(&a)); // a's count -> 3 // Both b and c share the tail; no copy of the data } [Output]
12.10.2.4 Risks: Reference Cycles
Rc cannot handle cyclic data structures. If two Rc values point to each other, their reference counts will never reach zero even after all external references are dropped — causing a memory leak. The standard library provides Weak<T> (weak references) to break cycles: a weak reference does not increment the strong count and the referent may be revoked.
12.10.2.5 Rc and Mutation
Rc<T> only allows immutable access to its contents. It does not implement DerefMut, so writing through an Rc is a compile error:
let mut b = Rc::new(42);
*b = 43; // ERROR: cannot assign to data in an Rc
// DerefMut is not implemented for Rc<i32>mut b means b itself can be rebound (e.g., b = Rc::new(43)), but you cannot mutate the heap value through the pointer. To combine sharing with mutation, you need interior mutability.
12.10.3 Interior Mutability: Cell<T> and RefCell<T>
Interior mutability is a design pattern that lets you mutate data even through a shared (&) reference. It shifts borrow checking from compile time to run time.
12.10.3.1 Cell<T>: Copy-Based Interior Mutability
Cell<T> wraps a value and allows mutation through &self (no mut required on the cell):
set(&self, val: T) — moves val into the cell.
get(&self) -> T — copies the value out (requires T: Copy).
take(&self) -> T — moves the value out, leaving Default::default() behind.
get_mut(&mut self) -> &mut T — gives a mutable reference, but requires &mut self.
Cell is appropriate when T is Copy and you can reason statically about lifetimes. If T is not Copy (e.g., a String or a struct), or if you need to hold a reference into the cell without copying, use RefCell.
12.10.3.2 Example: Cell<T> with a Copy integer
Here color is a Cell<u32> field inside an otherwise immutable struct. Because u32 is Copy, set can replace the value through any shared reference — no mut binding is needed on p1 or p2:
use std::cell::Cell;
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
color: Cell<u32>,
}
fn main() {
let p1 = Point { x: 10, y: 20, color: Cell::new(88) };
// ERROR: p1 is immutable — plain fields cannot be changed
// p1.x = 99;
println!("{:?}", p1); // Point { x: 10, y: 20, color: Cell { value: 88 } }
// WORKS: Cell allows mutation through a shared reference
let p2 = &p1;
p2.color.set(99);
println!("{:?}", p1); // Point { x: 10, y: 20, color: Cell { value: 99 } }
}12.10.3.3 Example: Cell<T> with a lifetime and get
References are Copy, so Cell<\&’a str> also works. This example adds a lifetime parameter ’a to the struct and shows get (which copies the reference out) alongside set:
#![allow(dead_code)]
#![allow(unused_variables)]
use std::cell::Cell;
#[derive(Debug)]
struct Point<'a> {
x: i32,
y: i32,
color: Cell<&'a str>,
}
fn main() {
let p1 = Point { x: 10, y: 20, color: Cell::new("red") };
// ERROR: p1 is immutable — plain fields cannot be changed
// p1.x = 99;
println!("{:?}", p1); // Point { x: 10, y: 20, color: Cell { value: "red" } }
// get() copies the &str out; set() replaces it — no mut needed
let t = p1.color.get(); // t == "red"
p1.color.set("blue");
println!("{:?}", p1); // Point { x: 10, y: 20, color: Cell { value: "blue" } }
}12.10.3.4 RefCell<T>: Dynamic Borrow Checking
RefCell<T> enforces the borrow rules at run time rather than compile time.
use std::cell::RefCell;
pub fn new(value: T) -> RefCell<T>
pub fn borrow(&self) -> Ref<'_, T> // dynamic immutable borrow
pub fn borrow_mut(&self) -> RefMut<'_, T> // dynamic mutable borrowKey points:
borrow and borrow_mut both take &self (not &mut self), so they work through a shared reference — that is the interior mutability.
Multiple Ref (immutable) borrows can coexist.
Calling borrow_mut while any Ref or RefMut is outstanding causes a panic at run time.
Ref<’_, T> and RefMut<’_, T> are guard types used only with RefCell.
12.10.3.5 How Dynamic Borrowing Works
Each RefCell maintains an internal borrow count. borrow and borrow_mut increment it; when a Ref or RefMut goes out of scope, drop decrements it. If the invariant (at most one mutable borrow, or any number of immutable borrows) would be violated, the call panics immediately:
use std::cell::RefCell;
let c = RefCell::new(5); // borrow count = 0
let m = c.borrow(); // borrow count = 1 (immutable)
let b = c.borrow_mut(); // PANIC: already immutably borrowedStatic vs.\ dynamic borrow tracking:
&T and &mut T — compile-time, zero runtime overhead, catches errors earlier.
RefCell::borrow* — run-time, small overhead, allows patterns the compiler cannot verify.
Prefer static borrow checking wherever possible; reach for RefCell only when you need the compiler to trust you (and you trust yourself not to panic at runtime).
12.10.4 Combining Rc and RefCell: Shared Mutable Data
The idiomatic way to have shared, mutable data in single-threaded Rust is Rc<RefCell<T>>:
Rc provides shared ownership — multiple handles, lifetime managed by reference count.
RefCell provides interior mutability — mutation through &self, borrow-checked at runtime.
Rc<RefCell<T>> carries two counts:
Reference count (Rc) — how many Rc handles exist; determines when to free.
Borrow count (RefCell) — how many outstanding Ref/RefMut guards exist; enforces the borrow rules at runtime.
use std::rc::Rc;
use std::cell::RefCell;
let r1 = Rc::new(RefCell::new(42));
let r2 = Rc::clone(&r1); // both r1 and r2 share ownership
{
let mut m = r1.borrow_mut(); // RefMut guard; borrow count -> 1
*m = 100;
} // guard drops; borrow count -> 0
println!("{}", r2.borrow()); // prints 100 — mutation visible through r2Quiz: What is the result?
let r1 = Rc::new(RefCell::new(42));
let r2 = r1.clone();
let m = (*r1).borrow_mut();
*m = 43;
println!("{:?}", *r2.borrow());
Quiz: What is the result?
let r1 = Rc::new(RefCell::new(42));
let r2 = r1.clone();
let m = (*r1).borrow_mut();
*m = 43;
println!("{:?}", *r2.borrow());Answer: D. Compiler error. borrow_mut() returns a RefMut<T>, which implements DerefMut. DerefMut::deref_mut takes &mut self, so to write *m = 43 the variable m must itself be declared mut. Without let mut m, the compiler rejects the assignment with: cannot borrow m as mutable, as it is not declared as mutable. The corrected code uses let mut m and drops the borrow before r2.borrow():
12.11 Box Smart Pointers and Trait Objects
12.11.1 The Box<T> Smart Pointer
A smart pointer is a data structure that acts like a pointer but carries additional metadata and capabilities. Box<T> is the simplest smart pointer in Rust: it allocates a value of type T on the heap and stores a pointer to it on the stack.
let x: Box<i32> = Box::new(5);The pointer lives on the stack; the i32 value lives on the heap.
When the Box goes out of scope, both the pointer and the heap allocation are freed automatically.
Box<T> implements the Deref trait (so *x works) and the Drop trait (so cleanup is automatic).
12.11.1.1 When to Use Box<T>
Avoid copying large data — move ownership via a small pointer instead of copying the full value.
Recursive types — a type whose size cannot be known at compile time because it contains itself (e.g., a linked list) can be made finite by boxing the recursive field.
Trait objects — store a value of a type that is only known at runtime (covered below).
12.11.2 Recursive Types with Box
Consider a simple singly-linked list defined as an enum:
enum List {
Cons(i32, List), // ERROR: recursive type has infinite size
Nil,
}This does not compile because Rust needs to know the size of List at compile time, and a List contains a List which contains a List… with no bound. The fix is to box the recursive field:
enum List {
Cons(i32, Box<List>), // OK: Box<List> is pointer-sized
Nil,
}
fn main() {
let list = List::Cons(1,
Box::new(List::Cons(2,
Box::new(List::Nil))));
}Because Box<List> is always exactly one pointer in size, the compiler can determine the total size of List.
12.11.3 The Deref Trait
The Deref trait allows a type to be dereferenced with the * operator, just like a raw reference.
For a reference &x, dereferencing gives back the original value: *(& x) == x. Box<T> implements Deref the same way: *b (where b: Box<T>) desugars to *(b.deref()).
Deref::deref returns &T rather than T to preserve ownership — returning T would move the value out of the box.
12.11.3.1 Deref Coercion
When a type implements Deref<Target = U>, Rust automatically inserts deref calls where a &U is expected but a &T is provided. These coercions can be chained:
fn hello(x: &str) { println!("Hello, {}!", x); }
fn main() {
let s = Box::new(String::from("world"));
hello(&s); // &Box<String> -> &String -> &str (two deref coercions)
}The compiler silently inserts * calls until the types line up. Without deref coercion you would have to write hello(&(*(*s))[..]).
12.11.4 The Drop Trait
The Drop trait lets you run custom code when a value goes out of scope — the Rust equivalent of a destructor. You implement a single method:
trait Drop {
fn drop(&mut self);
}Rust calls drop automatically when the value’s owner goes out of scope. You cannot call drop manually (the compiler forbids it to prevent double-free bugs). If you need to free a resource early, use std::mem::drop(value), which moves ownership into a no-op function so the destructor runs immediately.
12.11.5 Trait Objects and Dynamic Dispatch
12.11.5.1 The Problem: Unknown Size
Suppose you have a trait Summarizable and want to write a function that accepts any implementation of it:
// ERROR: Summarizable has unknown size
fn print_summary(s: Summarizable) { ... }
// dyn is required
fn print_summary(s: &Summarizable) { ... }Without knowing the concrete type at compile time, the compiler cannot determine the size of s or how to call its methods.
12.11.5.2 Static Dispatch (Monomorphisation)
Generics with trait bounds use static dispatch: the compiler generates a separate copy of the function for each concrete type at call sites. This requires the concrete type to be known at compile time:
fn print_summary<T: Summarizable>(s: T) { ... }
// compiles only when the concrete type of s is known at compile time12.11.5.3 Dynamic Dispatch with Trait Objects
Java solves this problem with dynamic dispatch: every object carries a pointer to a virtual method table (vtable), and the correct method is looked up at runtime. Rust provides the same mechanism through trait objects written as dyn Trait.
A trait object is a fat pointer — it contains:
a pointer to the concrete data, and
a pointer to the vtable for that concrete type’s trait implementation.
Because the fat pointer has a fixed, known size (two pointers = 16 bytes on a 64-bit system), it can be passed by value. The idiomatic way to pass a trait object by value in Rust is Box<dyn Trait>:
fn print_summary(s: Box<dyn Summarizable>) {
println!("{}", s.summarize());
}
fn main() {
let article = Article { ... };
let tweet = Tweet { ... };
print_summary(Box::new(article));
print_summary(Box::new(tweet));
}The caller wraps the concrete value in Box::new; print_summary sees only a Box<dyn Summarizable> and calls the right summarize at runtime.
12.11.5.4 Why Box and Not dyn Summarizable Alone?
dyn Summarizable by itself is an unsized type — the compiler cannot determine its size. Placing it behind Box gives it a fixed pointer-sized representation on the stack while the actual data lives on the heap.
12.11.5.5 Comparison: Static vs.\ Dynamic Dispatch
Generics (<T: Trait>) — static dispatch, zero runtime overhead, but each concrete type generates separate machine code (monomorphisation).
Trait objects (Box<dyn Trait>) — dynamic dispatch, slight runtime overhead for vtable lookup, but a single copy of the function works for every concrete type.
Use generics when performance is critical and the concrete types are fixed at compile time. Use trait objects when you need to mix different concrete types at runtime (e.g., a heterogeneous collection of shapes).
12.11.6 Smart Pointers Summary
Several familiar types in Rust are actually smart pointers:
String — owns heap-allocated UTF-8 bytes; implements Deref<Target = str> and Drop.
Vec<T> — owns a heap-allocated array; implements Deref<Target = [T]> and Drop.
Box<T> — owns a single heap-allocated value; implements Deref<Target = T> and Drop.
Additional smart pointers in the standard library:
Rc<T> — reference-counted shared ownership (single-threaded).
Cell<T> / RefCell<T> — interior mutability, allowing mutation through a shared reference.