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:
- Chapter 5.3: Method Syntax - Review
- Chapter 4.2: References and Borrowing - Focus on method calls
Pre-lecture Reflections
Before class, consider these questions:
- What are the implications of using
selfvs&selfvs&mut selfin method signatures? - How does method call syntax relate to function call syntax with explicit references?
- When would you design a method to take ownership of
self? - How do method calls interact with Rust's borrowing rules?
- What are the trade-offs between different
selfparameter types?
Learning Objectives
By the end of this module, you should be able to:
- Distinguish between
self,&self, and&mut selfparameter 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
selfparameter 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()orroad.update_speed(25)
- Then we could call them:
- How?
- Put them in the namespace of the type
- make
selfthe 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
&selfallow you to modify the struct's fields -
You can have multiple
implblocks for the same struct -
Associated functions without
selfare commonly used as constructors -
Enum variants can contain named struct-like data using curly braces
{} -
Methods are called using
::syntax, likerectangle::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:
-
Constructor (associated function):
new(name: String, id: u32) -> Student- Creates a new student with grades initialized to
[0.0; 5]andnum_gradesset to 0
-
Immutable reference methods (
&self):display(&self)- debug prints the Student structaverage_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)
-
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!(); }