Lecture 16 - Borrowing and References
Logistics
- Last time: Ownership system and Vec for heap data
- Today: Borrowing and references (&, *)
- HW4 will be released today
- There's pre-work for Tuesday but not Wednesday
- No sign-ups yet for coffee today (there really is coffee)
Learning Objectives
By the end of today, you should be able to:
- Use the & operator to create references (borrowing)
- Use the * operator to dereference and access borrowed data
- Apply borrowing rules to avoid "ownership too strict" problems
- Debug common ownership and borrowing compiler errors
Part 1 - Wrapping up Ownership
Let's practice fixing common ownership errors you'll encounter:
Error 1: Use After Move
This code won't compile:
fn main() { let data = vec![1, 2, 3]; process_data(data); println!("{:?}", data); // ERROR! } fn process_data(vec: Vec<i32>) { println!("Processing: {:?}", vec); }
Compiler error: "borrow of moved value: data"
Error 1: Use After Move
Compiler error: "borrow of moved value: data"
Fix option 1: Return the data from the function
fn main() { let data = vec![1, 2, 3]; let data = process_data(data); // Get it back! println!("{:?}", data); // OK! } fn process_data(vec: Vec<i32>) -> Vec<i32> { println!("Processing: {:?}", vec); vec // Return ownership }
Error 1: Use After Move
Compiler error: "borrow of moved value: data"
Fix option 2: Clone the data (makes a copy)
fn main() { let data = vec![1, 2, 3]; process_data(data.clone()); // Send a copy println!("{:?}", data); // OK! } fn process_data(vec: Vec<i32>) { println!("Processing: {:?}", vec); }
Error 2: Multiple Moves
This code won't compile:
fn main() { let message = String::from("Hello"); let a = message; let b = message; // ERROR! Can't move twice println!("{} {}", a, b); }
Compiler error: "use of moved value: message"
Error 2: Multiple Moves
Compiler error: "use of moved value: message"
Fix: Clone for multiple copies
fn main() { let message = String::from("Hello"); let a = message.clone(); // Make a copy let b = message; // Move original println!("{} {}", a, b); // Both work! }
Error 3: Trying to Copy Non-Copy Types
This code won't compile:
fn main() { let names = vec![String::from("Alice"), String::from("Bob")]; let first = names[0]; // ERROR! Can't copy String println!("{}", first); }
Compiler error: "cannot move out of index of Vec<String>"
Error 3: Trying to Copy Non-Copy Types
Compiler error: "cannot move out of index of Vec<String>"
Fix: Clone the specific element
fn main() { let names = vec![String::from("Alice"), String::from("Bob")]; let first = names[0].clone(); // Clone just this element println!("{}", first); // Works! println!("{:?}", names); // Original Vec still works! }
What's really going on with Copy and Clone (TC 12:25)
Sometimes you actually want duplicates of your data. Rust provides two mechanisms: Copy and Clone.
- Copy: Simple bitwise copy of stack bytes. (typically of data, sometimes of pointers)
- Clone: Explicit duplication that can do whatever the type needs
- might duplicate heap data (like String or Vec)
- might just copy stack values (like i32)
- might run a custom cloning function on your custom types
ROUGHLY BUT NOT EXACTLY:
- Copy duplicates values on the stack
- Clone duplicates values on the heap
Clone: Explicit Deep Copying
Use .clone() to make an explicit, complete copy of data:
fn main() { let mut vec1 = vec![1, 2, 3]; let mut vec2 = vec1.clone(); // Explicit copy of ALL the data // Proof they're separate - modify one vec1.push(4); // Both work! They're completely separate println!("vec1: {:?}", vec1); println!("vec2: {:?}", vec2); }
What .clone() does for Vec:
Before clone: After clone and push:
Stack: Heap: Stack: Heap:
┌─────────────┐ ┌─────────────┐
│ vec1: Vec │ ┌───────────┐ │ vec2: Vec │ ┌───────────┐
│ ├ ptr ──────┼──▶│ 1 │ 2 │ 3 │ │ ├ ptr ──────┼───▶│ 1 │ 2 │ 3 │
│ ├ len: 3 │ └───────────┘ │ ├ len: 3 │ └───────────┘
│ └ cap: 3 │ │ └ cap: 3 │
└─────────────┘ ├─────────────┤
│ vec1: Vec │ ┌───────────────┐
│ ├ ptr ──────┼───▶│ 1 │ 2 │ 3 │ 4 |
│ ├ len: 4 │ └───────────────┘
│ └ cap: 4 │
└─────────────┘
Adding Clone to Your Types
Use #[derive(Clone)] to make your custom types cloneable:
#[derive(Clone, Debug)] enum Temperature { Fahrenheit(f64), Celsius(f64), } fn main() { let temp1 = Temperature::Celsius(12.0); let temp2 = temp1.clone(); // Now this works! println!("temp1: {:?}", temp1); println!("temp2: {:?}", temp2); }
Copy: Automatic, Cheap Duplication
Some types are so simple that Rust can copy them automatically without .clone():
fn main() { // These types implement Copy - no explicit .clone() needed let a = 42; // i32 let b = a; // Automatic copy println!("{} {}", a, b); // Both work let e = true; // bool let f = e; // Automatic copy println!("{} {}", e, f); // Both work let g = (1, 2); // (i32, i32) - tuples of Copy types are Copy let h = g; // Automatic copy println!("{:?} {:?}", g, h); // Both work }
Adding Copy to Your Types
Use #[derive(Copy, Clone)] for simple types (note: Copy requires Clone):
#[derive(Copy, Clone, Debug)] enum Temperature { Fahrenheit(f64), Celsius(f64), } fn main() { let temp1 = Temperature::Celsius(12.0); let temp2 = temp1; // Now automatically copies! println!("temp1: {:?}", temp1); println!("temp2: {:?}", temp2); }
YOU CAN ONLY DO THIS IF the types inside the enum (or other structures) all have Copy as well
YOU CAN'T DO THIS IF YOUR ENUM CONTAINS STRINGS
This kind of sucks though right?
- Functions steal ownership even when they just want to read data
- You have to pass data back and forth like a hot potato
- You have to clone the contents of a vec/array when you just want to view it
- You wind up with code like this:
fn analyze_data(data: Vec<i32>, usernames: Vec<String>, big_array: Box<[i32]>) -> (Vec<i32>, Vec<String>, Box<[i32]>) { println!("Processing {} items", data.len()); // ... do some data cleaning ... (data, usernames, big_array) // Have to return it back! } fn main() { let my_data = vec![1, 2, 3, 4, 5]; let my_usernames = vec!["Alice", "Bob", "Charlie"]; let my_box = Box::new([0; 10_000_000]); let (my_data, my_usernames, my_box) = analyze_data(my_data, my_usernames, my_box); // Awkward! }
Rust's solution: Borrowing - let functions temporarily use data without taking ownership!
Part 2 - What is Borrowing? (TC 12:30)
Borrowing means temporarily accessing data without taking ownership of it.
Think of it like borrowing a book from a friend:
- Your friend still owns the book (original owner keeps ownership)
- You can read it while you have it (temporary access)
- You give it back when done (reference goes out of scope)
- Your friend can still use it after you return it (original data still accessible)
Creating References with &
The & operator creates a reference (pointer) to data without taking ownership:
fn main() { let data = vec![10, 20, 30]; // Create a reference to data (borrowing) let data_ref = &data; // Both work! No ownership was moved println!("Original: {:?}", data); // data still valid println!("Reference: {:?}", data_ref); // reference works too }
Stack/heap diagram:
Stack: Heap:
┌─────────────────┐
│ data_ref: &Vec │────┐ points to
├─────────────────┤ │ the stack!
│ data: Vec<i32> │◄───┘ ┌────┬────┬────┐
│ ├ ptr ──────────┼───────────────▶ │ 10 │ 20 │ 30 │
│ ├ len: 3 │ └────┴────┴────┘
│ └ cap: 3 │
└─────────────────┘
References in Functions
This is where borrowing shines:
fn main() { let my_data = vec![1, 2, 3, 4, 5]; analyze_data(&my_data); // Pass a reference let total = calculate_sum(&my_data); // Still works! println!("Original data: {:?}", my_data); // Still have it! println!("Sum: {}", total); } fn analyze_data(data: &Vec<i32>) { // Takes a reference! println!("Processing {} items", data.len()); println!("First item: {}", data[0]); // No need to return anything! } fn calculate_sum(data: &Vec<i32>) -> i32 { // Also takes a reference! data.iter().sum() }
No more ownership juggling! Each function borrows the data, uses it, and gives it back automatically.
Let's see it with a String
The & creates a new value on the stack that points to existing data:
fn main() { let name = String::from("Alice"); let name_ref = &name; // name_ref is a new stack value pointing to name println!("Name: {}", name); // Direct access println!("Reference: {}", name_ref); // Access through reference }
Stack diagram:
Stack: Heap:
┌─────────────────┐
│name_ref: &String│────┐ points to
├─────────────────┤ │ the stack!
│ name: String │◄───┘ ┌─────┬─────┬─────┬─────┬─────┐
│ ├ ptr ──────────┼───────────────▶ │ 'A' │ 'l' │ 'i' │ 'c' │ 'e' │
│ ├ len: 5 │ └─────┴─────┴─────┴─────┴─────┘
│ └ cap: 5 │
└─────────────────┘
String types: String vs &str vs &String (TC 12:35)
Lecture 17 will be the end of this craziness I promise!
You've seen different string types - let's clarify:
String: Owned, growable string on the heap (like we've been using)&str: A "string slice" - a reference directly to string data (points to heap or static memory)&String: A reference to a String (points to the String's stack metadata)
fn main() { let owned: String = String::from("Hello"); let string_ref: &String = &owned; // Reference to the String let str_slice: &str = &owned; // Slice of the string data let literal: &str = "Hello"; // String literal (also &str) println!("{}", owned); println!("{}", string_ref); println!("{}", str_slice); println!("{}", literal); }
Let's draw it out! (with cold calls)
In practice: Functions usually take &str as parameters, but you can pass &String and Rust will coerce it to &str automatically!
When to borrow and when to copy/clone?
Use borrowing (&) when:
- You just need to read/use the data temporarily and don't want to pass ownership around
- The data is large/expensive to copy
Use copying/cloning when:
- You're passing data to functions that need to own it
- You need to modify a copy without affecting the original
Part 3 - References and Dereferencing (& and *) (TC 12:40)
The & Operator (Creating References)
& creates a reference to existing data:
fn main() { let x = 42; let x_ref = &x; // Create reference to x let scores = vec![85, 92, 78]; let scores_ref = &scores; // Create reference to vec let name = String::from("Bob"); let name_ref = &name; // Create reference to string println!("x: {}, x_ref: {}", x, x_ref); println!("scores: {:?}, scores_ref: {:?}", scores, scores_ref); println!("name: {}, name_ref: {}", name, name_ref); }
Memory layout:
Stack: Heap:
┌─────────────────┐
│ name_ref: &String──┐
├─────────────────┤ │
│ name: String │◄─┘ ┌─────┬─────┬─────┐
│ ├ ptr ──────────┼────────────────▶│ 'B' │ 'o' │ 'b' │
│ ├ len: 3 │ └─────┴─────┴─────┘
│ └ cap: 3 │
├─────────────────┤
│ scores_ref: &Vec┼──┐
├─────────────────┤ │
│ scores: Vec │◄─┘ ┌────┬────┬────┐
│ ├ ptr ──────────┼─────────────────▶│ 85 │ 92 │ 78 │
│ ├ len: 3 │ └────┴────┴────┘
│ └ cap: 3 │
├─────────────────┤
│ x_ref: &i32 ────┼──┐
├─────────────────┤ │
│ x: 42 │◄─┘
└─────────────────┘
We can go on and on...
fn main() { let x = 42; let x_ref = &x; // Create reference to x let x_ref_2 = &x; // Create another reference to x let x_ref_ref = &x_ref; // Create another reference to x_ref println!("x_ref_2: {}, x_ref_ref: {}", x_ref_2, x_ref_ref); // thanks to the macro! }
Memory layout:
Stack: Heap:
┌─────────────────┐
│ x_ref_ref:&&i32 ┼────────────┐
├─────────────────┤ │
│ x_ref_2: &i32 ──┼────┐ │
├─────────────────┤ │ │
│ x_ref: &i32 ────┼──┐ │ ◄─────┘
├─────────────────┤ │ │
│ x: 42 │◄─┘─┘
└─────────────────┘
The * Operator (Dereferencing)
* is the inverse-operation to & and extracts data:
fn main() { let x = 42; let y = 10; let x_ref = &x; let y_ref = &y; // let sum = x_ref + y_ref; // Must dereference to do math! let sum = *x_ref + *y_ref; // Must dereference to do math! println!("Sum: {}", sum); // sometimes Rust helpfully "auto-dereferences" for you println!("x: {}", x); // Direct access: 42 println!("x_ref: {}", x_ref); // Through reference: 42 (auto-dereference) println!("*x_ref: {}", *x_ref); // Manual dereference: 42 }
Why do we need * and when will it auto-deref?
- References are pointers, not the actual data
- Some operations need the actual value, not the pointer (like math operations and comparisons, and
match) - Not always needed - Rust often auto-dereferences for convenience (like in
println!or vec functions likelenandcontains) - But you're always safe if you dereference yourself
Part 4 - References to Elements Inside Collections (TC 12:45)
One of the trickiest parts about references is working with elements inside collections. Let's demystify this!
fn main() { // Vec of Copy types (i32) let numbers = vec![10, 20, 30]; let first = numbers[0]; // Copies the value let first_ref = &numbers[0]; // Reference to the element println!("Copied value: {}", first); println!("Referenced value: {}", first_ref); }
Memory diagram for the numbers example:
Stack: Heap:
┌─────────────────┐
│ first_ref: &i32 ┼───────────────────┐
├─────────────────┤ │
│ first: 10 │ (copied) │
├─────────────────┤ │
│ numbers: Vec │ ┌────┬────┬────┐
│ ├ ptr ──────────┼───────────────▶ │ 10 │ 20 │ 30 │
│ ├ len: 3 │ └────┴────┴────┘
│ └ cap: 3 │
└─────────────────┘
It's trickier with elements that live on the heap
fn main() { // Vec of non-Copy types (String) let names = vec![ String::from("Alice"), String::from("Bob") ]; // let name = names[0]; // ERROR! Can't move out of Vec let name_ref = &names[0]; // OK! Borrow the element let name_clone = names[0].clone(); // OK! Clone it println!("Referenced name: {}", name_ref); println!("Cloned name: {}", name_clone); }
Memory diagram for the names example:
Stack: Heap:
┌──────────────────┐ ┌─────┬─────┬─────┬─────┬─────┐
│ name_clone:String│ │ 'A' │ 'l' │ 'i' │ 'c' │ 'e' │ (cloned copy)
│ ├ ptr ───────────┼──────────▶└─────┴─────┴─────┴─────┴─────┘
│ ├ len: 5 │
│ └ cap: 5 │ Vec
├──────────────────┤ ┌─────────────────┐
│ name_ref: &String┼─────────▶ │String("Alice") │ ┌─────┬─────┬─────┬─────┬─────┐
├──────────────────┤ (to [0]) │ ├ ptr ──────────┼────▶│ 'A' │ 'l' │ 'i' │ 'c' │ 'e' │
│ names: Vec │ │ ├ len: 5 │ └─────┴─────┴─────┴─────┴─────┘
│ ├ ptr ───────────┼──────────▶│ └ cap: 5 │
│ ├ len: 2 │ (to vec) ├─────────────────┤
│ └ cap: 2 │ │String("Bob") │ ┌─────┬─────┬─────┐
└──────────────────┘ │ ├ ptr ──────────┼────▶│ 'B' │ 'o' │ 'b' │
│ ├ len: 3 │ └─────┴─────┴─────┘
│ └ cap: 3 │
└─────────────────┘
Key insight: vec[i] tries to move/copy the value. For non-Copy types, use &vec[i] to borrow instead.
Iterating with .iter() - Why We Get References
When you iterate with .iter(), you get references to elements, not the elements themselves:
fn main() { let numbers = vec![10, 20, 30]; // .iter() gives us &i32 (references) for num_ref in numbers.iter() { println!("Type is &i32: {}", num_ref); // To use in math, dereference: let doubled = *num_ref * 2; println!("Doubled: {}", doubled); } // Original vec still valid! println!("Original: {:?}", numbers); }
Let's draw it (with cold calls)
.iter() creates references so the Vec retains ownership—iteration doesn't consume the data!
Whereas what we used before (for i in arr to loop through an array) passes ownership (or copies simple types)
Pattern Matching to Extract Values: &val (TC 12:50)
You can use pattern matching to automatically dereference:
fn main() { let numbers = vec![10, 20, 30]; // Without pattern matching - need to dereference manually for num_ref in numbers.iter() { let squared = *num_ref * *num_ref; println!("{}", squared); } // With pattern matching - automatic dereference! for &num in numbers.iter() { let squared = num * num; // num is i32, not &i32 println!("{}", squared); } }
The & in the pattern &num says: "Match a reference, and bind the value it points to"
Enumerate with References
.enumerate() gives you (index, &value):
fn main() { let scores = vec![85, 92, 78, 95]; // enumerate gives (usize, &i32) for (i, score_ref) in scores.iter().enumerate() { println!("Score {}: {}", i, score_ref); } // Pattern match to get the value directly for (i, &score) in scores.iter().enumerate() { if score > 90 { println!("High score at index {}: {}", i, score); } } }
Quick Reference: Iterator Types
fn main() { let vec = vec![1, 2, 3]; // Three ways to iterate: for val in vec.iter() { // val: &i32 (borrow each element) println!("{}", val); } for val in &vec { // val: &i32 (shorthand for .iter()) println!("{}", val); } for val in vec.iter_mut() { // val: &mut i32 (mutable borrow - next lecture!) *val += 10; } // for val in vec { // val: i32 (consumes the vec) // println!("{}", val); // } // Can't use vec here - it was moved! }