Midterm 2 Review

Table of Contents:

Suggested way to use this review material

  1. The material is organized by major topics.
  2. For each topic, there are:
    • links to lecture modules
    • high level overview
    • examples,
    • true/false questions,
    • predict the output questions, and
    • coding challenges.
  3. Try to answer the questions without peaking at the solutions.
  4. 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

  1. T/F: A tuple struct Point3D(i32, i32, i32) can be assigned to a variable of type (i32, i32, i32).

  2. T/F: Methods that take &self can modify the struct's fields.

  3. T/F: You can have multiple impl blocks for the same struct.

  4. T/F: Struct fields are public by default in Rust.

  5. T/F: Associated functions (like constructors) don't take any form of self as a parameter.

Answers
  1. False - Tuple structs create distinct types, even with identical underlying structure
  2. False - &self is immutable; you need &mut self to modify fields
  3. True - Multiple impl blocks are allowed and sometimes useful
  4. False - Struct fields are private by default; use pub to make them public
  5. 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
  1. Output: 2
  2. Output: 3 4 (newline) 3 4
  3. Output: 212.0
  4. Output: 24 24

Coding Challenges

Challenge 1: Circle struct

Create a Circle struct with a radius field. Implement methods:

  • new(radius: f64) -> Circle - constructor
  • area(&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) -> Student
  • average(&self) -> f64 - returns average of three exams
  • letter_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:

  1. Each value has exactly one owner
  2. When owner goes out of scope, value is dropped
  3. 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, owned
  • Vec<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

  1. T/F: After let s2 = s1; where s1 is a String, both s1 and s2 are valid.

  2. T/F: You can have multiple immutable references to the same data simultaneously.

  3. T/F: Vec::push() takes &mut self because it modifies the vector.

  4. T/F: When you pass a Vec<i32> to a function without &, the function takes ownership.

  5. T/F: A mutable reference &mut T can coexist with immutable references &T to the same data.

  6. T/F: String::clone() creates a deep copy of the string data on the heap.

Answers
  1. False - Ownership moves; s1 becomes invalid
  2. True - Multiple immutable borrows are allowed
  3. True - Modifying requires mutable reference
  4. True - Without &, ownership is transferred
  5. False - Cannot have &mut and & simultaneously
  6. 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
  1. Output: 4
  2. Output: 5 (the second println with text would cause compile error - moved)
  3. Output: hello hello (newline) hello world
  4. 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:

  • mod keyword defines modules
  • pub makes items public
  • use brings items into scope
  • File structure: mod.rs or module_name.rs

Crates and Projects:

  • Binary crate: has main(), produces executable
  • Library crate: has lib.rs, provides functionality
  • Cargo.toml: manifest with dependencies
  • cargo 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

  1. T/F: By default, all items (functions, structs, etc.) in a module are public.

  2. T/F: A Rust package can have both lib.rs and main.rs.

  3. T/F: The use statement imports items at compile time and has no runtime cost.

  4. T/F: Tests are typically placed in a tests module marked with #[cfg(test)].

  5. T/F: External dependencies are listed in Cargo.toml under the [dependencies] section.

Answers
  1. False - Items are private by default; need pub for public access
  2. True - This creates both a library and binary target
  3. True - Module system is resolved at compile time
  4. True - This is the standard pattern for unit tests
  5. 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
  1. Output: 7 (private_func() call would cause compile error - not public)
  2. 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 functions
  • assert!, assert_eq!, assert_ne! macros
  • cargo test runs all tests
  • #[should_panic] for testing panics
  • Result<T, E> return type for tests that can fail

Error Handling in Rust:

See Error Handling for more details.

  • panic! for unrecoverable errors
  • Result<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

  1. T/F: Test functions must return () or Result<T, E>.

  2. T/F: The assert_eq! macro checks if two values are equal using the == operator.

  3. T/F: Tests marked with #[should_panic] pass if they panic.

  4. T/F: Private functions cannot be tested in unit tests.

  5. T/F: cargo test compiles the code in release mode by default.

Answers
  1. True - Tests can return these types
  2. True - assert_eq!(a, b) checks a == b
  3. True - #[should_panic] expects the test to panic
  4. False - Unit tests in the same module can access private functions
  5. False - cargo test uses debug mode; use --release for 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
  1. Test passes (10/2 = 5, assertion succeeds, returns Ok(()))
  2. 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

back to top

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 Type syntax
  • 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

  1. T/F: Generics in Rust have runtime overhead because type checking happens at runtime.

  2. T/F: A struct Point<T> where both x and y are type T means x and y must be the same type.

  3. T/F: Option<T> and Result<T, E> are examples of generic enums in the standard library.

  4. T/F: Trait bounds like <T: Display + Clone> require T to implement both traits.

  5. T/F: The derive attribute can automatically implement certain traits like Debug and Clone.

Answers
  1. False - Monomorphization happens at compile time; zero runtime cost
  2. True - Both fields share the same type parameter
  3. True - Both are generic enums
  4. True - + combines multiple trait bounds
  5. 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
  1. Output: 42 (newline) hello (newline) 3.14
  2. Output: 2 1
  3. Output: Value: 42
  4. 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) -> Self
  • swap(&mut self) - swaps the two values
  • larger(&self) -> &T - returns reference to the larger value (requires T: 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

back to top

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
  • 'static lifetime 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

  1. T/F: All references in Rust have lifetimes, but most are inferred by the compiler.

  2. T/F: The lifetime 'static means the reference can live for the entire program duration.

  3. T/F: Lifetime parameters in function signatures change the actual lifetimes of variables.

  4. T/F: A struct that contains references must have lifetime parameters.

  5. T/F: The notation <'a> in a function signature creates a lifetime; it doesn't declare a relationship.

Answers
  1. True - Lifetime inference works in most cases
  2. True - 'static references live for the entire program
  3. False - Lifetime annotations describe relationships, don't change actual lifetimes
  4. True - Structs with references need lifetime parameters
  5. 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
  1. Output: longer
  2. 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

back to top

Modules

Quick Review

Closures are anonymous functions that can capture environment:

  • Syntax: |param| expression or |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: Iterator trait with next() method
  • Lazy evaluation - only compute when consumed
  • Common methods: map, filter, fold, collect
  • for loops use IntoIterator
  • Three forms: iter(), iter_mut(), into_iter()

Iterator Creation Methods

  • iter() -> Create an iterator from a collection that yields immutable references (&T)to elements
  • iter_mut() -> Create an iterator that yields mutable references (&mut T) to elements
  • into_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 collection
  • next() -> 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 collection
  • take(N) -> take first N elements of an iterator and turn them into an iterator
  • cycle() -> Turn a finite iterator into an infinite one that repeats itself
  • for_each(||, ) -> Apply a closure to each element in the iterator
  • filter(||, ) -> Create new iterator from old one for elements where closure is true
  • map(||, ) -> Create new iterator by applying closure to input iterator
  • filter_map(||, ) -> Creates an iterator that both filters and maps (added)
  • any(||, ) -> Return true if closure is true for any element of the iterator
  • fold(a, |a, |, ) -> Initialize expression to a, execute closure on iterator and accumulate into a
  • reduce(|x, y|, ) -> Similar to fold but the initial value is the first element in the iterator
  • zip(iterator) -> Zip two iterators together to turn them into pairs

Other useful methods:

  • sum() -> Sum the elements of an iterator
  • product() -> Product the elements of an iterator
  • min() -> Minimum element of an iterator
  • max() -> Maximum element of an iterator
  • count() -> Count the number of elements in an iterator
  • nth(N) -> Get the Nth element of an iterator
  • skip(N) -> Skip the first N elements of an iterator
  • skip_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

  1. T/F: Closures can capture variables from their environment, but regular functions cannot.

  2. T/F: Iterator methods like map and filter are eagerly evaluated.

  3. T/F: The collect() method consumes an iterator and produces a collection.

  4. T/F: for x in vec moves ownership, while for x in &vec borrows.

  5. T/F: Closures can have explicit type annotations like |x: i32| -> i32 { x + 1 }.

  6. T/F: The fold method requires an initial accumulator value.

Answers
  1. True - Closures capture environment; functions don't
  2. False - They're lazy; evaluated only when consumed
  3. True - collect() is a consumer that builds a collection
  4. True - Without &, ownership moves; with &, it borrows
  5. True - Type annotations are optional but allowed
  6. True - fold takes 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
  1. Output: 30 (sum of 2, 4, 6, 8, 10)
  2. Output: [4, 16] (squares of even numbers: 2² and 4²)
  3. Output: 21
  4. 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:

  1. Filters for numbers > 5
  2. Squares each number
  3. 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

back to top

  1. Ownership & Borrowing: Remember the rules - one owner, multiple & OR one &mut
  2. Lifetimes: Think about what references your function returns and where they come from
  3. Generics: Use trait bounds when you need specific capabilities (PartialOrd, Display, etc.)
  4. Iterators: They're lazy - need collect() or sum() to actually compute
  5. Tests: Write tests that cover normal cases, edge cases, and error cases
  6. Read error messages: Rust's compiler errors are very helpful - read them carefully!

Good luck on your midterm!