Structs and methods

Rust · Essentials · 12 min

Structs are how you model your domain, and impl blocks are where most Rust code lives.

A struct groups named fields into a new type. Instantiate it by naming every field; the field init shorthand lets you write User { name, active: true } when a variable has the same name as the field. Struct update syntax User { active: false, ..other } fills the rest from another value (moving any non-Copy fields out of it).

Rust also has tuple structs struct Meters(f64);, useful as lightweight wrappers (the newtype pattern) that stop you from mixing up a Meters and a Seconds, and unit structs struct Marker; with no fields.

Behaviour goes in an impl block. The first parameter decides how a method uses the value:

  • &self reads it (most methods),
  • &mut self modifies it,
  • self consumes it, typically to transform it into something else.

Functions in an impl block without a self parameter are associated functions, called with Type::name(). By convention a constructor is fn new(...) -> Self. Self is an alias for the type being implemented.

Fields are private outside the module that defines the struct unless marked pub. Keeping fields private and exposing methods lets you maintain invariants, such as "width is never negative".

#[derive(Debug)] above the struct lets you print it with {:?} or, pretty-printed, {:#?}. You will meet more derivable traits later.

Method calls auto-borrow: rect.area() turns into Rectangle::area(&rect) automatically, so you rarely write & at a call site.

Example

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn new(width: u32, height: u32) -> Self {   // associated function
        Self { width, height }                   // field init shorthand
    }
    fn square(size: u32) -> Self {
        Self::new(size, size)
    }
    fn area(&self) -> u32 { self.width * self.height }
    fn scale(&mut self, k: u32) { self.width *= k; self.height *= k; }
    fn into_tuple(self) -> (u32, u32) { (self.width, self.height) }
}

fn main() {
    let mut r = Rectangle::new(3, 4);
    r.scale(2);
    println!("{} {:?}", r.area(), Rectangle::square(2)); // 48 Rectangle { width: 2, height: 2 }
    let (w, h) = r.into_tuple();                         // r is consumed
    println!("{w}x{h}");                                 // 6x8
}

Remember

  • Methods take &self, &mut self or self depending on how they use the value
  • Associated functions like new have no self and are called with Type::new
  • Tuple structs make cheap newtypes; #[derive(Debug)] enables {:?}

Watch out for

  • Taking self by value in a method that only reads, which consumes the caller's struct.

Check yourself

  1. A method should modify the struct but the caller must keep using it. Which receiver fits?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/book/ch05-03-method-syntax.html