The Shape of Data
struct when you need "all of these at once", and an enum when you need "exactly one of these".
Structs: one record with named fields
A struct is a named bag of fields.
struct User {
name: String,
age: u32,
active: bool,
}
fn main() {
let ada = User {
name: String::from("Ada"),
age: 36,
active: true,
};
println!("{} is {}", ada.name, ada.age);
}
▶ Run this in the Rust Playground
A struct is a Python dict with the keys decided up front and the compiler enforcing that you did not forget one or misspell one. It is a TypeScript interface but the fields actually exist in memory.
A struct lays out its fields contiguously in memory. Fast to access, zero hidden allocations, exactly the size of its parts plus any padding the CPU needs.
Enums: exactly one of the listed options
#![allow(unused)]
fn main() {
enum Status {
Online,
Offline,
Busy,
}
}
That enum says: a Status is one of those three, and the compiler will never let it be anything else.
Rust enums go further than C enums — each variant can carry data of its own:
#![allow(unused)]
fn main() {
enum Event {
Click { x: i32, y: i32 },
KeyPress(char),
Disconnect,
}
}
A Click carries two coordinates. A KeyPress carries one character. A Disconnect carries nothing. But an Event is always one of those three, never two of them, never none.
struct = "and". enum = "or".
User has a name and an age and a flag. A Status is online or offline or busy.Matching on an enum
Once you have an enum, Rust wants you to handle every case. You do that with match:
enum Status {
Online,
Offline,
Busy,
}
fn greet(s: Status) -> &'static str {
match s {
Status::Online => "welcome back",
Status::Offline => "see you soon",
Status::Busy => "do not disturb",
}
}
fn main() {
println!("{}", greet(Status::Online));
}
▶ Run this in the Rust Playground
If you delete a branch — say, Busy — Rust will refuse to compile until you handle it:
error[E0004]: non-exhaustive patterns: `Busy` not covered
That is the feature. You cannot forget a case.
Pattern matching on an enum is a multiple-choice question where the compiler refuses to let you turn in the test with blanks. It will tell you which blanks you left, by name.
Tuples and arrays: the two lightweight containers
When you need a small, fixed group of values and do not want to bother naming fields, use a tuple:
#![allow(unused)]
fn main() {
let point = (3, 4);
let (x, y) = point;
println!("{x}, {y}");
}
When you need a fixed-size collection of values of the same type, use an array:
#![allow(unused)]
fn main() {
let week = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
println!("{}", week[0]);
}
For a growable list, you want Vec<T>. We will meet Vec in Chapter 7.
Methods on a struct
You attach behavior to a struct with impl:
struct Rect { width: f64, height: f64 }
impl Rect {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn main() {
let r = Rect { width: 3.0, height: 4.0 };
println!("{}", r.area());
}
▶ Run this in the Rust Playground
&self is the method’s way of saying “I will look at the struct but not take it over.” You will see &self on almost every method you ever write. Its meaning will be the subject of the next two chapters.
Here is where a struct actually lives. Step through and watch the User value take shape in memory.
Interactive simulation (requires JavaScript): a struct definition uses no memory; constructing a User places its three fields contiguously on the stack, with the String field pointing at a heap buffer that the struct owns.
- Define a
struct Book { title: String, pages: u32 }and print one. - Define an
enum Shapewith variantsCircle(f64)andSquare(f64). Write a functionarea(s: Shape) -> f64usingmatch. - Delete one of the
matchbranches in that function. Read the error.
Next, the idea that makes Rust Rust.