Structs: grouping data

A struct is a type made of named fields, each with its own type. You define the type once and then create values of it with the same braces, naming every field:

struct Reading {
    meter: String,
    kwh: u32,
    night: bool,
}

fn main() {
    let mut reading = Reading {
        meter: String::from("M-30004"),
        kwh: 350,
        night: false,
    };
    reading.kwh += 12;
    println!("{} used {} kWh (night: {})", reading.meter, reading.kwh, reading.night);
}
M-30004 used 362 kWh (night: false)

Mutability belongs to the binding, not to single fields: because reading is declared with let mut, every field can change; without mut, none can (see Variables, Types and Mutability). There is no way to mark one field as mutable and the others not. The struct owns its fields: when reading goes out of scope, its String is freed with it (see Ownership).

Leaving a field out is not allowed. C would fill a missing member with zero or leave it as whatever was in memory; Python would only notice when the attribute is read. Rust stops at compile time:

struct Reading {
    meter: String,
    kwh: u32,
    night: bool,
}

fn main() {
    let reading = Reading {
        meter: String::from("M-30004"),
        kwh: 350,
    };
    println!("{} {} {}", reading.meter, reading.kwh, reading.night);
}
example.rs:8:19: error[E0063]: missing field `night` in initializer of `Reading`: missing `night`

Field init shorthand and struct update syntax

Two small conveniences save typing. When a variable has the same name as the field, write the name once: kwh instead of kwh: kwh. And ..other at the end of a struct expression takes all fields you did not name from another value of the same type:

#[derive(Debug, Clone)]
struct Reading {
    meter: String,
    kwh: u32,
    night: bool,
}

fn day_reading(meter: &str, kwh: u32) -> Reading {
    Reading {
        meter: meter.to_string(),
        kwh, // shorthand for kwh: kwh
        night: false,
    }
}

fn main() {
    let day = day_reading("M-30007", 412);
    let night = Reading {
        night: true,
        kwh: 96,
        ..day.clone() // the remaining fields come from a copy of day
    };
    println!("{day:?}");
    println!("{night:#?}");
    let label = if night.night { "night" } else { "day" };
    println!("{} used {} kWh at {label}", night.meter, night.kwh);
}
Reading { meter: "M-30007", kwh: 412, night: false }
Reading {
    meter: "M-30007",
    kwh: 96,
    night: true,
}
M-30007 used 96 kWh at night

The update syntax moves the remaining fields out of the source. Without .clone(), the String in meter would move from day into night, and day could no longer be printed as a whole. {:?} prints the Debug form on one line, {:#?} spreads it over several lines, which helps with larger values.

Printing a struct

A new struct cannot be printed at all, not even for debugging. Rust does not invent a format for your type; the compiler says what is missing and how to add it:

struct Reading {
    meter: String,
    kwh: u32,
}

fn main() {
    let reading = Reading { meter: String::from("M-30004"), kwh: 350 };
    println!("{reading:?}");
}
example.rs:8:15: error[E0277]: `Reading` doesn't implement `Debug`: `Reading` cannot be formatted using `{:?}` because it doesn't implement `Debug`
error[E0277]: `Reading` doesn't implement `Debug`
 --> example.rs:8:15
  |
8 |     println!("{reading:?}");
  |               ^^^^^^^^^^^ `Reading` cannot be formatted using `{:?}` because it doesn't implement `Debug`
  |
  = help: the trait `Debug` is not implemented for `Reading`
help: consider annotating `Reading` with `#[derive(Debug)]`
  |
1 + #[derive(Debug)]
2 | struct Reading {
  |

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0277`.

The help: lines are the fix, down to the line where it goes. The section on derive below shows what else you can ask for in the same way.

Tuple structs and the newtype pattern

A tuple struct has fields without names, reached by position: .0, .1 and so on. The most common use has a single field and wraps a plain number in a type of its own, the newtype pattern. A Watts and a KilowattHours both hold a u32, but the compiler treats them as different types, so a power value can never be passed where an energy value is expected:

struct Watts(u32);
struct KilowattHours(u32);

fn cost_cents(energy: KilowattHours, price_per_kwh: u32) -> u32 {
    energy.0 * price_per_kwh / 100
}

fn main() {
    let power = Watts(1234);
    println!("{}", cost_cents(power, 2700));
}
example.rs:10:31: error[E0308]: mismatched types: expected `KilowattHours`, found `Watts`

With plain u32 parameters this mistake compiles in every language and produces a wrong bill. With newtypes it is a type error, and it costs nothing at run time: a Watts is stored exactly like a u32. The correct call, and a pattern that takes the number out again:

struct Watts(u32);
struct KilowattHours(u32);

fn cost_cents(energy: KilowattHours, price_per_kwh: u32) -> u32 {
    // price in ten-thousandths of a euro: 2700 = €0.2700 per kWh
    energy.0 * price_per_kwh / 100
}

fn main() {
    let power = Watts(1234);
    let energy = KilowattHours(350);
    let Watts(w) = power; // destructuring takes the number out again
    println!("power now: {w} W");
    println!("cost: {} cents", cost_cents(energy, 2700));
}
power now: 1234 W
cost: 9450 cents

Money is kept in whole units here (ten-thousandths of a euro per kWh, cents for the result) rather than in floating point; the reason is on Variables, Types and Mutability. A struct with no fields at all, struct Online;, is called a unit struct; it holds no data and is mostly used as a marker type to implement a trait on (see Traits and Generics).

Methods and impl blocks

Rust keeps data and behavior apart: the struct definition lists only fields, and functions that belong to the type go in an impl block (impl Meter { … }). A type can have several impl blocks, even in different files of the same crate. Inside the block, Self is a short name for the type itself.

struct Meter {
    id: String,
    total_wh: u64,
}

impl Meter {
    // An associated function: no self, called as Meter::new(...)
    fn new(id: &str) -> Self {
        Self { id: id.to_string(), total_wh: 0 }
    }

    // &self: reads the meter
    fn total_kwh(&self) -> u64 {
        self.total_wh / 1000
    }

    // &mut self: changes the meter
    fn record(&mut self, wh: u64) {
        self.total_wh += wh;
    }

    // self: takes the meter and gives back its id
    fn into_id(self) -> String {
        self.id
    }
}

fn main() {
    let mut meter = Meter::new("M-30004");
    meter.record(350_000);
    meter.record(12_400);
    println!("{} kWh", meter.total_kwh());
    let id = meter.into_id();
    println!("meter {id} retired");
}
362 kWh
meter M-30004 retired

&self, &mut self and self

The first parameter of a method is its receiver, and its form is a promise about what the method does with the value. The three forms are the three ways to use any value in Rust, described on Ownership and Borrowing and References:

Receiver Means The caller needs Typical names
&self borrows the value to read it any binding total_kwh, is_empty, len
&mut self borrows the value to change it a let mut binding record, push, clear
self takes ownership; the value is used up a value it may give away into_id, into_bytes, builders' build

The compiler holds you to the promise. Calling a &mut self method on a binding that is not mut fails, and the help line shows where to add it:

struct Meter {
    id: String,
    total_wh: u64,
}

impl Meter {
    fn new(id: &str) -> Self {
        Self { id: id.to_string(), total_wh: 0 }
    }

    fn record(&mut self, wh: u64) {
        self.total_wh += wh;
    }
}

fn main() {
    let meter = Meter::new("M-30004");
    meter.record(350_000);
    println!("{}: {} Wh", meter.id, meter.total_wh);
}
example.rs:18:5: error[E0596]: cannot borrow `meter` as mutable, as it is not declared as mutable: cannot borrow as mutable

And after a method that takes self, the value is gone. The full diagnostic even points at the method signature that took it:

error[E0382]: borrow of moved value: `meter`
  --> example.rs:15:29
   |
13 |     let meter = Meter { id: String::from("M-30004"), total_wh: 362_400 };
   |         ----- move occurs because `meter` has type `Meter`, which does not implement the `Copy` trait
14 |     let id = meter.into_id();
   |                    --------- `meter` moved due to this method call
15 |     println!("{id}: {} Wh", meter.total_wh);
   |                             ^^^^^^^^^^^^^^ value borrowed here after move
   |
note: `Meter::into_id` takes ownership of the receiver `self`, which moves `meter`
  --> example.rs:7:16
   |
 7 |     fn into_id(self) -> String {
   |                ^^^^

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0382`.

Associated functions and constructors

A function in an impl block that has no self parameter is an associated function. It belongs to the type, not to a value, and you call it with the type name and ::, like Meter::new("M-30004") or String::from("…"). Rust has no constructors built into the language; new is only a naming convention for the associated function that makes a value. When creating a value can fail, the convention is that the function says so in its return type, for example Option<Self> or Result<Self, E> (see making invalid values impossible below).

Enums: one of several variants

A Rust enum defines a type by listing its possible variants. A value of the type is always exactly one of them. The simplest enums just name the choices, like Kestrel's four tariff types:

#[derive(Debug, Clone, Copy, PartialEq)]
enum Tariff {
    Fixed,
    DayNight,
    Dynamic,
    Variable,
}

impl Tariff {
    fn from_name(name: &str) -> Option<Tariff> {
        match name {
            "fixed" => Some(Tariff::Fixed),
            "day_night" => Some(Tariff::DayNight),
            "dynamic" => Some(Tariff::Dynamic),
            "variable" => Some(Tariff::Variable),
            _ => None,
        }
    }

    fn has_hourly_prices(self) -> bool {
        self == Tariff::Dynamic
    }
}

fn main() {
    for name in ["fixed", "dynamic", "weekend"] {
        match Tariff::from_name(name) {
            Some(tariff) => println!("{name}: {tariff:?}, hourly: {}", tariff.has_hourly_prices()),
            None => println!("{name}: unknown tariff"),
        }
    }
}
fixed: Fixed, hourly: false
dynamic: Dynamic, hourly: true
weekend: unknown tariff

Enums get impl blocks and methods just like structs. Because Tariff derives Copy (it is a single byte), has_hourly_prices can take self by value without using anything up.

Variants that carry data

Each variant can carry its own data: named fields like a struct, unnamed fields like a tuple struct, or nothing. This is what makes Rust enums so much more useful than C's. A parsed line of a P1 telegram (see parsing a telegram) is a power reading, a meter id, a CRC, or something the parser skips, and each case has different data:

// One line of a P1 telegram, after parsing.
enum TelegramLine {
    Power { watts: u32 },           // 1-0:1.7.0(01.234*kW)
    MeterId(String),                // 0-0:96.1.1(...)
    Crc(u16),                       // !F1F4
    Unknown,                        // anything this parser skips
}

fn describe(line: &TelegramLine) -> String {
    match line {
        TelegramLine::Power { watts } => format!("power {watts} W"),
        TelegramLine::MeterId(id) => format!("meter id {id}"),
        TelegramLine::Crc(crc) => format!("CRC {crc:04X}"),
        TelegramLine::Unknown => String::from("skipped"),
    }
}

fn main() {
    let lines = [
        TelegramLine::MeterId(String::from("44454D4F303030303030303030303137")),
        TelegramLine::Power { watts: 1234 },
        TelegramLine::Unknown,
        TelegramLine::Crc(0xF1F4),
    ];
    for line in &lines {
        println!("{}", describe(line));
    }
}
meter id 44454D4F303030303030303030303137
power 1234 W
skipped
CRC F1F4

The data of a variant can only be reached through a pattern that first checks which variant it is. There is no way to read watts from a line that is really a Crc; the classic C mistake of reading the wrong member of a union cannot be written.

The four variants of the enum TelegramLine. Each row shows a tag naming the variant and the data it carries: Power carries watts as a u32, MeterId carries a String, Crc carries a u16, and Unknown carries nothing. A value is always exactly one of the four rows, and its size is that of the largest variant plus room for the tag, unless the compiler can hide the tag in bit patterns the data never uses. tag data carried by the variant Power watts: u32 MeterId String (pointer, length, capacity) Crc u16 Unknown no data one value = one of these rows Size: the largest variant (here MeterId); the tag fits in unused bit patterns.
An enum value is one tagged variant at a time. The compiler only lets you reach the data after checking the tag.

Matching on enums

match is how you look inside an enum, and it must cover every variant (see Control Flow and Pattern Matching). This is the feature that makes enums safe to extend: add a variant, and every match that forgot it stops compiling, with the missing variant named:

#[derive(Debug)]
enum Tariff {
    Fixed,
    DayNight,
    Dynamic,
    Variable,
}

fn price_per_kwh(tariff: &Tariff) -> u32 {
    match tariff {
        Tariff::Fixed => 2700,
        Tariff::DayNight => 3120, // the day price
        Tariff::Dynamic => 2200,  // a monthly average
    }
}

fn main() {
    let t = Tariff::Variable;
    println!("{t:?}: {}", price_per_kwh(&t));
}
example.rs:10:11: error[E0004]: non-exhaustive patterns: `&Tariff::Variable` not covered: pattern `&Tariff::Variable` not covered

(The prices come from Kestrel's tariff table: €0.2700 fixed, €0.3120 the day price of day/night, €0.2200 the monthly average of dynamic.) Resist the temptation to silence this with a catch-all _ => arm in code about your own enums: the wildcard also swallows the next variant someone adds, which is exactly the case you want the compiler to tell you about.

Option and Result are enums

Rust has no null. A value that may be absent has the type Option<T>, which the standard library defines as an ordinary enum with two variants:

enum Option<T> {
    None,
    Some(T),
}

Result<T, E> is the same idea with Ok(T) and Err(E). Because they are enums, a match on them must handle the None or Err case too: forgetting to check for "missing" is a compile error, not a crash at run time. Both are covered in depth on Error Handling.

Structs and enums together: algebraic data types

Structs and enums are the two halves of what type theory calls an algebraic data type. A struct is a "this and that" type (a product type): a Reading has a meter and an amount and a tariff. An enum is a "this or that" type (a sum type): a telegram line is power or a meter id or a CRC. Nesting the two lets a type describe exactly the values that make sense. Languages such as Haskell, OCaml, Swift and Kotlin have the same idea; TypeScript gets close with discriminated unions.

The payoff is in what you can not write. Compare two ways to store a contract's tariff:

// Loose: which combinations are valid? Nothing stops
// tariff = "fixed" with a night_price, or "dynamic" with no prices at all.
struct ContractLoose {
    tariff: String,
    day_price: Option<u32>,
    night_price: Option<u32>,
}

// Exact: each variant carries precisely the data it needs.
enum ContractTariff {
    Fixed { price: u32 },
    DayNight { day: u32, night: u32 },
    Dynamic,  // hourly prices come from elsewhere
}

With the second type, "a day/night contract without a night price" is not a bug that a test has to catch; it is a value that cannot exist.

How big are they?

Structs and enums are stored inline, without an object header or a hidden pointer: a struct is its fields side by side, an enum is its largest variant plus room for the tag, which the compiler often hides in bit patterns the data never uses. Sizes depend on the platform; these were measured on a 64-bit Linux machine (ARM64):

#![allow(dead_code)] // the types are only measured here, never used

enum Tariff {
    Fixed,
    DayNight,
    Dynamic,
    Variable,
}

struct Reading {
    meter_number: u32,
    wh: u64,
    tariff: Tariff,
}

enum TelegramLine {
    Power { watts: u32 },
    MeterId(String),
    Crc(u16),
    Unknown,
}

fn main() {
    println!("Tariff:            {} byte", size_of::<Tariff>());
    println!("Reading:           {} bytes", size_of::<Reading>());
    println!("String:            {} bytes", size_of::<String>());
    println!("TelegramLine:      {} bytes", size_of::<TelegramLine>());
    println!("Option<u32>:       {} bytes", size_of::<Option<u32>>());
    println!("Box<u64>:          {} bytes", size_of::<Box<u64>>());
    println!("Option<Box<u64>>:  {} bytes", size_of::<Option<Box<u64>>>());
}
Tariff:            1 byte
Reading:           16 bytes
String:            24 bytes
TelegramLine:      24 bytes
Option<u32>:       8 bytes
Box<u64>:          8 bytes
Option<Box<u64>>:  8 bytes

Three things stand out:

  • Reading is 16 bytes, not the 24 a C struct with the same members in the same order would need on this machine. Rust is free to reorder fields to reduce padding; the order in memory is not the order in the source. When the exact C layout matters, for example to share the struct with a C library, you ask for it with #[repr(C)] (see Unsafe Rust and Calling C).
  • TelegramLine is as large as its biggest variant, the String, and no larger: the compiler found room for the tag in bit patterns that a String can never contain.
  • Option<Box<u64>> is the same size as a Box. A Box can never be a null pointer, so None is stored as the all-zero pattern. Rust's way of saying "maybe absent" costs nothing compared with C's null pointer, but it cannot be dereferenced by accident.

Deriving common behavior

A new struct or enum can do almost nothing: it cannot be printed, copied, compared or sorted. Each of those abilities is a trait, and for the common ones the compiler can write the implementation for you when you list them in #[derive(…)] above the type. How derive works, and how to write such an implementation yourself, is explained on Traits and Generics.

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
struct Wh(u64);

fn main() {
    let a = Wh(350_000);
    let b = a; // Copy: a stays usable
    let mut readings = [Wh(412_338), a, Wh(120_500), Wh::default()];
    readings.sort();
    println!("{:?}", readings);
    println!("a == b: {}", a == b);
    println!("largest: {:?}", readings.iter().max());
}
[Wh(0), Wh(120500), Wh(350000), Wh(412338)]
a == b: true
largest: Some(Wh(412338))
Derive Gives you Note
Debug {:?} and {:#?} derive it on almost every type
Clone .clone(), an explicit copy every field must be Clone
Copy assignment copies instead of moving needs Clone; only for small types without a String, Vec or Box inside
PartialEq, Eq == and !=, field by field Eq is not possible with f64 fields (NaN is not equal to itself)
PartialOrd, Ord <, >, sort(), max() compares fields in declaration order; enum variants in the order they are listed
Hash use as a HashMap key together with Eq
Default Wh::default() zero, empty string, false for each field

Display, the format for people that {} uses, cannot be derived: only you know whether a reading should print as 350 kWh or 350.000 kWh. You write it yourself in an impl fmt::Display block, as in the next section.

Making invalid values impossible

Fields are private to the module that defines the struct unless they are marked pub (modules and privacy are on Functions, Modules and Crates). Combine private fields with a constructor that checks its input, and code outside the module can only get a value that passed the check:

mod reading {
    pub struct Percentage {
        value: u8, // private: only code in this module can set it
    }

    impl Percentage {
        pub fn new(value: u8) -> Option<Percentage> {
            if value <= 100 { Some(Percentage { value }) } else { None }
        }

        pub fn value(&self) -> u8 {
            self.value
        }
    }
}

use reading::Percentage;

fn main() {
    for input in [42, 100, 180] {
        match Percentage::new(input) {
            Some(p) => println!("{input}: battery at {}%", p.value()),
            None => println!("{input}: rejected"),
        }
    }
}
42: battery at 42%
100: battery at 100%
180: rejected

Going around the constructor does not compile:

mod reading {
    pub struct Percentage {
        value: u8,
    }

    impl Percentage {
        pub fn value(&self) -> u8 {
            self.value
        }
    }
}

fn main() {
    let p = reading::Percentage { value: 180 };
    println!("{}%", p.value());
}
example.rs:14:35: error[E0451]: field `value` of struct `Percentage` is private: private field

Every function that receives a Percentage can rely on it being 0 to 100 without checking again. This is the same idea as the newtypes above, taken one step further: the type does not only say what a number means, it also guarantees which values it can have.

Putting it together: a reading report

In this example, a small command-line tool reads lines of the form meter;kwh;tariff from standard input, turns each into a Reading with a Tariff, and prints it with its cost. Lines that do not parse are reported, not silently dropped. The ? after each step returns None from parse as soon as one step fails; Error Handling explains it in detail.

use std::fmt;
use std::io;

#[derive(Debug, Clone, Copy, PartialEq)]
enum Tariff {
    Fixed,
    DayNight,
    Dynamic,
}

impl Tariff {
    fn from_name(name: &str) -> Option<Tariff> {
        match name {
            "fixed" => Some(Tariff::Fixed),
            "day_night" => Some(Tariff::DayNight),
            "dynamic" => Some(Tariff::Dynamic),
            _ => None,
        }
    }

    // Price per kWh in ten-thousandths of a euro (2700 = €0.2700).
    fn price(self) -> u64 {
        match self {
            Tariff::Fixed => 2700,
            Tariff::DayNight => 3120, // the day price
            Tariff::Dynamic => 2200,  // a monthly average
        }
    }
}

struct Reading {
    meter: String,
    kwh: u64,
    tariff: Tariff,
}

impl Reading {
    fn parse(line: &str) -> Option<Reading> {
        let mut parts = line.trim().split(';');
        let meter = parts.next()?.to_string();
        let kwh = parts.next()?.parse().ok()?;
        let tariff = Tariff::from_name(parts.next()?)?;
        Some(Reading { meter, kwh, tariff })
    }

    fn cost_cents(&self) -> u64 {
        self.kwh * self.tariff.price() / 100
    }
}

impl fmt::Display for Reading {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let cents = self.cost_cents();
        write!(f, "{} {:>4} kWh {:?} €{}.{:02}",
               self.meter, self.kwh, self.tariff, cents / 100, cents % 100)
    }
}

fn main() {
    for line in io::stdin().lines() {
        let line = line.expect("stdin is readable");
        match Reading::parse(&line) {
            Some(reading) => println!("{reading}"),
            None => println!("skipped: {line:?}"),
        }
    }
}
M-30004  350 kWh Fixed €94.50
M-30007  412 kWh DayNight €128.54
M-30012  120 kWh Dynamic €26.40
skipped: "M-30009;95;weekend"
skipped: "M-30010;abc;fixed"

Each type has one job. Tariff knows the valid names and prices, Reading knows how to parse a line and what it costs, and Display decides how it looks. main only connects them. The same tool returns on later pages: Error Handling says why a line was rejected, and Collections and Strings adds totals per tariff.