Midterm 2 Review
Table of Contents:
- Preliminaries
- 1. Structs and Methods
- 2. Ownership and Borrowing, Strings and Vecs
- 3. Modules, Crates and Projects
- 4. Tests and Error Handling
- 5. Generics and Traits
- 6. Lifetimes
- 7. Closures and Iterators
- Final Tips for the Exam
Suggested way to use this review material
- The material is organized by major topics.
- For each topic, there are:
- links to lecture modules
- high level overview
- examples,
- true/false questions,
- predict the output questions, and
- coding challenges.
- Try to answer the questions without peaking at the solutions.
- The material is not guaranteed to be complete, so you should review the material in the lectures as well as this review material.
Book References:
The lectures modules all start with pre-reading assignments that point to the relevant chapters in The Rust Language Book.
Exam Format:
The exam will be in four parts:
- Part 1 (10 pts): 5 questions, 2 points each -- select all that are true
- Part 2 (16 pts): 4 questions, 4 points each -- find the bug in the code and fix it
- Part 3 (12 pts): 4 questions, 3 points each -- Predict the output and explain why
- Part 4 (12 pts): 2 questions, 6 points each -- hand-coding problems
Total Points: 50
Suggested time budget for each part:
- Part 1: (~10 min)
- Part 2: (~16 min)
- Part 3: (~12 min)
- Part 4: (~22 min)
for a total of 60 minutes and then another 15 minutes to check your work.
Preliminaries
The material for midterm 2 assumes that you have gained proficiency with Rust's basic syntax such as main and function definitions, basic data types including tuples and enums as well as defining and passing values as arguments to functions, etc.
For example you should be familiar enough with Rust syntax type in the following program code from memory, without notes.
Basic main function
// Write a main function that prints "Hello, DS210!"
Expected output:
Hello, DS210!
Basic Function Calling
// Create a function called `print_hello` that takes no arguments and // doesn't return anything, but prints "Hello, DS210!". // Write a main function that calls `print_hello`.
Expected output:
Hello, DS210!
Calling Function with Argument
// Create a function called 'print_hello' that takes an integer argument // and prints, for example for argument `340`, "Hello, DS340!". // Write a main function that call `print_hello with some integer number.
Output for argument 110:
Hello, DS110!
Challenge yourself with increasingly more complex exercises.
If you struggled with remembering the syntax for those exercises, then consider practicing these basics before moving on to the slightly more advanced syntax below. Practice by writing code into an empty Rust Playground.
You can review the basics of Rust syntax in the A1 Midterm 1 Review.
Review basic and complex data types, e.g. tuples, arrays, Vecs, Strings, enums, etc., methods on these data types like len(), push(), pop(), get(), insert(), remove(), etc.
1. Structs and Methods
Modules
Quick Review
Structs group related data together with named fields, providing type safety and semantic meaning. Unlike tuples, fields have names making code self-documenting.
Key Concepts:
- Regular structs:
struct Person { name: String, age: u32 } - Tuple structs:
struct Point3D(f64, f64, f64)- named tuples for type safety - Field access with
.notation - Methods with
self,&self, or&mut self
Examples
#![allow(unused)] fn main() { // Regular struct struct Rectangle { width: u32, height: u32, } // Implementation block with methods impl Rectangle { // Constructor (associated function) fn new(width: u32, height: u32) -> Rectangle { Rectangle { width, height } } // Method borrowing immutably fn area(&self) -> u32 { self.width * self.height } // Method borrowing mutably fn scale(&mut self, factor: u32) { self.width *= factor; self.height *= factor; } } // Tuple struct for type safety struct Miles(f64); struct Kilometers(f64); // Cannot accidentally mix these types! }
True/False Questions
-
T/F: A tuple struct
Point3D(i32, i32, i32)can be assigned to a variable of type(i32, i32, i32). -
T/F: Methods that take
&selfcan modify the struct's fields. -
T/F: You can have multiple
implblocks for the same struct. -
T/F: Struct fields are public by default in Rust.
-
T/F: Associated functions (like constructors) don't take any form of
selfas a parameter.
Answers
- False - Tuple structs create distinct types, even with identical underlying structure
- False -
&selfis immutable; you need&mut selfto modify fields - True - Multiple impl blocks are allowed and sometimes useful
- False - Struct fields are private by default; use
pubto make them public - True - Associated functions are called on the type itself (e.g.,
Rectangle::new())
Predict the Output (3-4 questions)
Question 1:
struct Counter { count: i32, } impl Counter { fn new() -> Counter { Counter { count: 0 } } fn increment(&mut self) { self.count += 1; } } fn main() { let mut c = Counter::new(); c.increment(); c.increment(); println!("{}", c.count); }
Question 2:
struct Point(i32, i32); fn main() { let p = Point(3, 4); println!("{} {}", p.0, p.1); let Point(x, y) = p; println!("{} {}", x, y); }
Question 3:
struct Temperature { celsius: f64, } impl Temperature { fn new(celsius: f64) -> Self { Self { celsius } } fn to_fahrenheit(&self) -> f64 { self.celsius * 1.8 + 32.0 } } fn main() { let temp = Temperature::new(100.0); println!("{:.1}", temp.to_fahrenheit()); }
Question 4:
struct Box3D { width: u32, height: u32, depth: u32, } impl Box3D { fn volume(&self) -> u32 { self.width * self.height * self.depth } } fn main() { let b = Box3D { width: 2, height: 3, depth: 4 }; let v1 = b.volume(); let v2 = b.volume(); println!("{} {}", v1, v2); }
Answers
- Output:
2 - Output:
3 4(newline)3 4 - Output:
212.0 - Output:
24 24
Coding Challenges
Challenge 1: Circle struct
Create a Circle struct with a radius field. Implement methods:
new(radius: f64) -> Circle- constructorarea(&self) -> f64- returns area (use π ≈ 3.14159)scale(&mut self, factor: f64)- multiplies radius by factor
// your code here
Solution
struct Circle { radius: f64, } impl Circle { fn new(radius: f64) -> Circle { Circle { radius } } fn area(&self) -> f64 { 3.14159 * self.radius * self.radius } fn scale(&mut self, factor: f64) { self.radius *= factor; } } fn main() { let mut c = Circle::new(5.0); println!("Area: {}", c.area()); c.scale(2.0); println!("New area: {}", c.area()); }
Challenge 2: Student struct with grade calculation
Create a Student struct with fields for name (String) and three exam scores (exam1, exam2, exam3 as u32). Implement:
new(name: String, e1: u32, e2: u32, e3: u32) -> Studentaverage(&self) -> f64- returns average of three examsletter_grade(&self) -> char- returns 'A' (90+), 'B' (80-89), 'C' (70-79), 'D' (60-69), 'F' (<60)
// your code here
Solution
#![allow(unused)] fn main() { struct Student { name: String, exam1: u32, exam2: u32, exam3: u32, } impl Student { fn new(name: String, e1: u32, e2: u32, e3: u32) -> Student { Student { name, exam1: e1, exam2: e2, exam3: e3 } } fn average(&self) -> f64 { (self.exam1 + self.exam2 + self.exam3) as f64 / 3.0 } fn letter_grade(&self) -> char { let avg = self.average(); if avg >= 90.0 { 'A' } else if avg >= 80.0 { 'B' } else if avg >= 70.0 { 'C' } else if avg >= 60.0 { 'D' } else { 'F' } } } }
2. Ownership and Borrowing, Strings and Vecs
Modules
Quick Review
Ownership Rules:
- Each value has exactly one owner
- When owner goes out of scope, value is dropped
- Ownership can be moved or borrowed
Borrowing:
- Immutable references
&T: multiple allowed, read-only - Mutable references
&mut T: only ONE at a time, exclusive access - References must always be valid (no dangling)
Key Types:
String: heap-allocated, growable, ownedVec<T>: heap-allocated dynamic array, owns elements- Both have ptr, length, capacity on stack
Examples
#![allow(unused)] fn main() { // Ownership transfer (move) let s1 = String::from("hello"); let s2 = s1; // s1 is now invalid // println!("{}", s1); // ERROR! // Borrowing immutably let s3 = String::from("world"); let len = calculate_length(&s3); // borrow println!("{} has length {}", s3, len); // s3 still valid // Borrowing mutably let mut v = vec![1, 2, 3]; add_one(&mut v); // exclusive mutable borrow fn calculate_length(s: &String) -> usize { s.len() } fn add_one(v: &mut Vec<i32>) { for item in v.iter_mut() { *item += 1; } } }
True/False Questions
-
T/F: After
let s2 = s1;wheres1is aString, boths1ands2are valid. -
T/F: You can have multiple immutable references to the same data simultaneously.
-
T/F:
Vec::push()takes&mut selfbecause it modifies the vector. -
T/F: When you pass a
Vec<i32>to a function without&, the function takes ownership. -
T/F: A mutable reference
&mut Tcan coexist with immutable references&Tto the same data. -
T/F:
String::clone()creates a deep copy of the string data on the heap.
Answers
- False - Ownership moves;
s1becomes invalid - True - Multiple immutable borrows are allowed
- True - Modifying requires mutable reference
- True - Without
&, ownership is transferred - False - Cannot have
&mutand&simultaneously - True -
clone()performs a deep copy
Predict the Output
Question 1:
fn main() { let mut v = vec![1, 2, 3]; v.push(4); println!("{}", v.len()); }
Question 2:
fn process(s: String) -> usize { s.len() } fn main() { let text = String::from("hello"); let len = process(text); println!("{}", len); //println!("{}", text); // Would this compile? }
Question 3:
fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &s; println!("{} {}", r1, r2); let r3 = &mut s; r3.push_str(" world"); println!("{}", r3); }
Question 4:
fn main() { let v1 = vec![1, 2, 3]; let v2 = v1.clone(); println!("{} {}", v1.len(), v2.len()); }
Answers
- Output:
4 - Output:
5(the second println withtextwould cause compile error - moved) - Output:
hello hello(newline)hello world - Output:
3 3
Coding Challenges
Challenge 1: Fix the borrowing errors
// Fix this code so it compiles fn main() { let mut numbers = vec![1, 2, 3]; let sum = calculate_sum(numbers); double_all(numbers); println!("Sum: {}, Doubled: {:?}", sum, numbers); } fn calculate_sum(v: Vec<i32>) -> i32 { v.iter().sum() } fn double_all(v: Vec<i32>) { for x in v.iter() { x *= 2; } }
Solution
fn main() { let mut numbers = vec![1, 2, 3]; let sum = calculate_sum(&numbers); // borrow instead of move double_all(&mut numbers); // mutable borrow println!("Sum: {}, Doubled: {:?}", sum, numbers); } fn calculate_sum(v: &Vec<i32>) -> i32 { // take reference v.iter().sum() } fn double_all(v: &mut Vec<i32>) { // take mutable reference for x in v.iter_mut() { // mutable iterator *x *= 2; // dereference to modify } }
Challenge 2: String manipulation
Write a function reverse_words(s: &str) -> String that takes a string slice and returns a new String with words in reverse order. For example, "hello world rust" becomes "rust world hello".
hint #1
The string method .split_whitespace() might be very useful.
hint #2
Collect the splitted string into a Vec<&str>.
// Your code here
Solution
fn reverse_words(s: &str) -> String { let words: Vec<&str> = s.split_whitespace().collect(); let mut result = String::new(); for (i, word) in words.iter().rev().enumerate() { if i > 0 { result.push(' '); } result.push_str(word); } result } fn main() { let original = "hello world rust"; let reversed = reverse_words(original); println!("{}", reversed); // "rust world hello" }
3. Modules, Crates and Projects
Modules
Quick Review
Modules organize code within a crate:
modkeyword defines modulespubmakes items publicusebrings items into scope- File structure:
mod.rsormodule_name.rs
Crates and Projects:
- Binary crate: has
main(), produces executable - Library crate: has
lib.rs, provides functionality Cargo.toml: manifest with dependenciescargo build,cargo test,cargo run
Examples
// lib.rs pub mod shapes { pub struct Circle { pub radius: f64, } impl Circle { pub fn new(radius: f64) -> Circle { Circle { radius } } pub fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } } } // main.rs use crate::shapes::Circle; fn main() { let c = Circle::new(5.0); println!("Area: {}", c.area()); }
True/False Questions
-
T/F: By default, all items (functions, structs, etc.) in a module are public.
-
T/F: A Rust package can have both
lib.rsandmain.rs. -
T/F: The
usestatement imports items at compile time and has no runtime cost. -
T/F: Tests are typically placed in a
testsmodule marked with#[cfg(test)]. -
T/F: External dependencies are listed in
Cargo.tomlunder the[dependencies]section.
Answers
- False - Items are private by default; need
pubfor public access - True - This creates both a library and binary target
- True - Module system is resolved at compile time
- True - This is the standard pattern for unit tests
- True - Dependencies are declared in
Cargo.toml
Predict the Output
Question 1:
mod math { pub fn add(a: i32, b: i32) -> i32 { a + b } fn private_func() { println!("Private"); } } fn main() { println!("{}", math::add(3, 4)); // math::private_func(); // What happens? }
Question 2:
mod outer { pub mod inner { pub fn greet() { println!("Hello from inner"); } } } use outer::inner; fn main() { inner::greet(); }
Answers
- Output:
7(private_func() call would cause compile error - not public) - Output:
Hello from inner
Coding Challenge
Challenge: Create a temperature conversion module
Create a module called temperature with:
- Function
celsius_to_fahrenheit(c: f64) -> f64- fahrenheit = celsius * 1.8 + 32.0
- Function
fahrenheit_to_celsius(f: f64) -> f64- celsius = (fahrenheit - 32.0) / 1.8
- Function
celsius_to_kelvin(c: f64) -> f64- kelvin = celsius + 273.15
All functions should be public.
In a main function, use the module to convert 100°C to Fahrenheit, 32°F to Celsius, and 0°C to Kelvin and print the results.
// your code here
Solution
pub mod temperature { pub fn celsius_to_fahrenheit(c: f64) -> f64 { c * 1.8 + 32.0 } pub fn fahrenheit_to_celsius(f: f64) -> f64 { (f - 32.0) / 1.8 } pub fn celsius_to_kelvin(c: f64) -> f64 { c + 273.15 } } fn main() { use temperature::*; println!("100°C = {}°F", celsius_to_fahrenheit(100.0)); println!("32°F = {}°C", fahrenheit_to_celsius(32.0)); println!("0°C = {}K", celsius_to_kelvin(0.0)); }
4. Tests and Error Handling
Modules
Quick Review
Testing in Rust:
- Unit tests: in same file with
#[cfg(test)]module #[test]attribute marks test functionsassert!,assert_eq!,assert_ne!macroscargo testruns all tests#[should_panic]for testing panicsResult<T, E>return type for tests that can fail
Error Handling in Rust:
See Error Handling for more details.
panic!for unrecoverable errorsResult<T,E>for recoverable errors?to propagate errors
Examples
#![allow(unused)] fn main() { pub fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn test_add() { assert_eq!(add(2, 3), 5); } #[test] fn test_add_negative() { assert_eq!(add(-1, 1), 0); } #[test] #[should_panic] fn test_overflow() { let _x = i32::MAX + 1; // Should panic in debug mode } } }
True/False Questions
-
T/F: Test functions must return
()orResult<T, E>. -
T/F: The
assert_eq!macro checks if two values are equal using the==operator. -
T/F: Tests marked with
#[should_panic]pass if they panic. -
T/F: Private functions cannot be tested in unit tests.
-
T/F:
cargo testcompiles the code in release mode by default.
Answers
- True - Tests can return these types
- True -
assert_eq!(a, b)checksa == b - True -
#[should_panic]expects the test to panic - False - Unit tests in the same module can access private functions
- False -
cargo testuses debug mode; use--releasefor release mode
Predict the Output
Question 1:
What would the result be for cargo test on this code?
#![allow(unused)] fn main() { #[cfg(test)] mod tests { #[test] fn test_pass() { assert_eq!(2 + 2, 4); } #[test] fn test_fail() { assert_eq!(2 + 2, 5); } } }
Question 2:
What would the result be for cargo test on this code?
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_divide_ok() -> Result<(), String> {
let result = divide(10, 2);
assert_eq!(result, Ok(5));
Ok(())
}
#[test]
fn test_divide_err() {
let result = divide(10, 0);
assert_eq!(result, Err(String::from("Division by zero")));
}
}
Answers
- Test passes (10/2 = 5, assertion succeeds, returns Ok(()))
- Test passes (10/0 = error, assertion succeeds, returns Err(String::from("Division by zero")))
Coding Challenge
Challenge: Write tests for a max function
Write a function max_of_three(tup: (i32, i32, i32)) -> i32 that returns the maximum of three integers given in a tuple. Then write at least 3 test cases.
// your code here
Solution
#![allow(unused)] fn main() { pub fn max_of_three(tup: (i32, i32, i32)) -> i32 { if tup.0 >= tup.1 && tup.0 >= tup.2 { tup.0 } else if tup.1 >= tup.2 { tup.1 } else { tup.2 } } #[cfg(test)] mod tests { use super::*; #[test] fn test_first_is_max() { assert_eq!(max_of_three((5, 2, 3)), 5); } #[test] fn test_second_is_max() { assert_eq!(max_of_three((1, 9, 4)), 9); } #[test] fn test_third_is_max() { assert_eq!(max_of_three((2, 3, 10)), 10); } #[test] fn test_all_equal() { assert_eq!(max_of_three((7, 7, 7)), 7); } } }
5. Generics and Traits
Modules
Quick Review
Generics enable code reuse across different types:
- Type parameters:
<T>,<T, U>, etc. - Monomorphization: compiler generates specialized versions
- Zero runtime cost
- Trait bounds constrain generic types:
<T: Display>
Traits define shared behavior:
- Like interfaces in other languages
impl Trait for Typesyntax- Standard traits:
Debug,Clone,PartialEq,PartialOrd,Display, etc. - Trait bounds:
fn foo<T: Trait>(x: T) - Trait bounds can be combined with multiple traits:
fn foo<T: Trait1 + Trait2>(x: T)
Examples
Generic function:
#![allow(unused)] fn main() { // Generic function fn largest<T: PartialOrd>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } }
Generic struct:
#![allow(unused)] fn main() { // Generic struct struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn new(x: T, y: T) -> Self { Point { x, y } } } }
Trait definition:
#![allow(unused)] fn main() { // Trait definition trait Summary { fn summarize(&self) -> String; } // Trait implementation struct Article { title: String, author: String, } impl Summary for Article { fn summarize(&self) -> String { format!("{} by {}", self.title, self.author) } } }
True/False Questions
-
T/F: Generics in Rust have runtime overhead because type checking happens at runtime.
-
T/F: A struct
Point<T>where both x and y are type T means x and y must be the same type. -
T/F:
Option<T>andResult<T, E>are examples of generic enums in the standard library. -
T/F: Trait bounds like
<T: Display + Clone>require T to implement both traits. -
T/F: The
deriveattribute can automatically implement certain traits likeDebugandClone.
Answers
- False - Monomorphization happens at compile time; zero runtime cost
- True - Both fields share the same type parameter
- True - Both are generic enums
- True -
+combines multiple trait bounds - True -
#[derive(Debug, Clone)]auto-implements these traits
Predict the Output
Question 1:
fn print_type<T: std::fmt::Display>(x: T) { println!("{}", x); } fn main() { print_type(42); print_type("hello"); print_type(3.14); }
Question 2:
fn swap<T>(a: T, b: T) -> (T, T) { (b, a) } fn main() { let (x, y) = swap(1, 2); println!("{} {}", x, y); }
Question 3:
struct Container<T> { value: T, } impl<T: std::fmt::Display> Container<T> { fn show(&self) { println!("Value: {}", self.value); } } fn main() { let c = Container { value: 42 }; c.show(); }
Question 4:
trait Double { fn double(&self) -> Self; } impl Double for i32 { fn double(&self) -> Self { self * 2 } } fn main() { let x = 5; println!("{}", x.double()); }
Answers
- Output:
42(newline)hello(newline)3.14 - Output:
2 1 - Output:
Value: 42 - Output:
10
Coding Challenges
Challenge 1: Generic pair
Create a generic struct Pair<T> that holds two values of the same type. Implement:
new(first: T, second: T) -> Selfswap(&mut self)- swaps the two valueslarger(&self) -> &T- returns reference to the larger value (requiresT: PartialOrd)
// your code here
Solution
struct Pair<T> { first: T, second: T, } impl<T> Pair<T> { fn new(first: T, second: T) -> Self { Pair { first, second } } fn swap(&mut self) { std::mem::swap(&mut self.first, &mut self.second); } } impl<T: PartialOrd> Pair<T> { fn larger(&self) -> &T { if self.first > self.second { &self.first } else { &self.second } } } fn main() { let mut p = Pair::new(5, 10); println!("{}", p.larger()); // 10 p.swap(); println!("{}", p.larger()); // 10 }
Challenge 2: Trait for area calculation
Define a trait Area with a method area(&self) -> f64. Implement it for Circle (radius) and Rectangle (width, height).
// your code here
Solution
trait Area { fn area(&self) -> f64; } struct Circle { radius: f64, } struct Rectangle { width: f64, height: f64, } impl Area for Circle { fn area(&self) -> f64 { 3.14159 * self.radius * self.radius } } impl Area for Rectangle { fn area(&self) -> f64 { self.width * self.height } } fn main() { let c = Circle { radius: 5.0 }; let r = Rectangle { width: 4.0, height: 6.0 }; println!("Circle area: {}", c.area()); println!("Rectangle area: {}", r.area()); }
6. Lifetimes
Modules
Quick Review
Lifetimes ensure references are valid:
- Prevent dangling references at compile time
- Notation:
'a,'b, etc. - Most lifetimes are inferred
- Explicit annotations needed when ambiguous
- Lifetime elision rules reduce annotations needed
Key Concepts:
- Every reference has a lifetime
- Function signatures sometimes need lifetime annotations
- Structs with references need lifetime parameters
'staticlifetime lasts entire program
Examples
#![allow(unused)] fn main() { // Explicit lifetime annotations fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } // Struct with lifetime struct Book<'a> { title: &'a str, author: &'a str, } // Multiple lifetimes fn first_word<'a, 'b>(s: &'a str, _other: &'b str) -> &'a str { s.split_whitespace().next().unwrap_or("") } // Static lifetime let s: &'static str = "This string lives forever"; }
True/False Questions
-
T/F: All references in Rust have lifetimes, but most are inferred by the compiler.
-
T/F: The lifetime
'staticmeans the reference can live for the entire program duration. -
T/F: Lifetime parameters in function signatures change the actual lifetimes of variables.
-
T/F: A struct that contains references must have lifetime parameters.
-
T/F: The notation
<'a>in a function signature creates a lifetime; it doesn't declare a relationship.
Answers
- True - Lifetime inference works in most cases
- True -
'staticreferences live for the entire program - False - Lifetime annotations describe relationships, don't change actual lifetimes
- True - Structs with references need lifetime parameters
- False -
<'a>declares a lifetime parameter; annotations describe relationships
Predict the Output
Question 1:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } fn main() { let s1 = String::from("short"); let s2 = String::from("longer"); let result = longest(&s1, &s2); println!("{}", result); }
Question 2:
fn first<'a>(x: &'a str, _y: &str) -> &'a str { x } fn main() { let s1 = "hello"; let s2 = "world"; println!("{}", first(s1, s2)); }
Answers
- Output:
longer - Output:
hello
Coding Challenge
Challenge: Implement a function with lifetimes
Write a function get_first_sentence<'a>(text: &'a str) -> &'a str that returns the first sentence (up to the first period, or the whole string if no period exists).
// your code here
Solution
fn get_first_sentence<'a>(text: &'a str) -> &'a str { match text.find('.') { Some(pos) => &text[..=pos], None => text, } } fn main() { let text = "Hello world. This is Rust."; let first = get_first_sentence(text); println!("{}", first); // "Hello world." let text2 = "No period here"; let first2 = get_first_sentence(text2); println!("{}", first2); // "No period here" }
7. Closures and Iterators
Modules
Quick Review
Closures are anonymous functions that can capture environment:
- Syntax:
|param| expressionor|param| { body } - Capture variables from surrounding scope
- Enable lazy evaluation
- Used with iterators and functional programming
- A predicate is a closure (or function) that returns a boolean value.
Iterators:
- Trait-based:
Iteratortrait withnext()method - Lazy evaluation - only compute when consumed
- Common methods:
map,filter,fold,collect forloops useIntoIterator- Three forms:
iter(),iter_mut(),into_iter()
Iterator Creation Methods
iter()-> Create an iterator from a collection that yields immutable references(&T)to elementsiter_mut()-> Create an iterator that yields mutable references(&mut T)to elementsinto_iter()-> Consumes the collection and yields owned values(T)transferring ownership to the iterator
Iterator Methods and Adapters
From Iterator Methods and Adapters module:
Pay special attention to what the output is.
into_iter()-> Create an iterator that consumes the collectionnext()-> Get the next element of an iterator (None if there isn't one)enumerate()-> Create an iterator that yields the index and the element (added)collect()-> Put iterator elements in collectiontake(N)-> take first N elements of an iterator and turn them into an iteratorcycle()-> Turn a finite iterator into an infinite one that repeats itselffor_each(||, )-> Apply a closure to each element in the iteratorfilter(||, )-> Create new iterator from old one for elements where closure is truemap(||, )-> Create new iterator by applying closure to input iteratorfilter_map(||, )-> Creates an iterator that both filters and maps (added)any(||, )-> Return true if closure is true for any element of the iteratorfold(a, |a, |, )-> Initialize expression to a, execute closure on iterator and accumulate into areduce(|x, y|, )-> Similar to fold but the initial value is the first element in the iteratorzip(iterator)-> Zip two iterators together to turn them into pairs
Other useful methods:
sum()-> Sum the elements of an iteratorproduct()-> Product the elements of an iteratormin()-> Minimum element of an iteratormax()-> Maximum element of an iteratorcount()-> Count the number of elements in an iteratornth(N)-> Get the Nth element of an iteratorskip(N)-> Skip the first N elements of an iteratorskip_while(||, )-> Skip elements while the closure is true
If the method returns an iterator, you have to do something with the iterator.
See Rust provided methods for the complete list.
Examples
#![allow(unused)] fn main() { // Closure basics let add = |x, y| x + y; let result = add(3, 4); // 7 // Capturing environment let multiplier = 3; let multiply = |x| x * multiplier; println!("{}", multiply(5)); // 15 // Iterators let numbers = vec![1, 2, 3, 4, 5]; // map and filter (lazy) let doubled: Vec<i32> = numbers.iter() .map(|x| x * 2) .filter(|x| x > &5) .copied() .collect(); // fold let sum: i32 = numbers.iter().fold(0, |acc, x| acc + x); // Lazy evaluation let result = Some(5).unwrap_or_else(|| expensive_function()); }
True/False Questions
-
T/F: Closures can capture variables from their environment, but regular functions cannot.
-
T/F: Iterator methods like
mapandfilterare eagerly evaluated. -
T/F: The
collect()method consumes an iterator and produces a collection. -
T/F:
for x in vecmoves ownership, whilefor x in &vecborrows. -
T/F: Closures can have explicit type annotations like
|x: i32| -> i32 { x + 1 }. -
T/F: The
foldmethod requires an initial accumulator value.
Answers
- True - Closures capture environment; functions don't
- False - They're lazy; evaluated only when consumed
- True -
collect()is a consumer that builds a collection - True - Without
&, ownership moves; with&, it borrows - True - Type annotations are optional but allowed
- True -
foldtakes initial value and closure
Predict the Output
Question 1:
fn main() { let numbers = vec![1, 2, 3, 4, 5]; let sum: i32 = numbers.iter().map(|x| x * 2).sum(); println!("{}", sum); }
Question 2:
fn main() { let numbers = vec![1, 2, 3, 4, 5]; let result: Vec<i32> = numbers.iter() .filter(|x| *x % 2 == 0) .map(|x| x * x) .collect(); println!("{:?}", result); }
Question 3:
fn main() { let factor = 3; let multiply = |x| x * factor; println!("{}", multiply(7)); }
Question 4:
fn main() { let numbers = vec![1, 2, 3]; let result = numbers.iter() .fold(0, |acc, x| acc + x); println!("{}", result); }
Answers
- Output:
30(sum of 2, 4, 6, 8, 10) - Output:
[4, 16](squares of even numbers: 2² and 4²) - Output:
21 - Output:
6(1 + 2 + 3)
Coding Challenges
Challenge 1: Custom filter
Write a function count_if<F>(vec: &Vec<i32>, predicate: F) -> usize where F is a closure that takes &i32 and returns bool. The function returns the count of elements satisfying the predicate.
// your code here
Solution
fn count_if<F>(vec: &Vec<i32>, predicate: F) -> usize where F: Fn(&i32) -> bool { let mut count = 0; for item in vec { if predicate(item) { count += 1; } } count } // Alternative using iterators: fn count_if_iter<F>(vec: &Vec<i32>, predicate: F) -> usize where F: Fn(&i32) -> bool { vec.iter().filter(|x| predicate(x)).count() } fn main() { let numbers = vec![1, 2, 3, 4, 5, 6]; let evens = count_if(&numbers, |x| x % 2 == 0); println!("{}", evens); // 3 }
Challenge 2: Iterator chain
Given a Vec<i32>, create an iterator chain that:
- Filters for numbers > 5
- Squares each number
- Sums the results
// your code here
Solution
fn process_numbers(numbers: &Vec<i32>) -> i32 { numbers.iter() .filter(|&&x| x > 5) .map(|x| x * x) .sum() } fn main() { let numbers = vec![1, 3, 6, 8, 10, 2]; let result = process_numbers(&numbers); println!("{}", result); // 6² + 8² + 10² = 36 + 64 + 100 = 200 }
Challenge 3: Custom map
Implement a function apply_to_all<F>(vec: &mut Vec<i32>, f: F) that applies a closure to each element, modifying the vector in place.
// your code here
Solution
fn apply_to_all<F>(vec: &mut Vec<i32>, f: F) where F: Fn(i32) -> i32 { for item in vec.iter_mut() { *item = f(*item); } } fn main() { let mut numbers = vec![1, 2, 3, 4, 5]; apply_to_all(&mut numbers, |x| x * 2); println!("{:?}", numbers); // [2, 4, 6, 8, 10] }
Final Tips for the Exam
- Ownership & Borrowing: Remember the rules - one owner, multiple
&OR one&mut - Lifetimes: Think about what references your function returns and where they come from
- Generics: Use trait bounds when you need specific capabilities (PartialOrd, Display, etc.)
- Iterators: They're lazy - need
collect()orsum()to actually compute - Tests: Write tests that cover normal cases, edge cases, and error cases
- Read error messages: Rust's compiler errors are very helpful - read them carefully!
Good luck on your midterm!