Structs are the primary mechanism for defining custom types in Rust. Unlike classes in C++ or Java, Structs in Rust do not support inheritance. Composition and trait-based polymorphism are the Idiomatic alternatives.
Unit structs have no fields. They are useful as marker types or phantom types:
fn describe ( & self ) -> & "static str {
Unit structs have size 0 (they are zero-sized types). This makes them free to create and pass around — the compiler optimizes away all storage for them.
assert_eq! ( std :: mem :: size_of :: < Marker >(), 0 );
Tuple structs are named tuples. Each field is unnamed but accessible by index:
fn distance_from_origin ( & self ) -> f64 {
( self . 0 * self . 0 + self . 1 * self . 1 ) . sqrt ()
assert_eq! (p . distance_from_origin (), 5.0 );
Tuple structs with a single field implement the newtype pattern , creating a distinct type from The wrapped type:
fn get_user (id : UserId ) { /* ... */ }
// get_user(oid); // ERROR: expected UserId, found OrderId
This is type-safe and zero-cost — the compiler eliminates the wrapper at optimization time.
The most common form of struct definition:
name : String :: from ( "Alice" ),
email : Some ( String :: from ( "alice@example.com" )),
println! ( "{} is {}" , alice . name, alice . age);
When the variable name matches the field name, you can use the shorthand:
fn make_person (name : String , age : u32 ) -> Person {
Person { name, age, email : None }
Create a new struct from an existing one, overriding specific fields:
name : String :: from ( "Bob" ),
.. alice // remaining fields copied from alice
// bob.name == "Bob", bob.age == 30, bob.email == Some("alice@example.com")
Struct update syntax moves the remaining fields. After ..aliceThe alice binding can no longer Be used in its entirety (it is partially moved), but individual Copy fields remain accessible.
By default, the compiler is free to reorder fields and add padding for alignment. The #[repr] Attribute controls the memory layout:
#[repr( C )] // C-compatible layout — fields in declaration order, C alignment rules
#[repr(transparent)] // has the same layout as the single field inside
#[repr(packed)] // no padding — fields are packed tightly (may cause unaligned access)
b : u32 , // at offset 1, not offset 4 — misaligned on most platforms
#[repr(align(16))] // forced alignment of 16 bytes
Fields through references requires `unsafe` because the compiler cannot guarantee alignment for Dereferences. Use `#[repr(packed(2))]` or similar to specify minimum alignment.By default, all struct fields are private (visible only within the module where the struct is Defined). Use pub to make fields public:
pub radius : f64 , // public — accessible from other modules
center : Point , // private — only accessible within this module
pub fn new (radius : f64 , center : Point ) -> Self {
Circle { radius, center }
pub fn area ( & self ) -> f64 {
std :: f64 :: consts :: PI * self . radius * self . radius
Note: making a struct pub does not make its fields pub. Each field must be individually marked As pub. This is different from C++ where public: in a class definition makes all subsequent Members public.
Methods are defined inside impl blocks:
// Associated function (no self parameter) — like a static method
fn new (width : f64 , height : f64 ) -> Self {
Rectangle { width, height }
fn square (size : f64 ) -> Self {
Rectangle { width : size, height : size }
// Method taking immutable reference
// Method taking mutable reference
fn scale ( &mut self , factor : f64 ) {
// Method taking ownership
fn into_components ( self ) -> ( f64 , f64 ) {
( self . width, self . height)
let r = Rectangle :: new ( 3.0 , 4.0 );
assert_eq! (r . area (), 12.0 );
A type can have multiple impl blocks. This is useful for organizing methods by functionality or For separating trait implementations from inherent methods:
fn area ( & self ) -> f64 { self . width * self . height }
fn perimeter ( & self ) -> f64 {
2.0 * ( self . width + self . height)
Rust uses static dispatch by default — the compiler knows the exact type at the call site and Monomorphizes the code. Trait methods called through dyn Trait use dynamic dispatch via vtable Indirection.
struct Circle { radius : f64 }
fn area ( & self ) -> f64 { std :: f64 :: consts :: PI * self . radius * self . radius }
// Static dispatch — no vtable
let c = Circle { radius : 5.0 };
let a = c . area (); // compiler generates Circle::area directly
// Dynamic dispatch — vtable lookup
let s : &dyn Shape = & Circle { radius : 5.0 };
let a = s . area (); // indirect call through vtable
Enums are algebraic data types (sum types). Each variant can optionally carry data. Rust enums are Discriminated unions — the compiler stores a tag (discriminant) to identify which variant is active.
let d = Direction :: North ;
let home = IpAddr :: V4 ( 127 , 0 , 0 , 1 );
let loopback = IpAddr :: V6 ( String :: from ( "::1" ));
Rectangle { width : f64 , height : f64 },
Point { x : f64 , y : f64 },
let s = Shape :: Circle { radius : 5.0 };
Resize { width : u32 , height : u32 }, // struct
The compiler stores a discriminant tag alongside the variant data. The default discriminant type is The smallest integer that can represent all variants:
// sizeof(Color) == 1 (tag only, no data)
Integer ( i64 ), // 1 — 8 bytes of data
Text ( String ), // 2 — 24 bytes (ptr + len + cap)
// sizeof(Payload) == 32 (8 bytes tag + 24 bytes data, with padding)
You can control the discriminant with #[repr]:
Enums that carry data are one of Rust’s most powerful features. The standard library’s Option and Result are both enums:
Custom enums with data are equally powerful:
Add ( Box < Expr >, Box < Expr >),
Mul ( Box < Expr >, Box < Expr >),
fn eval (expr : & Expr , env : & std :: collections :: HashMap < String , i64 >) -> i64 {
Expr :: Add (l, r) => eval (l, env) + eval (r, env),
Expr :: Mul (l, r) => eval (l, env) * eval (r, env),
Expr :: Var (name) => env . get (name) . copied () . unwrap_or ( 0 ),
Note the use of Box<Expr> — without boxing, the enum would be infinitely sized because Expr Contains itself recursively.
Pattern matching is Rust’s primary control flow mechanism for enums and is exhaustively checked by The compiler.
fn color_to_rgb (c : Color ) -> ( u8 , u8 , u8 ) {
Color :: Red => ( 255 , 0 , 0 ),
Color :: Green => ( 0 , 255 , 0 ),
Color :: Blue => ( 0 , 0 , 255 ),
The compiler verifies that every variant is handled. If you add a new variant to ColorEvery match on Color will produce a compile error until updated. This is exhaustive pattern matching.
Match ergonomics automatically add & and ref patterns when matching through references:
Color :: Red => println! ( "red" ),
// Before match ergonomics: &Color::Red => ...
// Now: Color::Red => ... (compiler auto-refs)
A match guard is an additional if condition on a match arm:
Some (x) if x < 5 => println! ( "less than five: {}" , x),
Some (x) => println! ( "{}" , x),
None => println! ( "none" ),
Match guards do not participate in exhaustiveness checking. The compiler cannot prove that a guard Will always match for a given variant, so you may still need a catch-all arm.
struct Point { x : i32 , y : i32 }
let p = Point { x : 1 , y : 2 };
Point { x : 0 , y : 0 } => println! ( "origin" ),
Point { ref x, ref y } => println! ( "x={}, y={}" , x, y),
// x and y are &i32, p is not moved
let mut v = vec! [ 1 , 2 , 3 ];
ref mut v => v . push ( 4 ), // borrow v mutably and push
assert_eq! (v, vec! [ 1 , 2 , 3 , 4 ]);
The @ operator binds a value to a name while also testing it against a pattern:
n @ 1 ..= 12 => println! ( "child of age {}" , n),
n @ 13 ..= 19 => println! ( "teenager of age {}" , n),
n => println! ( "adult of age {}" , n),
This is especially useful when you need to destructure and also capture the whole value:
let v = Value :: Number ( 42 );
Value :: Number (n @ 0 ..= 100 ) => println! ( "small number: {}" , n),
Value :: Number (n) => println! ( "large number: {}" , n),
Value :: Text (s) => println! ( "text: {}" , s),
1 ..= 5 => println! ( "one through five" ),
6 ..= 10 => println! ( "six through ten" ),
_ => println! ( "something else" ),
Range patterns only work on numeric types and char. They are inclusive on both ends.
( 0 , y) => println! ( "x is zero, y is {}" , y),
(x, 0 ) => println! ( "x is {}, y is zero" , x),
(x, y) if x == y => println! ( "equal: {}" , x),
(x, y) => println! ( "different: {} and {}" , x, y),
struct Point { x : f64 , y : f64 }
let p = Point { x : 0.0 , y : 7.0 };
Point { x : 0.0 , y } => println! ( "on the y-axis at {}" , y),
Point { x, y : 0.0 } => println! ( "on the x-axis at {}" , x),
Point { x, y } => println! ( "at ({}, {})" , x, y),
When you only care about one variant, if let is more concise than match:
let some_value = Some ( 7 );
if let Some (n) = some_value {
println! ( "value is {}" , n);
if let does not check exhaustiveness. The else branch handles all non-matching cases:
if let Some (n) = some_value {
println! ( "value is {}" , n);
let-else combines pattern matching with early return:
fn process (data : Option < Vec < i32 >>) -> i32 {
let Some (values) = data else {
The else block must diverge (return, break, continue, panic, or loop). This is cleaner than the Equivalent match with a single arm and a fallback.
let mut stack = Vec :: new ();
while let Some (top) = stack . pop () {
while let runs the loop body as long as the pattern matches. When it stops matching, the loop Ends.
The matches! macro is a concise way to check whether a value matches a pattern:
assert! ( matches! (x, Some ( 5 )));
assert! ( matches! (x, Some (_)));
assert! ( ! matches! (x, None ));
assert! ( matches! (x, Some (n) if n > 3 ));
fn maybe_double (x : Option < i32 >) -> Option < i32 > {
fn parse_int (s : & str ) -> Result < i32 , std :: num :: ParseIntError > {
In practice, you rarely write explicit match for Option and Result. Combinator methods and the ? operator are more idiomatic:
fn maybe_double (x : Option < i32 >) -> Option < i32 > {
fn parse_and_double (s : & str ) -> Result < i32 , std :: num :: ParseIntError > {
The #[derive] attribute auto-generates implementations for common traits:
#[derive( Debug , Clone , PartialEq , Eq , Hash )]
Trait What it generates Debugfmt::Debug for {:?} formattingCloneclone() — deep copy (requires all fields to be Clone)CopyImplicit bitwise copy (requires CloneNo Drop) PartialEq== and != — structural equalityEqMarks type as having reflexive equality (requires PartialEq) PartialOrd<``>``<=``>= — derived from field orderOrdTotal ordering (requires PartialOrd``Eq) HashHash function for HashMap/HashSet keys DefaultDefault value (all fields must implement Default)
Struct, the derived ordering changes. Deriving `Ord` on a struct with a `f64` field will fail Because `f64` does not implement `Ord`. Use a custom implementation or wrap the field in the `ordered-float` crate's `OrderedFloat` type instead.You can implement custom derive macros. The derive crate ecosystem includes serde::Serialize serde::Deserialize``thiserror::ErrorAnd many more. These work by procedural macro expansion at Compile time.
The newtype pattern wraps an existing type in a tuple struct to provide type safety, implement Different traits, or add domain-specific behavior:
fn to_millimeters ( & self ) -> Millimeters {
Millimeters ( self . 0 * 1000 )
fn measure (distance : Millimeters ) {
println! ( "{}mm" , distance . 0 );
measure (m . to_millimeters ()); // OK
// measure(m); // ERROR: expected Millimeters, found Meters
You can implement traits on newtypes that the wrapped type does not implement:
struct Wrapper ( Vec < String >);
impl std :: fmt :: Display for Wrapper {
fn fmt ( & self , f : &mut std :: fmt :: Formatter ) -> std :: fmt :: Result {
write! (f, "[{}]" , self . 0. join ( ", " ))
let w = Wrapper ( vec! [ String :: from ( "hello" ), String :: from ( "world" )]);
println! ( "{}" , w); // [hello, world]
Enums can have impl blocks just like structs:
Message :: Quit => println! ( "quitting" ),
Message :: Move { x, y } => println! ( "moving to ({}, {})" , x, y),
Message :: Write (text) => println! ( "writing: {}" , text),
Message :: ChangeColor (r, g, b) => {
println! ( "changing color to ({}, {}, {})" , r, g, b)
Rust’s visibility system is based on modules, not classes. The pub keyword makes an item visible To the parent module. To make an item visible to the entire crate, use pub(crate). To make it Visible to a specific module, use pub(in path).
pub stream : TcpStream , // visible everywhere
pub ( crate ) buffer : Vec < u8 >, // visible within the crate
pub ( super ) state : State , // visible in parent module (network)
local_addr : SocketAddr , // private — only visible in tcp
Use pub use to re-export items from another module, making them accessible under the current Module’s path:
pub use internal :: Config ; // Config is now available as crate::Config
This is commonly used to flatten module hierarchies and provide a clean public API that differs from The internal organization.
Option<T> represents a value that may or may not be present. It is defined as:
let x : Option < i32 > = Some ( 2 );
x . map ( | n | n * 2 ); // Some(4)
x . and_then ( | n | if n > 0 { Some (n) } else { None }); // Some(2)
x . filter ( |& n | n > 1 ); // Some(2)
x . unwrap_or_else ( || expensive_fall ()); // 2 (fallthrough only evaluated for None)
x . ok_or ( "error" ); // Ok(2)
x . as_mut (); // Some(&mut 2)
let y : Option < i32 > = None ;
y . map ( | n | n * 2 ); // None
The ? operator works with Option in functions that return Option:
fn first_even (nums : & [ i32 ]) -> Option < i32 > {
. find ( |&& n | n % 2 == 0 ) ? ; // returns None if find returns None
let & n = nums . iter () . find ( |&& n | n % 2 == 0 ) ? ;
Result<T, E> represents an operation that can either succeed (Ok(T)) or fail (Err(E)):
let r : Result < i32 , & str > = Ok ( 42 );
r . map ( | n | n * 2 ); // Ok(84)
r . map_err ( | e | format! ( "error: {}" , e)); // Ok(42)
r . and_then ( | n | if n > 0 { Ok (n) } else { Err ( "negative" ) }); // Ok(42)
r . unwrap_or_else ( | e | { e . len () }); // 42
let e : Result < i32 , & str > = Err ( "bad" );
e . unwrap_or_default (); // 0 (requires T: Default)
let opt : Option < i32 > = Some ( 42 );
let res : Result < i32 , & str > = opt . ok_or ( "missing value" ); // Ok(42)
let res : Result < i32 , & str > = Ok ( 42 );
let opt : Option < i32 > = res . ok (); // Some(42)
Forgetting to handle all enum variants. The compiler will error if a match is not exhaustive. Add a _ => catch-all only when it makes semantic sense — it hides future variant additions. Prefer explicit handling of every variant for types you control.
Struct update syntax and partial moves. Struct { ..other } moves the remaining fields. If other has any Drop types, the entire struct is considered partially moved and cannot be used as a whole afterward. This is correct behavior — you have transferred ownership of some fields.
Matching on references without understanding match ergonomics. Pre-2021 Rust required explicit & in patterns when matching through references. Match ergonomics simplify this, but can be confusing when you need to capture a reference vs. A value. If the compiler suggests adding refPay attention — it is telling you that a move would occur otherwise.
Using unwrap() in production code. unwrap() panics on None/Err. Panics are for unrecoverable programming errors (bugs), not for expected failure modes. Use ?``unwrap_or unwrap_or_elseOr pattern matching for expected failures.
Enum size explosion. An enum’s size is the size of its largest variant plus the discriminant, aligned to the largest variant’s alignment. If one variant is much larger than the others (e.g., a String variant alongside u8 variants), consider boxing the large variant: Large(String) vs Large(Box<String>).
Deriving traits on enums with non-unit variants. #[derive(PartialEq)] compares variant discriminants first, then compares the inner data. For structs, it compares field-by-field. If your type contains f64You cannot derive Eq or Ord because f64 does not implement them.
Not using if let/let-else when appropriate. A match with a single non-trivial arm and a _ => catch-all is less readable than an if let or let-else. Use match when you need exhaustive handling; use if let/let-else for focused pattern extraction.
Overusing Option for boolean semantics. Option<bool> has three states: None Some(true)``Some(false). If you need three states, use an enum instead — it is clearer and self-documenting.
Pattern binding shadowing. In match arms, the binding name shadows any outer binding with the same name. This can lead to confusion when the same variable name appears in multiple arms:
let x = "zero" ; // shadows outer x
println! ( "{}" , x); // "zero"
n => println! ( "{}" , n), // n is a new binding, not the outer x
Enum variants are not types. You cannot write fn takes_v4(addr: IpAddr::V4). Enum variants are not types — they are constructors. Use the full enum type and pattern match inside the function body, or use a newtype wrapper around the variant.
This topic covers the core concepts of structs and enums, including underlying theory, practical implementation, and key applications.
Key concepts include:
ownership, borrowing, and lifetimes structs, enums, and pattern matching traits and generics error handling (Result, Option) concurrency with threads and async Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
## Intuition
Structs group related data under named fields, while enums represent variants where exactly one variant is active at a time. Rust enums are algebraic data types: each variant can carry different data, enabling pattern matching that the compiler verifies for exhaustiveness. Structs are value types that move on assignment unless they implement Copy. Methods are defined in impl blocks, and associated functions (like constructors) are called with :: syntax.
[[rust/02-ownership-borrowing/ownership]] - Value types and move semantics [[rust/04-error-handling/error-handling]] - Result and Option enums [[rust/05-traits-generics/traits-and-generics]] - Trait implementations for custom types [[rust/03-structs-enums/advanced-patterns]] - Pattern matching and destructuring