Structs, Enums and Methods
Grouping data in structs, modeling choices with enums that carry data, adding methods and associated functions in impl blocks, and deriving common behavior such as Debug and PartialEq.
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.
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:
-
Readingis 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). -
TelegramLineis as large as its biggest variant, theString, and no larger: the compiler found room for the tag in bit patterns that aStringcan never contain. -
Option<Box<u64>>is the same size as aBox. ABoxcan never be a null pointer, soNoneis 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.