127. Structs and impl
The particle has been an object since chapter 3. In Rust it becomes a struct: a record with a fixed shape, its methods kept in a separate block, and every method saying out loud how it uses the particle.
- Define a struct, build a value of it and read and change its fields.
- Attach methods and associated functions to a type with an impl block.
- Choose between &self, &mut self and self for each method.
- Explain how Rust lays an array of structs out in memory.
A particle with a fixed shape
Here is the particle from chapter 3, in TypeScript:
interface Particle {
x: number; y: number;
vx: number; vy: number;
}
const p: Particle =
{ x: 0, y: 0, vx: 3, vy: 4 };
The interface is only a promise to the type checker. At run time p is
an ordinary object, and the engine has to discover its shape for itself
(the shapes of chapter 63). Rust has no ordinary objects. You declare
the shape first, and every value is built from it.
A printed form with labelled boxes. Every copy of the form has the same boxes in the same places, and you can’t hand one in with a box missing or with an extra box scribbled in the margin.
Structs
struct Particle {
x: f64,
y: f64,
vx: f64,
vy: f64,
}
let mut p = Particle {
x: 0.0, y: 0.0, vx: 3.0, vy: 4.0,
};
p.x += p.vx;
You must give every field, and their types are checked. Fields are read
and written with a dot, as in TypeScript. Mutability belongs to the
binding: let mut p makes all of p changeable, and you can’t mark
just one field as mutable[1].
When a variable has the same name as a field, you can write it once.
Particle { x, y, vx: 0.0, vy: 0.0 } means x: x, y: y. That is
field init shorthand.
A struct moves like a String does. let q = p; moves the particle
into q, and p can’t be used afterwards (chapter 124). Chapter 131
shows how to make small structs copy instead.
A struct is a named record type: a fixed list of fields, each with a
name and a type. A struct expression Name { field: value, ... }
builds a value and must give every field. value.field reads or writes
one field, and writing needs a mut binding.
Methods live in impl
A TypeScript class (chapter 7) puts fields and methods in one block.
Rust splits them. The struct holds the data, and an impl block attaches
functions to the type:
impl Particle {
fn new(vx: f64, vy: f64) -> Self {
Particle { x: 0.0, y: 0.0, vx, vy }
}
fn speed(&self) -> f64 {
let v2 = self.vx * self.vx
+ self.vy * self.vy;
v2.sqrt()
}
fn step(&mut self, dt: f64) {
self.x += self.vx * dt;
self.y += self.vy * dt;
}
}
new has no self. It is an associated function, called on the
type: Particle::new(3.0, 4.0). There is no new keyword in Rust, and
new is just the usual name for a function that builds a value. Inside
the impl, Self is another name for Particle.
speed and step are methods: their first parameter is self, the
value the method was called on. You call them with a dot, p.speed().
An impl block attaches functions to a type. Those whose first
parameter is self are methods, called as value.method(). The
rest are associated functions, called as Type::function(). In the
block, Self names the type being implemented.
Three kinds of self
JavaScript’s this is whatever object you called the method on, and the
method can do anything to it. Rust’s self is a parameter like any
other, so its type says exactly what the method may do.
let mut p = Particle::new(3.0, 4.0);let s = p.speed();p.step(1.0);p.step(1.0);let q = p.stopped();p.speed(); // ✗ p was moved
&selfborrows the value to read it. Most methods are like this.&mut selfborrows it to change it. The caller needslet mut.selftakes ownership. The value moves into the method, and the caller can’t use it afterwards. It is rare, and used when a method turns the value into something else[1].
You don’t write the borrow at the call. p.step(1.0) means
Particle::step(&mut p, 1.0). Rust looks at the receiver type and adds
the & or &mut for you[1]. That makes the
borrowing rules from chapter 125 apply to every method call. Forget the
mut (real rustc 1.97 output, trimmed):
error[E0596]: cannot borrow `p` as
mutable, as it is not declared as
mutable
Use p after p.stopped() and the error even explains why:
note: `Particle::stopped` takes
ownership of the receiver `self`,
which moves `p`
The self receiver is a method’s first parameter. &self (short
for self: &Self) borrows the value shared, &mut self borrows it
mutably, and self takes ownership of it. Method calls borrow
automatically to match.
struct Particle {
x: f64,
y: f64,
vx: f64,
vy: f64,
}
impl Particle {
fn new(vx: f64, vy: f64) -> Self {
Particle {
x: 0.0,
y: 0.0,
vx,
vy,
}
}
fn speed(&self) -> f64 {
let v2 = self.vx * self.vx
+ self.vy * self.vy;
v2.sqrt()
}
fn step(&mut self, dt: f64) {
self.x += self.vx * dt;
self.y += self.vy * dt;
}
fn stopped(mut self) -> Self {
self.vx = 0.0;
self.vy = 0.0;
self
}
}
fn main() {
let mut p = Particle::new(3.0, 4.0);
println!("speed {}", p.speed());
p.step(1.0);
p.step(1.0);
println!("at {} {}", p.x, p.y);
let q = p.stopped();
println!("speed {}", q.speed());
}The Rust Drip VM is mostly structs with methods. Its Vm struct owns
the value stack, and the receivers say who changes what (from
companion/rust/src/vm.rs):
fn push(&mut self, value: Value)
fn pop(&mut self) -> Value
fn peek(&self, distance: usize)
-> ValueStruct layout is where Viscosity comes back in. The default Rust layout
promises very little: fields don’t overlap, and each one starts at an
offset its alignment allows. The compiler is free to reorder them. What
it does promise is that an array of T puts element n exactly
n * size_of::<T>() bytes from the start[2].
So [Particle; 1000] is an array of structs in the most literal sense:
the fields sit inline, one particle after another, with no pointer to
chase. A JavaScript array of objects holds references to objects that
live elsewhere, which is what made pointer chasing slow in chapter 20. Chapter 139 measures these layouts, and
chapter 140 turns one into a struct of arrays by hand.
Your turn
Finish this fixed-size value stack, adapted from push, pop and peek in the Rust Drip VM. top is the number of values on the stack, so the top value is at slots[top - 1]. peek(0) is the top value, peek(1) the one under it (d is the distance down from the top), and peeking removes nothing. The receivers are already written: notice which methods need &mut self. Replace each todo!(), which stops the program if it is ever reached.
struct Stack {
slots: [f64; 8],
top: usize,
}
impl Stack {
fn new() -> Self {
Stack {
slots: [0.0; 8],
top: 0,
}
}
fn push(&mut self, v: f64) {
todo!()
}
fn pop(&mut self) -> f64 {
todo!()
}
fn peek(&self, d: usize) -> f64 {
todo!()
}
fn len(&self) -> usize {
self.top
}
}A Span marks a byte range of source code, like a token’s position. Write len(&self), the number of bytes it covers, and join(self, b), which consumes both spans and returns the smallest span covering both. a.min(b) and a.max(b) work on usize.
struct Span {
start: usize,
end: usize,
}
impl Span {
fn len(&self) -> usize {
0
}
fn join(self, b: Span) -> Span {
self
}
}What you learned
- A struct is a named record with typed fields. Build it with every
field, read with a dot, and change it through a
mutbinding. - An
implblock attaches functions to a type. Methods takeself; associated functions likenewdon’t, and are called withType::. - The self receiver says how a method uses the value:
&selfreads,&mut selfchanges,selfconsumes. Calls borrow automatically. - An array of structs stores the structs inline, back to back.
Next: enums and match, Rust’s own discriminated unions, and the
match over opcodes at the heart of the Rust VM.
Check your understanding
0/6- 1.
Does this compile?
let p = Particle::new(3.0, 4.0); p.step(1.0); // step(&mut self, ..) - 2.
A method only reads two fields and returns a number. Which receiver should it take?
- 3.
let q = p.stopped();wherestoppedtakesself. Which is true afterwards? - 4.
How do you call
fn new(vx: f64, vy: f64) -> Selffromimpl Particle? - 5.
What does the default Rust layout promise about a struct’s fields?
- 6.
In TypeScript, a class bundles fields and methods together. How does Rust split them?
References
- [1] Steve Klabnik, Carol Nichols, Chris Krycho (2026). The Rust Programming Language (3rd edition). No Starch Press; free online at doc.rust-lang.org/book. doc.rust-lang.org/book/ (checked 2026-10-31)
- [2] The Rust Reference contributors. Type layout. The Rust Reference. doc.rust-lang.org/reference/type-layout.html (checked 2026-10-31)