Methods Continued

About This Module

This module revisits and expands on method syntax in Rust, focusing on different types of self parameters and their implications for ownership and borrowing. You'll learn the differences between self, &self, and &mut self, and when to use each approach for method design.

Prework

Prework Readings

Read the following sections from "The Rust Programming Language" book:

Pre-lecture Reflections

Before class, consider these questions:

  1. What are the implications of using self vs &self vs &mut self in method signatures?
  2. How does method call syntax relate to function call syntax with explicit references?
  3. When would you design a method to take ownership of self?
  4. How do method calls interact with Rust's borrowing rules?
  5. What are the trade-offs between different self parameter types?

Learning Objectives

By the end of this module, you should be able to:

  • Distinguish between self, &self, and &mut self parameter types
  • Understand when methods take ownership vs. borrow references
  • Design method APIs that appropriately handle ownership and mutability
  • Apply method call syntax with different reference types
  • Recognize the implications of different self parameter choices

Method Review

We saw these in the previous lecture.

  • We can add functions that are directly associated with structs and enums!
    • Then we could call them: road.display() or road.update_speed(25)
  • How?
    • Put them in the namespace of the type
    • make self the first argument
#[derive(Debug)]
struct Road {
    intersection_1: u32,
    intersection_2: u32,
    max_speed: u32,
}

impl Road {
    
    // constructor
    fn new(i1:u32,i2:u32,speed:u32) -> Road {
        Road {
            intersection_1: i1,
            intersection_2: i2,
            max_speed: speed,
        }
    }
    // note &self: immutable reference
    fn display(&self) {
        println!("{:?}",*self);
    }
}

// You can invoke the display method on the road instance
// or on a reference to the road instance.

fn main() {
    let mut road = Road::new(1,2,35);

    road.display();
    &road.display();
    (&road).display();
}

In C++ the syntax is different. It would be something like:

road.display();
(&road)->display();

Method with immutable self reference

Rember that self is a reference to the instance of the struct.

By default, self is an immutable reference, so we can't modify the struct.

The following will cause a compiler error.

#![allow(unused)]
fn main() {
struct Road {
    intersection_1: u32,
    intersection_2: u32,
    max_speed: u32,
}

// ERROR
impl Road {
    fn update_speed(&self, new_speed:u32) {
        self.max_speed = new_speed;
    }
}
}

Method with mutable self reference

Let's change it to a mutable reference.

#[derive(Debug)]
struct Road {
    intersection_1: u32,
    intersection_2: u32,
    max_speed: u32,
}

impl Road {
    // constructor
    fn new(i1:u32,i2:u32,speed:u32) -> Road {
        Road {
            intersection_1: i1,
            intersection_2: i2,
            max_speed: speed,
        }
    }

    // note &self: immutable reference
    fn display(&self) {
        println!("{:?}",*self);
    }

    fn update_speed(&mut self, new_speed:u32) {
        self.max_speed = new_speed;
    }
}

fn main() {
    let mut road = Road::new(1,2,35);

    road.display();
    road.update_speed(45);
    road.display();
}

Methods that take ownership of self

There are some gotchas to be aware of.

Consider the following code:

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Road {
    intersection_1: u32,
    intersection_2: u32,
    max_speed: u32,
}

impl Road {
    
    fn this_will_move(self) -> Road {   // this will take ownership of the instance of Road
        self
    }
    
    fn this_will_not_move(&self) -> &Road {  // this will _not_ take ownership of the instance of Road
        self
    }
}
}

We'll talk about ownership and borrowing in more detail later.

Methods that borrow self

Let's experiment a bit.

#![allow(unused_variables)]

#[derive(Debug)]
struct Road {
    intersection_1: u32,
    intersection_2: u32,
    max_speed: u32,
}

impl Road {
    // constructor
    fn new(i1:u32,i2:u32,speed:u32) -> Road {
        Road {
            intersection_1: i1,
            intersection_2: i2,
            max_speed: speed,
        }
    }

    // note &self: immutable reference
    fn display(&self) {
        println!("{:?}",*self);
    }

    fn update_speed(&mut self, new_speed:u32) {
        self.max_speed = new_speed;
    }

    fn this_will_move(self) -> Road {   // this will take ownership of the instance of Road
        self
    }
    
    fn this_will_not_move(&self) -> &Road {
        self
    }
}

fn main() {
  let r = Road::new(1,2,35);       // create a new instance of Road, r
  let r3 = r.this_will_not_move(); // create a new reference to r, r3

  // run the code with the following line commented, then try uncommenting it
  //let r2 = r.this_will_move();  // this will take ownership of r

  r.display();

  // r2.display();
  r3.display();
}

Methods (summary)

  • Make first parameter self
  • Various options:
    • self: move will occur
    • &self: self will be immutable reference
    • &mut self: self will be mutable reference

In-Class Poll

A1 Piazza Poll:

Select ALL statements below that are true. Multiple answers may be correct.

  • Structs can hold items of different types, similar to tuples
  • Tuple structs provide type safety by preventing confusion between different tuple types
  • Methods with &self allow you to modify the struct's fields
  • You can have multiple impl blocks for the same struct
  • Associated functions without self are commonly used as constructors
  • Enum variants can contain named struct-like data using curly braces {}
  • Methods are called using :: syntax, like rectangle::area()

In-Class Activity

Coding Exercise: Student Grade Tracker (15 minutes)

Objective: Practice defining structs and implementing methods with different types of self parameters.

Scenario: You're building a simple grade tracking system for a course. Create a Student struct and implement various methods to manage student information and grades.

You can work in teams of 2-3 students. Suggest cargo new grades-struct to create a new project and then work in VS Code.

Copy your answer into Gradescope.

Part 1: Define the Struct (3 minutes)

Create a Student struct with the following fields:

  • name: String (student's name)
  • id: u32 (student ID number)
  • grades: [f64; 5] (array of up to 5 grades)
  • num_grades: usize (number of grades added)

Part 2: Implement Methods (10 minutes)

Implement the following methods in an impl block:

  1. Constructor (associated function):

    • new(name: String, id: u32) -> Student
    • Creates a new student with grades initialized to [0.0; 5] and num_grades set to 0
  2. Immutable reference methods (&self):

    • display(&self) - debug prints the Student struct
    • average_grade(&self) -> f64 - returns average grade
    • Optional: get_letter_grade(&self) -> Option<char> - returns 'A' (≥90), 'B' (≥80), 'C' (≥70), 'D' (≥60), or 'F' (<60)
  3. Mutable reference methods (&mut self):

    • add_grade(&mut self, grade: f64) - adds a grade to the student's record

Part 3: Test Your Implementation (2 minutes)

Write a main function that creates a new student.

We provide code to:

  • Add several grades
  • Displays the student info, average and letter grade

Expected Output Example:

Student { name: "Alice Smith", id: 12345, grades: [85.5, 92.0, 78.5, 88.0, 0.0], num_grades: 4 }
Average grade: 86
Letter grade: B

Starter Code:

#![allow(unused)]

#[derive(Debug)]
struct Student {
    // TODO: Add fields
}

impl Student {
    // TODO: Implement methods
}

fn main() {
    let mut student = ...  // TODO: Create a new student

    // Add several grades
    student.add_grade(85.5);
    student.add_grade(92.0);
    student.add_grade(78.5);
    student.add_grade(88.0);

    // Display initial information
    student.display();
    println!();
}