Lecture 21 - Pattern Matching and Review
Logistics
- HW4 due Friday night
- HW3 corrections due Sunday night
- Feedback on pre-task value: mixed
- After oral exams and corrections, 25/50/75 %iles: 86%, 92%, 95%
- Our next midterm is in 2 weeks
- Changes to corrections procedure for midterm 2
Presenting Struct Design from Monday
See PDFs - 10 min
Learning Objectives
By the end of today, you should be able to:
- Use pattern matching to extract data from structs and enums
- Apply simple pattern guards for conditional matching
- Review ownership concepts from L14-L20
Part 1 - Pattern Matching with Structs
Getting data out of enums and structs
The same match statements we saw for enums works for structs:
#![allow(unused)] fn main() { #[derive(Debug)] struct Book { title: String, year: u32, rating: f64, genre: Genre, } enum Genre { Fiction, NonFiction, Mystery, SciFi, } // Extract data from enums fn describe_genre(genre: &Genre) -> &str { match genre { Genre::Fiction => "Literary fiction", Genre::NonFiction => "Factual content", Genre::Mystery => "Mystery and suspense", Genre::SciFi => "Science fiction", } } // Extract data from structs fn get_rating(book: &Book) -> f64 { match book { Book { rating, .. } => *rating, } } fn check_highly_rated(book: &Book) -> bool { match book { Book { rating, .. } if *rating >= 4.5 => true, _ => false, } } }
Pattern Guards for Complex Conditions
#![allow(unused)] fn main() { #[derive(Debug)] struct Book { title: String, rating: f64, pages: u32, } }
#![allow(unused)] fn main() { fn classify_book(book: &Book) -> &'static str { match book { Book { rating, pages, .. } if rating >= 4.5 && pages >= 400 => { "Epic Bestseller" } Book { rating, pages, .. } if rating >= 4.5 => { "Highly Rated" } Book { pages, .. } if pages >= 600 => "Epic", Book { pages, .. } if pages >= 300 => "Standard Novel", Book { rating, .. } if rating < 2.0 => "Needs Review", _ => "Short Read", } } }
Destructuring in Let Bindings
#[derive(Debug)] struct DataPoint { x: f64, y: f64, label: String, confidence: f64, } let point1 = DataPoint { x: 1.5, y: 2.3, label: "Positive".to_string(), confidence: 0.95, }; let point2 = DataPoint { x: 5.0, y: -1.1, label: "Negative".to_string(), confidence: 0.70, };
#![allow(unused)] fn main() { // Destructure in let binding let DataPoint { x, y, label, confidence } = point1; println!("({}, {}) - {} ({:.1}%)", x, y, label, confidence * 100.0); // Partial destructuring let DataPoint { label, confidence, .. } = point2; // Ignore x, y println!("We only learned: {} ({:.1}%)", label, confidence * 100.0); // In function parameters fn print_coords(DataPoint { x, y, .. }: &DataPoint) { println!("Point at ({:.2}, {:.2})", x, y); } }
Ownership Interlude: Destructuring Moves Quiz
Question: After this destructuring, what can we still use?
#![allow(unused)] fn main() { let point = DataPoint { x: 1.0, y: 2.0, label: "test".to_string(), confidence: 0.9, }; let DataPoint { x, label, .. } = point; }
What's still usable: point, point.x, point.y, point.label, point.confidence?
Part 2: Review of L14-L20
Stack vs. Heap (Lecture 14)
- Stack: Fast, fixed-size, automatic cleanup (LIFO - "stack of plates")
- Each function call gets a stack frame
- Stores simple types:
i32,bool,char, arrays, tuples - Variables cleaned up when function ends
- Heap: Flexible size, manual management (Rust helps!)
- For data that can grow/shrink or are very large
- Types like
String,Vec,HashMap,Boxstore data here - Stack holds pointers to heap data
- Memory addresses: Every location has a unique address to a physical part of RAM (like 0x7fff5fbff6bc)
Ownership Rules (Lecture 15)
The three fundamental ownership rules:
- Each value has an owner
- Only one owner at a time
- When owner goes out of scope, value is dropped
Key concepts:
- Move semantics:
let s2 = s1;moves ownership for heap types (String,Vec) - Copy semantics:
let y = x;copies value for stack types (i32,bool) - Clone:
.clone()creates explicit copy of heap data (and copies stack data) - Function calls: Passing to function moves or copies (same rules)
- Return values: Transfer ownership back to caller
Borrowing and References (Lecture 16)
- Creating references:
&operator borrows without taking ownershiplet data_ref = &data;- bothdataanddata_refusable
- Functions with references:
fn process(data: &Vec<i32>)- can use data without moving it - Dereferencing:
*operator accesses value through reference*x_ref > *y_refto compare values, and for use inmatch- Often auto-dereferenced (eg
println!and arithmetic)
- Pattern matching:
for &num in numbers.iter()extracts values (Copy types only)
Mutable References (Lecture 17)
Borrowing rules (the borrow checker):
- Rule 1: Many immutable references OR one mutable reference (not both!)
- Rule 2: References must not outlive what they point to
Mutable references (&mut T):
fn modify(data: &mut Vec<i32>)- can change borrowed data- Must declare variable as
mutto create mutable reference - Can't have other references (mutable or immutable) at same time
- Use
*to modify through reference:*x = 10;
Reference timing:
- Rust tracks when references are last used
- Can create new references after previous ones stop being used
Strings and Slices (Lecture 18)
- String types:
String: Owned, growable string on heap&str: String slice - reference to string data (points to the heap)- string literals point to the binary itself
- string slices built on owned strings point to the heap
- they have the same type/structure (a "fat pointer")
&String: Reference to a String (points to the stack)
Slices:
- Syntax:
&data[start..end],&data[..3],&data[2..] &[T]: Slice of array/Vec elements (pointer + length)- Slices are references - don't take ownership
UTF-8 encoding:
- Characters can be 1-4 bytes
.len()returns bytes, not character count- Use
.chars()to iterate over characters - No
text[0]indexing - would fail to compile
Iterators:
.iter(): Generates immutable references&T.iter_mut(): Generates mutable references&mut T.iter().enumerate(): Generates pairs (i32,&T).iter_mut().enumerate(): Generates pairs (i32,&mut T).collect(): Collapse iterators into target type
HashMap and HashSet (Lecture 19)
Hash functions:
- Convert any input to a number (deterministic, fast, uniform distribution)
- Used to determine bucket index:
hash % capacity
HashMap<K, V>:
- Hashes the key to get a hash value
- Creates a bucket array that stores hash values and pointers to (key, value) pairs
- Create:
HashMap::new()or collect from iterator - Insert:
.insert(key, value)- overwrites if key exists - Get:
.get(&key)returnsOption<&V> - Check:
.contains_key(&key)returnsbool - Iterate:
for (key, value) in map.iter() - Pattern:
.entry(key).or_insert(value)for counting/defaults
HashSet
- Stores unique values (no duplicates)
.insert(value),.contains(&value)- Create from Vec:
vec.iter().cloned().collect() - Fast membership testing (better than Vec for large data)
Structs and Methods (Lecture 20)
Defining structs:
#![allow(unused)] fn main() { struct Customer { name: String, age: u32, member: bool, } }
- Group related data together
- Access fields with dot notation:
customer.name - Tuple structs:
struct Point(f64, f64, f64); - Update syntax:
Customer { name: "Bob".to_string(), ..alice }
Methods (impl blocks):
#![allow(unused)] fn main() { impl Customer { fn new(name: String) -> Customer { ... } // Constructor fn display(&self) { ... } // Read-only fn update_age(&mut self, age: u32) { ... } // Modify fn into_name(self) -> String { ... } // Consume } }
Some function signature best practices
#![allow(unused)] fn main() { // ❌ BAD: Unnecessary ownership transfer fn bad_process(key: String, value: f64) -> f64 { value * 2.0 // Doesn't need to own `key`! } // ✅ GOOD: Flexible parameter fn good_process(key: &str, value: f64) -> f64 { value * 2.0 // Accepts both String and &str } // ❌ BAD: Dangerous unwrap fn bad_lookup(map: &HashMap<String, i32>, key: &str) -> i32 { map.get(key).unwrap() // Panics if key missing! } // ✅ GOOD: Safe Option handling fn good_lookup(map: &HashMap<String, i32>, key: &str) -> Option<i32> { map.get(key).copied() // Returns Option for safety } }
Debug Quiz
Question: What's wrong with this code?
#![allow(unused)] fn main() { fn get_first_line(text: String) -> &str { let lines: Vec<&str> = text.lines().collect(); if lines.is_empty() { "" } else { lines[0] } } }
A) Should return Option<&str> for safety
B) Can't return reference to text which goes out of scope
C) collect() is unnecessary here
A few common ownership issues
#![allow(unused)] fn main() { #[derive(Debug, Clone)] struct Data { value: i32, label: String } // Issue 1: Unnecessary ownership fn process_label(label: String) -> String { // Should be &str? label.to_uppercase() } // Issue 2: Lifetime problem fn get_first_item(items: Vec<String>) -> &String { // ERROR - Can't return ref to owned data! &items[0] } // Issue 3: Move in loop fn analyze_data(data_list: Vec<Data>) { for data in data_list { // Moves each Data println!("{:?}", data); } println!("Count: {}", data_list.len()); // ERROR - data_list moved! } // Issue 4: Borrowing conflict fn modify_and_read(items: &mut Vec<i32>) { let first = &items[0]; // Immutable borrow items.push(42); // ERROR - Mutable borrow while immutable exists! println!("First: {}", first); } }