Lecture 11 - Structs: bundling data and giving it methods
Announcements
Learning objectives
By the end of today, you should be able to:
- Define a struct to group related data under one name
- Use a tuple struct when the fields don't need names
- Write methods in an
implblock, and call them with a dot - Choose between
&selfand&mut self, and say whatselfalone would do - Write a constructor and explain why it uses
::instead of. - Say what
pubdoes to a field, and why you would leave it off - Use a
Vecas a list that can grow
Part 1: You've already been using one
Three weeks of keeper.something()
From Project 1:
let mut keeper = dealer.deal();
if keeper.ask_if_greater(50) {
// the secret is bigger than 50
}
println!("Questions asked: {}", keeper.questions_asked());
keeper is not an i32, a bool, or an array. It is a custom type we wrote.
What secret_keeper.rs actually says
Abridged a little, but this is the shape of it:
struct SecretKeeper { secret: u32, questions: u32, } impl SecretKeeper { fn questions_asked(&self) -> u32 { self.questions } fn ask_if_greater(&mut self, guess: u32) -> bool { self.questions += 1; guess < self.secret } } fn main() { let mut keeper = SecretKeeper { secret: 42, questions: 0 }; println!("{}", keeper.ask_if_greater(50)); println!("Questions asked: {}", keeper.questions_asked()); }
Two halves:
struct: the data it holdsimpl: what you can ask it to do
The problem structs solve
Say you're tracking a customer. You could use loose variables:
let customer_name = "Alice Smith";
let customer_age = 25;
let customer_state = "NY";
let customer_member = true;
This works, but it's not ideal.
What sucks about this?
Three things wrong with loose variables
let customer_name = "Alice Smith";
let customer_age = 25;
let customer_state = "NY";
let customer_member = true;
Nothing holds them together. You'd need to pass them as four arguments into a function, and the compiler might not notice if you mix them up.
A second customer is a mess. 4 more vars, nothing linking Alice's name to Alice's age.
The logic lives somewhere else. (We'll see more of that in a minute)
Group them into one type
#![allow(dead_code)] struct Customer { name: String, age: u32, state: String, member: bool, } fn main() { let alice = Customer { name: "Alice Smith".to_string(), age: 25, state: "NY".to_string(), member: true, }; println!("{} is {}", alice.name, alice.age); }
struct Customer { ... }defines a new type, once- The block with the values makes one of them
- Access a field with a dot:
alice.age(like tuple notation!)
The loose version said "NY" (&str) but the field needs "NY".to_string()(String) because the compiler wants to know where the text lives and for how long. Like before... more info later in the term.
Changing fields and printing
#![allow(dead_code)] #[derive(Debug)] struct Customer { name: String, age: u32, state: String, member: bool, } fn main() { let mut alice = Customer { name: "Alice Smith".to_string(), age: 25, state: "NY".to_string(), member: true, }; alice.age = 26; // needs `mut` on alice println!("{:?}", alice); }
mutis on the whole struct, not on one field. Either all of it can change or none of it can#[derive(Debug)]gives you{:?}, the same one you used for arrays
Tuple structs: when we don't need field names
#![allow(dead_code)] #[derive(Debug)] struct Point3D(f64, f64, f64); #[derive(Debug)] struct BoxOfDonuts(u32); fn main() { let corner = Point3D(3.0, 4.0, 5.0); let dozen = BoxOfDonuts(12); println!("x is {}, y is {}", corner.0, corner.1); println!("{:?}", dozen); }
Fields by position, not by name.
BoxOfDonuts(12) is not a u32, so you can't hand it to a function expecting a count of something else. That's the point of it.
Part 2: Methods
Functions that belong to a type
Without methods, every function takes the struct as an argument:
fn area(rect: &Rectangle) -> f64 { ... }
fn perimeter(rect: &Rectangle) -> f64 { ... }
let a = area(&rect);
With an impl block, they hang off the type itself:
impl Rectangle {
fn area(&self) -> f64 { ... }
fn perimeter(&self) -> f64 { ... }
}
let a = rect.area();
The difference is that now they travel with the type, and you find them by typing rect.
What that buys you: adding a shape
Say a circle turns up. With one function for everything, you need a way to tell the shapes apart, and every shape's fields end up on one struct:
struct Shape {
kind: String,
width: f64,
height: f64,
radius: f64, // meaningless unless kind is "circle"
}
fn area(shape: &Shape) -> f64 {
if shape.kind == "rectangle" {
shape.width * shape.height
} else if shape.kind == "circle" {
3.14159 * shape.radius * shape.radius
} else {
0.0 // and now what?
}
}
A triangle adds a field nobody else uses, and it edits area, and perimeter, and everything else that branches on kind. Nothing stops you writing Shape { kind: "circle", width: 10.0, radius: 0.0 } either.
The way out of that is one function per shape:
fn area_rectangle(r: &Rectangle) -> f64 { ... }
fn area_circle(c: &Circle) -> f64 { ... }
fn perimeter_rectangle(r: &Rectangle) -> f64 { ... }
fn perimeter_circle(c: &Circle) -> f64 { ... }
Now the compiler checks the types for you, but you are the one keeping the names straight, and the triangle is two more functions.
With methods, they can all just be called area:
struct Rectangle { width: f64, height: f64 } struct Circle { radius: f64 } impl Rectangle { fn area(&self) -> f64 { self.width * self.height } } impl Circle { fn area(&self) -> f64 { 3.14159 * self.radius * self.radius } } fn main() { let rect = Rectangle { width: 10.0, height: 5.0 }; let circle = Circle { radius: 3.0 }; println!("{:.2} {:.2}", rect.area(), circle.area()); }
Two types, same method name, and no clash: rect. can only reach Rectangle's. The triangle is one new struct and one new impl, and no other code needs to change.
&self: the method just reads
struct Rectangle { width: f64, height: f64, } impl Rectangle { fn area(&self) -> f64 { self.width * self.height } } fn main() { let rect = Rectangle { width: 10.0, height: 5.0 }; println!("{}", rect.area()); // like calling area(&rect) println!("{}", rect.width); // rect is still fine }
selfis the value you called the method on. Here,rect- The
&means the method borrows it: a look, not a handover - Most methods are
&self
&mut self: the method changes something
struct Rectangle { width: f64, height: f64, } impl Rectangle { fn scale(&mut self, factor: f64) { self.width *= factor; self.height *= factor; } } fn main() { let mut rect = Rectangle { width: 10.0, height: 5.0 }; rect.scale(2.0); println!("{} by {}", rect.width, rect.height); // 20 by 10 }
&mut selfmeans the method may change the fields- The variable has to be
mut, or the call won't compile - That's why
keeperhad to bemutin Project 1:ask_if_greatercounts the question
There is a third one, but you don't want it yet
impl Rectangle {
fn into_area(self) -> f64 { // no &
self.width * self.height
}
}
Plain self takes the whole struct with it. After you call the method, the variable is gone, and using it again is a compile error.
Useful for turning one thing into another. Rare. For now, write &self or &mut self.
Which one do I write?
| Parameter | The method... | After the call |
|---|---|---|
&self | reads the fields | the value is still yours |
&mut self | changes the fields | the value is still yours, and changed |
Plain self is the third option: the value is consumed, and you'll rarely write it this term.
Start with &self. Reach for &mut self only when the method actually changes a field. If the compiler wants more, it will say so.
Think-pair-share: &self or &mut self?
You're writing a Playlist. Which does each method take?
total_minutes, adds up the length of every songadd_song, puts one more song on the endis_empty, says whether there are any songsrename, gives the playlist a new title
&self, reading&mut self, changing&self, reading&mut self, changing
The question is always the same one: does this change a field?
Constructors: building one without spelling it out
#![allow(dead_code)] #[derive(Debug)] struct Rectangle { width: f64, height: f64, } impl Rectangle { fn new(width: f64, height: f64) -> Rectangle { Rectangle { width, height } // short for width: width, height: height } fn square(side: f64) -> Rectangle { Rectangle { width: side, height: side } } } fn main() { let rect = Rectangle::new(10.0, 5.0); let unit = Rectangle::square(1.0); println!("{:?} {:?}", rect, unit); }
No self parameter, because there's nothing to call it on yet. You're making the thing.
So it's Rectangle::new(...) with two colons, not rect.new(...) with a dot.
A dot means "I already have one of these." Two colons means "make me one" or "this belongs to the type, not to a value."
new is not a keyword. It's a convention, and a strong one, but nothing special happens when you use the name. Rectangle::square is a constructor too.
Vec: an array that can grow
An array is a fixed size forever. A Vec is a list that grows:
fn main() { let mut grades = Vec::new(); // the same :: you just saw grades.push(85.0); grades.push(92.0); println!("{} grades", grades.len()); println!("first is {}", grades[0]); // index it like an array println!("{:?}", grades); }
Vec is a struct with methods, exactly like the ones you're writing today.
How it manages memory is a later lecture, and in Project 2 you'll build your own.
let grades = Vec::new(); on its own gives you "type annotations needed". Rust works out what the Vec holds from the first thing you push, so it needs to see one.
You've been calling methods for weeks
cards.sort(); // Friday
scores.iter(); // Activity 8
name.len();
text.to_string();
Every one of those is a method on a type someone else wrote:
| What you wrote | What it really is | Which self |
|---|---|---|
cards.sort() | sort(&mut cards) | &mut self, it reorders the cards |
name.len() | len(&name) | &self, just reading |
Vec::new() | nothing to call it on | no self at all |
Method or plain function?
A method goes on the type when it needs the data inside it.
impl Customer {
fn is_adult(&self) -> bool { self.age >= 18 } // needs the customer
}
fn tax_rate(state: &str) -> f64 { ... } // doesn't
fn average_age(customers: &Vec<Customer>) -> f64 { ... } // needs multiple
When writing a struct, ask yourself "what would someone expect this type to have and do?"
Naming for structs and methods
- Types are
CamelCase, fields and methods aresnake_case - Don't repeat the type inside it.
Customer { name, age }, notCustomer { customer_name, customer_age } is_andhas_for methods that answer yes or no- A field called
datatells the next reader nothing - Rust style avoids "get_" for access (
customer.age()notcustomer.get_age())
The other reason: there is only one place to get it wrong
Say a BankAccount must never go negative, and other functions want to withdraw from it.
If you let them reach in, this has to be written every time:
if amount > account.balance {
println!("not enough funds");
}
account.balance -= amount;
Put it in a method, and there is exactly one:
impl BankAccount {
fn withdraw(&mut self, amount: i32) {
if amount > self.balance {
println!("not enough funds");
}
self.balance -= amount;
}
}
Same code, but the difference is how many places you need to keep in sync.
So what stops you reaching in anyway?
You have written this:
println!("Questions asked: {}", keeper.questions_asked());
SecretKeeper has a field called questions. Why did you call a method instead of just writing keeper.questions?
Because you are not allowed to
println!("{}", keeper.questions);
error[E0616]: field `questions` of struct `SecretKeeper` is private
--> src/main.rs:7:22
|
7 | println!("{}", keeper.questions);
| ^^^^^^^^^ private field
In secret_keeper.rs:
pub struct SecretKeeper {
source: Source,
questions: u32,
already_used: Vec<u32>,
}
impl SecretKeeper {
pub fn questions_asked(&self) -> u32 {
self.questions
}
}
pub means other files can see this. The struct is pub, and so is the method. The fields are not.
So from your code you can ask how many questions you have asked. You cannot set the counter to zero.
Let's build one together: a grade tracker
struct Student { name: String, grades: Vec<f64>, } impl Student { fn new(name: String) -> Student { Student { name, grades: Vec::new(), } } fn add_grade(&mut self, grade: f64) { self.grades.push(grade); } fn average(&self) -> f64 { if self.grades.len() == 0 { return 0.0; } let mut total = 0.0; for grade in &self.grades { total += grade; } total / self.grades.len() as f64 } } fn main() { let mut alice = Student::new("Alice".to_string()); alice.add_grade(85.0); alice.add_grade(92.0); println!("{}'s average: {:.1}", alice.name, alice.average()); }
Activity Time
See Activity 11