Rust from the ground up

Structs, methods and enums

Making your own types: a struct gathers fields, impl gathers the behaviour that goes with them, and an enum says a value is one of a fixed set of shapes.

Part 6 of 12

Intermediate16 min readBack to Courses

A struct is a named shape

A struct gathers related values under one name: struct Book { title: String, pages: u32, read: bool }. Making one uses the same names — Book { title, pages, read } — and a value you already hold can be filled in with the shorthand field syntax when the variable has the field's name.

Fields are private outside their module by default, which sounds strict until you have debugged a program where anything could write anything. pub opens a field or a function on purpose, one at a time.

Privacy is what makes a struct's invariants worth anything. If every field is pub, any code anywhere can set pages to a nonsense value; with the fields private and a constructor in the same module, every value that exists has passed through the checks written there. pub is not a switch to flip once — it is a decision per field.

impl puts the behaviour next to the data

Methods live in an impl block, and their first parameter is self: &self to read the value, &mut self to change it. The dot call is then ordinary — book.summary() — and the borrowing rule from the last chapter still applies, because a method taking &mut self is exactly one writer.

An impl block can also hold functions that take no self at all, like Book::new(...). Those are constructors and helpers, called through the type rather than through a value.

Enums say one of these

An enum is a value that is one of a fixed set of options, and in Rust each option can carry its own data: enum Shape { Circle(f64), Rect { w: f64, h: f64 } }. A Circle carries its radius; a Rect carries two sides; nothing carries a field that does not apply.

The built-in Option<T> is an enum with two options — Some(value) and None — and it is how Rust says "there may be nothing here". There is no null; if a value might be missing, its type says so, and the compiler will not let you use it without dealing with the missing case.

A type with behaviour, and one that varies

struct Book {
    title: String,
    pages: u32,
    read: bool,
}

impl Book {
    fn new(title: &str, pages: u32) -> Book {
        Book { title: title.to_string(), pages, read: false }
    }

    fn finish(&mut self) {
        self.read = true;
    }

    fn summary(&self) -> String {
        let state = if self.read { "read" } else { "unread" };
        format!("{} ({}, {} pages)", self.title, state, self.pages)
    }
}

enum Shape {
    Circle(f64),
    Rect { w: f64, h: f64 },
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle(r) => 3.14159 * r * r,
        Shape::Rect { w, h } => w * h,
    }
}

fn main() {
    let mut book = Book::new("The Rust Book", 420);
    println!("{}", book.summary());
    book.finish();
    println!("{}", book.summary());

    println!("area: {:.2}", area(&Shape::Rect { w: 3.0, h: 4.5 }));
}

A reading list with a memory

Run it, then add a book of your own and a method that returns the longest title. Give the enum one more option — Audio(u32) with a length in minutes — and a match arm for it.

Output
Click "Run" to execute your code

String in the struct and &str in the constructor is the usual pairing: the constructor accepts anything text-shaped, and the struct owns a copy so it does not depend on the caller's value living on.

Deriving the traits you will miss

#[derive(Debug, Clone, PartialEq)] above a struct asks the compiler to write those implementations. Debug is the one that makes {:?} work, and it is worth adding to every type you make: a struct you cannot print is a struct you will inspect the hard way. Clone gives you .clone(), and PartialEq gives you ==.

Deriving only works when every field supports the trait. A struct holding a file handle cannot be cloned, because there is no sensible copy of an open file. That failure is information: the type is telling you which operations are honest for it.

Option is the enum you already use

Option<T> is defined in the standard library as an enum with two options: Some(T) and None. That is the whole definition. Because it is an ordinary enum, every tool from the matching chapter applies — match, if let, unwrap_or, map.

The difference from a null is where the possibility lives. With null, any reference might be null, so every use carries the question; here the question is in the type, and the compiler makes you answer it once, where the value is read. unwrap_or(default) is the short answer when a missing value has an obvious substitute.

A struct can be a name for a value

A struct does not need named fields. struct Meters(f64); is a tuple struct with one anonymous field, read back as Meters(3.5).0. The point of a one-field struct is not storage but naming: a function taking Meters cannot be handed a Feet by accident, even though both wrap an f64. That is the newtype pattern, and it is how a units bug becomes a compile error instead of a wrong number in a report.

Three struct mistakes. A constructor that takes &str and stores it needs a lifetime; store a String with .to_string() unless you mean to borrow. Changing a field through &self is a compile error — the method needs &mut self, and the binding needs mut. And struct update syntax ..other moves the remaining fields out of other, so it is no longer usable unless those fields are Copy.

Try these next

  • Add a pages field to the book struct, update the constructor, and print the value through the derived Debug.
  • Give Book a method that returns Option<&str> — the author, when it is known — and handle both cases at the call site.
  • Add a Square(f64) option to the shape enum, handle it in the match, and check that the compiler needs no _ arm.

Shapes with data in each option

Run it, then add a `Square(f64)` option and its area arm. Change `largest` to return the shape with the longest description instead of the biggest area.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does a method that should be able to change the value take as its first parameter?