A struct groups values that exist together. An enum selects one alternative from a set of variants. Many data models need both: some information belongs to every value, while other information depends on its kind.
Consider a drawing program. Every shape has an identifier and a colour. Circles have a radius, rectangles have a width and height, and polygons have a list of points.
Two ways to represent the same information
The first model puts all the fields inside each enum variant:
#[derive(Clone, Copy)]
enum Colour { Red, Blue }
enum ShapeA {
Circle { id: u32, colour: Colour, radius: f64 },
Rect { id: u32, colour: Colour, width: f64, height: f64 },
Polygon { id: u32, colour: Colour, points: Vec<(f64, f64)> },
}
The second stores shared fields once and puts only the varying information
in an enum. It uses the same Colour type:
struct ShapeB {
id: u32,
colour: Colour,
geometry: Geometry,
}
enum Geometry {
Circle { radius: f64 },
Rect { width: f64, height: f64 },
Polygon { points: Vec<(f64, f64)> },
}
Both representations pair the right geometry fields with the selected kind. Neither validates the numbers: a negative radius or an insufficient number of polygon points is still representable. If those values are invalid in your application, add validation and control construction through your API.
Shared fields need different access code
An accessor can hide the first model's matching from its callers:
impl ShapeA {
fn id(&self) -> u32 {
match self {
Self::Circle { id, .. }
| Self::Rect { id, .. }
| Self::Polygon { id, .. } => *id,
}
}
}
This is one match arm with three alternatives. Adding a variant requires
updating this exhaustive accessor, but callers can continue using shape.id().
It does not force every caller to match on geometry.
In the second model, code with field access can simply read shape.id.
Geometry changes do not affect that access. You can also provide an accessor
if the field should stay private.
| Change | Fields inside variants | Shared fields plus geometry |
|---|---|---|
| Add a shared field | Update every variant and its construction sites | Update the struct and its construction sites |
| Add a geometry kind | Add a variant and revisit exhaustive shape matches | Add a geometry variant and revisit exhaustive geometry matches |
| Compute area | Match on ShapeA |
Match on shape.geometry |
| Read the identifier | Use an accessor or a match | Read the field or use an accessor |
For this domain, ShapeB makes the shared information easy to work with.
ShapeA remains useful when fields differ between variants, or when grouping
each variant's complete payload makes the API clearer. Similar field names
alone do not establish that the fields mean the same thing.
Independent choices can use separate enums
Suppose a payment has a method and a status. If every method can have every status, model them independently:
enum Method { Card, BankTransfer, Wallet }
enum Status { Pending, Authorised, Captured, Failed }
struct Payment {
amount_cents: u64,
method: Method,
status: Status,
}
This represents twelve method/status combinations without twelve named
variants. A function concerned only with status can match on that field.
A function concerned with both can match on (&payment.method, &payment.status).
Check your understanding What changes if your application's rules disallow one of those twelve combinations? Show answer Hide answer
The two-enum struct still permits it. An enum containing only the valid combinations can exclude it structurally. Another option is a type with private fields and constructors or transition methods that validate the pair. Method-specific status types may also fit the domain.
Choose based on which combinations and transitions are legal. Merely omitting an allowed case from a flat enum is a modelling omission, not a way of creating an illegal value. Conversely, separate enums do not establish that the choices are independent; that has to come from the domain rules.
Rust checks whether a match covers the values its type can represent. It does not know an application's business rules unless the representation or API enforces them. Ordinary struct fields still have type checking; they simply do not need an exhaustive match to be accessed.
Measure layout instead of assuming a winner
Neither shape representation is guaranteed to be smaller. Rust's default enum layout can use padding and unused bit patterns, so “largest payload plus a tag” is not an exact size formula. Field placement and alignment affect the surrounding struct too.
Measure both complete types with std::mem::size_of::<ShapeA>() and
std::mem::size_of::<ShapeB>() for your compiler and target. Comparing
ShapeA with Geometry alone would omit ShapeB's shared fields.
size_of measures inline storage, including padding. It does not include a
polygon vector's separately allocated points. Boxing a large payload may
reduce an enum's inline size, but adds allocation and indirection; measure
the workload before choosing that trade-off.
A separate choice: rows or columns
A collection of complete shapes is one layout. Separate vectors for each field are another:
struct Shapes {
ids: Vec<u32>,
colours: Vec<Colour>,
geometries: Vec<Geometry>,
}
Separate vectors can improve locality when an operation reads only one field across many shapes. They are not universally faster: access patterns, allocation and updates affect the result.
They also introduce an invariant: the vectors must have equal lengths, and entries at the same index must refer to the same shape. Equal lengths alone would not detect accidentally reordering just one vector.
Keep the vectors private and expose operations that update them together. When implementing insertion or removal, consider failure paths as well as successful updates; privacy gives you a place to enforce the invariant, but does not prove the implementation correct. Tests can check these operations, while benchmarks can help compare layouts.
Start by identifying which data is shared, which alternatives exclude each other, and which choices vary independently. Then choose a representation that makes the important rules straightforward to express and maintain.