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 aresnake_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 210Field 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.5celsius 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
#ff7f50You 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/sBoth 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 bytesIt 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.0derive 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.1Before 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: trueYou 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 parameter | Short for | The method can... |
|---|---|---|
&self | self: &Self | read the fields |
&mut self | self: &mut Self | read and change the fields |
self | self: Self | take 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/helloAfter 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 }
trueA 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 centsmain 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.2A 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 bemut. Name { field, ..other }uses the field init shorthand and fills the rest fromother.- 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{:?},{:#?}anddbg!.- An
implblock holds methods (first parameter&self,&mut selforself, called with a dot) and associated functions (noself, called withType::name). - By convention a constructor is an associated function called
newthat returnsSelf. 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.