Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lecture 11 - Structs: bundling data and giving it methods

A container holding three fields, with a method attached below it

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 impl block, and call them with a dot
  • Choose between &self and &mut self, and say what self alone would do
  • Write a constructor and explain why it uses :: instead of .
  • Say what pub does to a field, and why you would leave it off
  • Use a Vec as 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 holds
  • impl: 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);
}
  • mut is 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
}
  • self is 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 self means the method may change the fields
  • The variable has to be mut, or the call won't compile
  • That's why keeper had to be mut in Project 1: ask_if_greater counts 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?

ParameterThe method...After the call
&selfreads the fieldsthe value is still yours
&mut selfchanges the fieldsthe 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?

  1. total_minutes, adds up the length of every song
  2. add_song, puts one more song on the end
  3. is_empty, says whether there are any songs
  4. rename, gives the playlist a new title
  1. &self, reading
  2. &mut self, changing
  3. &self, reading
  4. &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 wroteWhat it really isWhich 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 onno 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 are snake_case
  • Don't repeat the type inside it. Customer { name, age }, not Customer { customer_name, customer_age }
  • is_ and has_ for methods that answer yes or no
  • A field called data tells the next reader nothing
  • Rust style avoids "get_" for access (customer.age() not customer.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