Structs & Methods (impl blocks)
Structs group related data into named fields, and impl blocks attach behavior to that data — Rust's answer to a class, without any inheritance.
Every language needs a way to bundle related pieces of data into one meaningful unit, and Rust's answer is the struct. Where a raw tuple like (f64, f64) groups two numbers with no names attached, a struct gives every field an identity: width and height instead of .0 and .1. This might look like a small ergonomic win, but it compounds fast — a function signature that takes a Rectangle documents intent far better than one that takes two anonymous floats, and the compiler will never let you accidentally swap width and height the way it would silently accept swapped tuple positions.
Defining a struct is just a struct keyword, a name, and a list of typed fields; instantiating one fills in every field by name inside curly braces, in any order you like. When a local variable shares its name with a field — let width = 30.0; followed by Rectangle { width, height } — Rust lets you drop the redundant width: width entirely, a shorthand you will use constantly once functions start taking parameters that match field names. Reading a field back out is just dot syntax, rect.width, and because structs are ordinary values, an entire Rectangle can be moved, borrowed, or (if it derives Clone) copied like any other type you've already met.
A struct definition only describes the data; the behavior that belongs to it lives in a separate impl block — impl Rectangle { ... } — which you can even write more than once for the same type if it helps organize things. This split between data and behavior is deliberate: unlike a class in Java or C#, a Rust struct never bundles fields and methods into one declaration, and there is no inheritance to muddy which impl block a method actually came from. Every function inside an impl block is either a method, which takes some form of self as its first parameter, or an associated function, which does not.
The form self takes is where Rust's ownership rules show up directly in your API design, and it's a genuine design decision every time you write one. &self borrows the instance immutably — the method can read fields but not change them, and the caller keeps using the value afterward, which is why read-only methods like area(&self) are the overwhelming default. &mut self borrows mutably, letting the method update fields in place, but it requires the caller to hold a mut binding and forbids any other borrow of that value while the mutable borrow is active. Taking self by value — no & at all — consumes the instance: the method now owns it, the original binding can no longer be used, and this is exactly what you want for a transformation that produces a genuinely new state from an old one, like a builder's final .build() step or a state-machine transition that shouldn't leave the old state lying around to be misused.
Rust has no new keyword and no constructor syntax, so 'construction' is just a convention: an associated function, almost always named new, that takes no self and returns Self (an alias for whatever type the impl block is for). You call it with :: instead of . — Rectangle::new(30.0, 50.0) — because there is no existing instance to call a method on yet; the associated function's whole job is to produce the first one. This is also where you enforce invariants at creation time: a new function can validate arguments, compute derived fields, or return a Result if construction can fail, none of which a bare struct literal can do on its own.
Two lighter-weight relatives round out the picture. A tuple struct — struct Point(f64, f64); — has a name and a type but positional, unnamed fields accessed as .0 and .1; it's the right choice when the position is genuinely self-explanatory (an RGB color, a 2D coordinate) or when you're wrapping a single value in the 'newtype pattern' purely to give it a distinct type (struct UserId(u64); is not interchangeable with a bare u64, and the compiler will stop you from mixing them up). A unit struct — struct Marker; — has no fields at all; you'll meet these later as zero-sized markers you attach trait implementations to, useful when the type itself carries all the meaning you need and no data has to travel with it.
With structs, you now have a way to model 'this shape of data, always.' The next lesson introduces enums, which model the opposite and often more realistic case — 'this data is one of these several shapes, and I don't know which until runtime' — and pairs that with match, the tool that forces you to handle every one of those shapes before your code will even compile.
// A classic struct: named fields, each with its own type.struct Rectangle {width: f64,height: f64,}// Tuple struct: fields have positions, not names — good for lightweight wrappers.struct Point(f64, f64);// Unit struct: no fields at all, useful as a marker type or for trait impls.struct Marker;fn main() {let rect = Rectangle { width: 30.0, height: 50.0 };println!("width = {}, height = {}", rect.width, rect.height);let origin = Point(0.0, 0.0);println!("origin = ({}, {})", origin.0, origin.1);let _m = Marker;}
Defining a classic named-field struct alongside a tuple struct and a unit struct, then instantiating and reading each one.
struct Rectangle {width: f64,height: f64,}impl Rectangle {// Associated function (constructor): no `self`, called as Rectangle::new(...).fn new(width: f64, height: f64) -> Rectangle {Rectangle { width, height }}// &self: borrows the instance immutably — just reads data.fn area(&self) -> f64 {self.width * self.height}// &mut self: borrows mutably — can change fields in place.fn scale(&mut self, factor: f64) {self.width *= factor;self.height *= factor;}// self (no &): takes ownership — the instance is consumed by this call.fn into_square(self) -> Rectangle {let side = (self.width + self.height) / 2.0;Rectangle { width: side, height: side }}}fn main() {let mut rect = Rectangle::new(30.0, 50.0);println!("area = {}", rect.area());rect.scale(2.0);println!("scaled area = {}", rect.area());let square = rect.into_square();println!("square side = {}", square.width);}
An impl block showing all three forms self can take — &self to read, &mut self to mutate, and self by value to consume — plus an associated function used as a constructor.
#[derive(Debug)]struct User {username: String,email: String,active: bool,sign_in_count: u64,}impl User {fn new(username: String, email: String) -> User {User { username, email, active: true, sign_in_count: 0 }}}fn main() {let user1 = User::new(String::from("ferris"), String::from("ferris@rust-lang.org"));// Struct update syntax: build a new instance from user1, overriding just one field.let user2 = User {email: String::from("crab@rust-lang.org"),..user1};println!("{:?}", user2);}
Deriving Debug for free printing, plus struct update syntax (..user1) to build a new instance that copies every field from an existing one except the ones you override.
🧠 Check your understanding
0/1 · 0/1 answered1. Given `fn into_square(self) -> Rectangle` defined inside `impl Rectangle`, why does `rect.into_square()` leave `rect` unusable afterward, while a call like `rect.area()` (defined as `fn area(&self) -> f64`) does not?