Lesson 9 of 22ยท16 min

Structs and Methods

So far every value in these lessons has been a number, a string, an array or a tuple. Real programs deal in things like users, orders and sensor readings, each made of several pieces of data that belong together. You could pass those pieces around as separate variables or as a tuple, but nothing would say what each one means, and nothing would stop you mixing up two of them.

A struct lets you define your own type: a name, and a list of named fields with their types. This lesson covers the three kinds of struct, how to print them, and how to attach behaviour to them with impl blocks.

Defining and creating a struct

A struct definition lists its fields and their types:

struct Account {
    owner: String,
    balance: i64,
    active: bool,
}
 
fn main() {
    let account = Account {
        owner: String::from("ferris"),
        balance: 250,
        active: true,
    };
 
    println!("{} has {}", account.owner, account.balance);
    println!("active: {}", account.active);
}
ferris has 250
active: true
  • Struct names are UpperCamelCase; field names are snake_case.
  • To create a value (an instance), write the struct name and give every field a value, in any order. Leaving a field out is a compile error.
  • Read a field with a dot: account.owner.

Each field holds its own type, so an Account owns its String. When the Account goes out of scope, so does the owner inside it.

Changing fields

To change a field, the whole instance has to be mut. Rust has no way to mark only some fields as mutable:

struct Account {
    owner: String,
    balance: i64,
}
 
fn main() {
    let mut account = Account {
        owner: String::from("ferris"),
        balance: 250,
    };
 
    account.balance -= 40;
    account.owner.push_str("_the_crab");
    println!("{} has {}", account.owner, account.balance);
}
ferris_the_crab has 210

Field init shorthand

When a variable has the same name as a field, you can write the name once:

struct Reading {
    sensor: String,
    celsius: f64,
}
 
fn reading_from(sensor: &str, celsius: f64) -> Reading {
    Reading {
        sensor: sensor.to_string(),
        celsius,
    }
}
 
fn main() {
    let r = reading_from("attic", 31.5);
    println!("{}: {}", r.sensor, r.celsius);
}
attic: 31.5

celsius on its own is short for celsius: celsius. The sensor field still needs the long form, because the parameter is a &str and the field is a String.

Struct update syntax

To build an instance that is mostly a copy of another, list the fields that differ and finish with ..other to take the rest from other:

struct Settings {
    volume: u8,
    brightness: u8,
    dark_mode: bool,
}
 
fn main() {
    let defaults = Settings {
        volume: 50,
        brightness: 80,
        dark_mode: false,
    };
    let night = Settings {
        brightness: 20,
        dark_mode: true,
        ..defaults
    };
 
    println!("{} {} {}", night.volume, night.brightness, night.dark_mode);
    println!("{} {}", defaults.brightness, defaults.dark_mode);
}
50 20 true
80 false

..defaults copies or moves the remaining fields, following the ownership rules: here they are all Copy, so defaults is still usable. Had it taken a String field from defaults, that field would have moved out, and using it from defaults afterwards would be an error.

Tuple structs

A tuple struct has a name but its fields don't. You read them by position, like a tuple:

struct Rgb(u8, u8, u8);
 
fn main() {
    let coral = Rgb(255, 127, 80);
    println!("red {} green {} blue {}", coral.0, coral.1, coral.2);
 
    let Rgb(r, g, b) = coral;
    println!("#{r:02x}{g:02x}{b:02x}");
}
red 255 green 127 blue 80
#ff7f50

You create one by calling its name like a function, and you can destructure it with a pattern that repeats the name. Tuple structs suit small values where the order of the fields is obvious and names would add nothing.

A common use is the newtype: a tuple struct with one field that gives an existing type a new meaning.

struct Meters(f64);
struct Seconds(f64);
 
fn speed(distance: Meters, time: Seconds) -> f64 {
    distance.0 / time.0
}
 
fn main() {
    let d = Meters(100.0);
    let t = Seconds(9.58);
    println!("{:.2} m/s", speed(d, t));
}
10.44 m/s

Both wrap an f64, but they are different types: calling speed(Seconds(9.58), Meters(100.0)) doesn't compile. Two tuple structs with the same field types are never interchangeable.

Unit structs

A unit struct has no fields at all:

struct Maintenance;
 
fn main() {
    let _mode = Maintenance;
    println!("{} bytes", std::mem::size_of::<Maintenance>());
}
0 bytes

It holds no data and takes no memory. What it gives you is a type, and a type can have functions and methods attached, as the next sections show. Unit structs are handy as markers, or as a place to group behaviour that needs no state.

Printing structs with #[derive(Debug)]

println!("{}", account) doesn't work for a struct you wrote, and neither does {:?} at first: Rust doesn't guess how your type should be printed. For debug output, ask the compiler to write the code for you by putting #[derive(Debug)] above the definition:

#[derive(Debug)]
struct Ticket {
    id: u32,
    title: String,
    tags: Vec<String>,
}
 
#[derive(Debug)]
struct Version(u8, u8, u8);
 
fn main() {
    let ticket = Ticket {
        id: 42,
        title: String::from("Fix login"),
        tags: vec![String::from("bug"), String::from("auth")],
    };
    let version = Version(1, 4, 0);
 
    println!("{ticket:?}");
    println!("{version:?}");
    println!("{ticket:#?}");
 
    println!("#{} {}", ticket.id, ticket.title);
    println!("{} tags", ticket.tags.len());
    println!("v{}.{}.{}", version.0, version.1, version.2);
}
Ticket { id: 42, title: "Fix login", tags: ["bug", "auth"] }
Version(1, 4, 0)
Ticket {
    id: 42,
    title: "Fix login",
    tags: [
        "bug",
        "auth",
    ],
}
#42 Fix login
2 tags
v1.4.0

derive generates an implementation of the Debug trait, which is what {:?} and {:#?} use. The last two lines read the fields by hand, because the compiler warns about fields that nothing reads, and it doesn't count the derived Debug as reading them. Traits get their own lesson; for now it is enough that #[derive(Debug)] makes a struct printable for debugging, as long as every field is printable too. {:#?} spreads the output over several lines, which helps with bigger values.

The dbg! macro is another quick way to look at a value. It prints to standard error with the file and line number, and returns the value you gave it, so you can wrap an expression without changing what the code does:

#[derive(Debug)]
struct Version(u8, u8, u8);
 
fn main() {
    let v = dbg!(Version(2, 0, 1));
    println!("v{}.{}.{}", v.0, v.1, v.2);
}
v2.0.1

Before that line, standard error shows where dbg! was called, the expression and its value, printed like {:#?}:

[src/main.rs:5:13] Version(2, 0, 1) = Version(
    2,
    0,
    1,
)

Methods

Functions that belong to a type go in an impl block. A method is one whose first parameter is self, the instance it is called on:

struct Timer {
    label: String,
    seconds: u32,
}
 
impl Timer {
    fn describe(&self) -> String {
        format!("{} ({}s left)", self.label, self.seconds)
    }
 
    fn is_done(&self) -> bool {
        self.seconds == 0
    }
 
    fn tick(&mut self) {
        if self.seconds > 0 {
            self.seconds -= 1;
        }
    }
}
 
fn main() {
    let mut tea = Timer {
        label: String::from("tea"),
        seconds: 2,
    };
 
    println!("{}", tea.describe());
    tea.tick();
    tea.tick();
    tea.tick();
    println!("{} done: {}", tea.describe(), tea.is_done());
}
tea (2s left)
tea (0s left) done: true

You call a method with a dot, tea.tick(), and Rust passes tea as self. The kind of self says what the method needs from the instance, using the same ownership rules as any other parameter:

First parameterShort forThe method can...
&selfself: &Selfread the fields
&mut selfself: &mut Selfread and change the fields
selfself: Selftake ownership of the whole instance

Self is an alias for the type the impl block is for, here Timer. Pick the weakest one that works: &self for anything that only looks, &mut self for anything that changes state. Calling a &mut self method needs a mut variable, just like taking &mut of it by hand.

When you call a method, Rust adds the & or &mut for you, so you write tea.tick() rather than (&mut tea).tick().

Methods that take self

A method that takes self by value consumes the instance. That suits methods that turn a value into something else, after which the original shouldn't be used:

struct Draft {
    text: String,
}
 
struct Published {
    text: String,
    url: String,
}
 
impl Draft {
    fn publish(self, slug: &str) -> Published {
        Published {
            text: self.text,
            url: format!("/posts/{slug}"),
        }
    }
}
 
fn main() {
    let draft = Draft {
        text: String::from("Hello!"),
    };
    let post = draft.publish("hello");
    println!("{} at {}", post.text, post.url);
}
Hello! at /posts/hello

After draft.publish(...), draft has moved and can't be used again, so there is no way to accidentally edit a draft that has already been published.

Associated functions

A function in an impl block without a self parameter is an associated function. It belongs to the type rather than to an instance, and you call it with :: on the type name. String::from is one you have been using all along.

The most common associated function is a constructor. Rust has no special constructor syntax; by convention, a function named new that returns Self plays that role:

#[derive(Debug)]
struct Temperature {
    celsius: f64,
}
 
impl Temperature {
    fn new(celsius: f64) -> Self {
        Self { celsius }
    }
 
    fn from_fahrenheit(fahrenheit: f64) -> Self {
        Self::new((fahrenheit - 32.0) / 1.8)
    }
 
    fn freezing() -> Self {
        Self { celsius: 0.0 }
    }
 
    fn is_below(&self, other: &Temperature) -> bool {
        self.celsius < other.celsius
    }
}
 
fn main() {
    let today = Temperature::new(21.5);
    let oven = Temperature::from_fahrenheit(356.0);
    let ice = Temperature::freezing();
 
    println!("{today:?} {oven:?}");
    println!("{}", ice.is_below(&today));
}
Temperature { celsius: 21.5 } Temperature { celsius: 180.0 }
true

A type can have as many associated functions as it needs, each with a name that says how the value is made. Inside the impl, Self { ... } builds an instance and Self::new(...) calls another associated function.

Constructors earn their keep once a struct has fields the caller shouldn't have to think about, such as a counter that always starts at zero or a value that must be checked first. They also matter for privacy: in a module, fields are private unless marked pub, so code outside the module can't write Temperature { celsius: 21.5 } at all and has to go through the functions you make public:

mod shop {
    pub struct Order {
        pub id: u32,
        total_cents: u64,
    }
 
    impl Order {
        pub fn new(id: u32) -> Self {
            Self { id, total_cents: 0 }
        }
 
        pub fn add(&mut self, cents: u64) {
            self.total_cents += cents;
        }
 
        pub fn total(&self) -> u64 {
            self.total_cents
        }
    }
}
 
fn main() {
    let mut order = shop::Order::new(7);
    order.add(1250);
    order.add(399);
    println!("order {} costs {} cents", order.id, order.total());
}
order 7 costs 1649 cents

main can read order.id, which is pub, but not order.total_cents. The only way to change the total is add, so the Order type decides what a valid order looks like.

Unit structs can have associated functions and methods too, which is how a field-less type ends up with behaviour:

struct Units;
 
impl Units {
    fn km_to_miles(km: f64) -> f64 {
        km * 0.621371
    }
}
 
fn main() {
    println!("{:.1}", Units::km_to_miles(42.195));
}
26.2

A type can have several impl blocks, and methods and associated functions can be mixed freely in each. Most code keeps them in one block per type.

Common mistakes

Printing a struct without Debug

struct Point3 {
    x: i32,
    y: i32,
    z: i32,
}
 
fn main() {
    let p = Point3 { x: 1, y: 2, z: 3 };
    println!("{p:?}");
}

The compiler tells you exactly what to add. Put #[derive(Debug)] on the line above struct Point3.

Leaving out a field

struct User {
    name: String,
    email: String,
    verified: bool,
}
 
fn main() {
    let user = User {
        name: String::from("ana"),
        email: String::from("ana@example.com"),
    };
    println!("{} {} {}", user.name, user.email, user.verified);
}

Every field needs a value when you create an instance; there are no implicit defaults. Give it one, use struct update syntax to take it from another instance, or write a constructor that fills it in.

Changing a field of an immutable instance

struct Player {
    score: u32,
}
 
impl Player {
    fn add_points(&mut self, points: u32) {
        self.score += points;
    }
}
 
fn main() {
    let player = Player { score: 0 };
    player.add_points(10);
    println!("{}", player.score);
}

add_points takes &mut self, so calling it borrows player mutably, which needs let mut player. Assigning a field directly, player.score = 10, fails the same way (with E0594).

Calling an associated function with a dot

struct Grid {
    cells: Vec<u8>,
}
 
impl Grid {
    fn empty(size: usize) -> Self {
        Self {
            cells: vec![0; size],
        }
    }
}
 
fn main() {
    let grid = Grid::empty(4);
    let other = grid.empty(9);
    println!("{} {}", grid.cells.len(), other.cells.len());
}

empty has no self parameter, so it can't be called on an instance. Call it on the type, Grid::empty(9), or add a self parameter if it really does need an instance.

Using a value after a method took self

struct Draft {
    text: String,
}
 
impl Draft {
    fn into_text(self) -> String {
        self.text
    }
}
 
fn main() {
    let draft = Draft {
        text: String::from("notes"),
    };
    let text = draft.into_text();
    println!("{text} / {}", draft.text);
}

The error points at the call that moved draft and at the self parameter that took it. Use the value you got back, or change the method to take &self if the caller needs to keep the instance.

Summary

  • A struct is your own type made of named fields. Create one with Name { field: value, ... } and read fields with a dot. Changing a field needs the whole instance to be mut.
  • Name { field, ..other } uses the field init shorthand and fills the rest from other.
  • Tuple structs, struct Rgb(u8, u8, u8);, have positional fields (.0, .1). A one-field tuple struct makes a distinct newtype.
  • Unit structs, struct Marker;, have no fields and take no memory.
  • #[derive(Debug)] makes a struct printable with {:?}, {:#?} and dbg!.
  • An impl block holds methods (first parameter &self, &mut self or self, called with a dot) and associated functions (no self, called with Type::name).
  • By convention a constructor is an associated function called new that returns Self. Private fields plus public constructors and methods let a type control what a valid value looks like.

Practise what you read

Challenges that use the ideas from this lesson.