The newtype pattern wraps an existing type in a tuple struct, creating a distinct type with the same Memory representation. This provides type safety without runtime overhead — the compiler eliminates The wrapper after optimization.
fn get_user (id : UserId ) -> String {
fn get_order (id : OrderId ) -> String {
format! ( "order_{}" , id . 0 )
// get_user(oid); // ERROR: expected UserId, found OrderId
The newtype pattern prevents accidentally passing an OrderId where a UserId is expected. Both Are u64 internally, but the compiler treats them as completely different types.
Newtypes have the same size and alignment as the wrapped type:
assert_eq! ( std :: mem :: size_of :: < Millimeters >(), 4 );
assert_eq! ( std :: mem :: size_of :: < Meters >(), 4 );
assert_eq! ( std :: mem :: align_of :: < Millimeters >(), 4 );
Implementing Deref and DerefMut allows the newtype to behave like the wrapped type for method Calls and deref coercion:
struct Wrapper ( Vec < String >);
type Target = Vec < String >;
fn deref ( & self ) -> & Self :: Target {
let w = Wrapper ( vec! [ String :: from ( "hello" )]);
let len = w . len (); // calls Vec::len through deref coercion
`Deref`Callers can use the newtype as if it were the inner type, potentially defeating the purpose Of the wrapper. Only implement `Deref` when you intentionally want this behavior. fn to_millimeters ( & self ) -> Millimeters {
Millimeters ( self . 0 * 1000 )
fn to_meters ( self ) -> Meters {
let distance = Meters ( 5 );
let mm = distance . to_millimeters ();
The transparent representation guarantees that the newtype has the same layout as the inner type. This is important for FFI:
fn new (value : u32 ) -> Option < Self > {
The builder pattern constructs complex objects step by step, enforcing required fields at compile Time when using the typestate pattern.
headers : Vec <( String , String )>,
struct HttpRequestBuilder {
headers : Vec <( String , String )>,
impl HttpRequestBuilder {
fn new (method : & str ) -> Self {
method : method . to_string (),
fn url ( mut self , url : & str ) -> Self {
self . url = Some (url . to_string ());
fn header ( mut self , key : & str , value : & str ) -> Self {
self . headers . push ((key . to_string (), value . to_string ()));
fn body ( mut self , body : & str ) -> Self {
self . body = Some (body . to_string ());
fn timeout ( mut self , ms : u64 ) -> Self {
fn build ( self ) -> Result < HttpRequest , String > {
let url = self . url . ok_or ( "url is required" ) ? ;
timeout_ms : self . timeout_ms,
let request = HttpRequestBuilder :: new ( "GET" )
. url ( "https://example.com/api" )
. header ( "Content-Type" , "application/json" )
. header ( "Authorization" , "Bearer token" )
use derive_builder :: Builder ;
#[builder(default = "8080" )]
#[builder(default = r#"String::from("localhost")"# )]
#[builder(setter(into), default = "4" )]
let config = ConfigBuilder :: default ()
. database_url ( "postgres://localhost/mydb" )
The typestate pattern encodes state machines in the type system. Each state is a different type, and State transitions are represented as methods that consume the current state and return the next State. Invalid transitions are compile errors.
struct Configured { host : String , port : u16 }
struct Connected { host : String , port : u16 , stream : std :: net :: TcpStream }
struct Authenticated { host : String , port : u16 , stream : std :: net :: TcpStream , token : String }
impl Client < Unconfigured > {
Client { state : Unconfigured }
fn configure ( self , host : & str , port : u16 ) -> Client < Configured > {
impl Client < Configured > {
fn connect ( self ) -> Result < Client < Connected >, std :: io :: Error > {
let stream = std :: net :: TcpStream :: connect (( self . state . host . as_str (), self . state . port)) ? ;
fn authenticate ( self , username : & str , password : & str ) -> Result < Client < Authenticated >, String > {
let token = format! ( "token_for_{}" , username);
stream : self . state . stream,
impl Client < Authenticated > {
fn send_request ( & self , path : & str ) -> String {
format! ( "GET {} HTTP/1.1 \n Authorization: Bearer {} \n Host: {} \n " ,
path, self . state . token, self . state . host)
let client = Client :: new ()
. configure ( "example.com" , 443 )
. authenticate ( "admin" , "secret" ) ? ;
let request = client . send_request ( "/api/data" );
The type system prevents calling send_request before authenticateOr authenticate before connect. Each method consumes self and returns a new state, making the state transition Irreversible and type-safe.
Attempting an invalid transition is a compile error:
let client = Client :: new ();
// client.send_request("/api"); // ERROR: method not found on Client<Unconfigured>
// client.authenticate("a", "b"); // ERROR: method not found on Client<Unconfigured>
Enum dispatch uses enums to implement polymorphism without trait objects, providing static dispatch And better performance:
Rectangle { width : f64 , height : f64 },
Triangle { base : f64 , height : f64 },
Shape :: Circle { radius } => std :: f64 :: consts :: PI * radius * radius,
Shape :: Rectangle { width, height } => width * height,
Shape :: Triangle { base, height } => 0.5 * base * height,
Property Enum Dispatch Trait Objects (dyn Trait) Dispatch mechanism Static (branch table) Dynamic (vtable indirection) Binary size One copy per variant Shared vtable Extensibility Closed (all variants known) Open (any implementor) Performance Predictable, inlinable Indirect call, not inlinable Type information Full at compile time Erased at runtime
Use enum dispatch when:
The set of variants is known and closed Performance is critical (hot paths, game loops) You need to match on specific variants Use trait objects when:
The set of types is open (plugins, user-defined types) You need heterogeneous collections Binary size is more important than peak performance Zero-sized types occupy no memory at runtime. They are useful as marker types, phantom types, and For compile-time programming.
assert_eq! ( std :: mem :: size_of :: < Marker >(), 0 );
assert_eq! ( std :: mem :: size_of :: < Benchmark >(), 0 );
PhantomData<T> is a zero-sized type that makes the compiler behave as if the struct contains a TEven though it does not. This is useful for variance and drop check annotations:
use std :: marker :: PhantomData ;
let id : Id < String > = Id { value : 42 , _marker : PhantomData };
let id2 : Id < Vec < u8 >> = Id { value : 43 , _marker : PhantomData };
assert_eq! ( std :: mem :: size_of :: < Id < String >>(), 8 );
assert_eq! ( std :: mem :: size_of :: < Id < Vec < u8 >>>(), 8 );
PhantomData<T> affects variance: Id<T> is covariant in T because PhantomData<T> is covariant In T.
ZSTs enable powerful generic programming patterns. A Vec<()> has zero per-element storage cost:
let v : Vec <()> = vec! [(); 1_000_000 ];
assert_eq! (v . len (), 1_000_000 );
// v occupies only the Vec metadata (24 bytes), no element storage
The unit type () is a ZST and is used as a default or placeholder type:
fn process < T >(_ : T ) -> T {
let result = process (()); // T = (), zero overhead
Create a new struct from an existing one, overriding specific fields:
let p1 = Point { x : 1.0 , y : 2.0 , z : 3.0 };
let p2 = Point { y : 5.0 , .. p1 };
// p2.x == 1.0, p2.y == 5.0, p2.z == 3.0
Struct update syntax moves the remaining fields. After ..p1``p1 is partially moved:
let p1 = Point { x : 1.0 , y : 2.0 , z : 3.0 };
let p2 = Point { y : 5.0 , .. p1 };
// println!("{:?}", p1); // ERROR: p1 partially moved
println! ( "{}" , p1 . x); // ERROR: x was moved into p2
Heap-allocated types, those are moved (not copied) into the new struct. After the spread, the Original struct is no longer usable in its entirety.struct Point { x : f64 , y : f64 }
let p = Point { x : 1.0 , y : 2.0 };
let Point { x : a, y : b } = p;
fn swap (( mut x, mut y) : ( i32 , i32 )) -> ( i32 , i32 ) {
std :: mem :: swap ( &mut x, &mut y);
assert_eq! ( swap (( 1 , 2 )), ( 2 , 1 ));
fn unwrap_or < T , E >(result : Result < T , E >, default : T ) -> T {
Result :: Ok (value) => value,
Result :: Err (_) => default,
Tuple structs with a single field are the standard form of the newtype pattern:
struct PhoneNumber ( String );
fn send_email (to : Email , body : & str ) {
println! ( "sending to {}: {}" , to . 0 , body);
let email = Email ( String :: from ( "user@example.com" ));
send_email (email, "hello" );
// send_email(PhoneNumber(String::from("555-1234")), "hello"); // ERROR
#[derive( Debug , Clone , PartialEq , Eq , Hash )]
#[derive( Debug , Clone , PartialEq , Eq , Hash )]
assert_ne! (uid, oid); // different types, even though inner values are equal
Marker types carry no data but encode information in the type system:
struct Distance < Unit >( f64 , std :: marker :: PhantomData < Unit >);
fn add_km (a : Distance < Kilometers >, b : Distance < Kilometers >) -> Distance < Kilometers > {
Distance (a . 0 + b . 0 , PhantomData )
fn add_mi (a : Distance < Miles >, b : Distance < Miles >) -> Distance < Miles > {
Distance (a . 0 + b . 0 , PhantomData )
Enums can carry different data payloads per variant, making them algebraic data types:
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 ),
Without BoxRecursive enums would be infinitely sized:
// This does NOT compile — infinite size
// enum Expr { Add(Expr, Expr) }
// Fix: Box the recursive variants
Add ( Box < Expr >, Box < Expr >), // pointer-sized (8 bytes)
The #[non_exhaustive] attribute prevents downstream crates from constructing the enum or matching Exhaustively on it. This allows you to add variants in a semver-compatible way:
// In a downstream crate:
ApiVersion :: V1 => println! ( "v1" ),
ApiVersion :: V2 => println! ( "v2" ),
_ => println! ( "unknown version" ), // required because of #[non_exhaustive]
// ApiVersion::V1 // ERROR: cannot construct variants of #[non_exhaustive] enum
Enums can contain other enums, enabling expressive type hierarchies:
Object ( Vec <( String , Value )>),
fn parse_value (input : & str ) -> Value {
} else if input == "true" {
} else if let Ok (n) = input . parse :: < f64 >() {
Value :: String (input . to_string ())
Connecting { addr : String , attempts : u32 },
Connected { addr : String , stream : std :: net :: TcpStream },
Failed { addr : String , error : String },
fn connect (addr : & str ) -> Self {
ConnectionState :: Connecting {
if let ConnectionState :: Connecting { attempts, .. } = self {
fn is_connected ( & self ) -> bool {
matches! ( self , ConnectionState :: Connected { .. })
Enums do not implement Iterator by default. Use a helper to iterate over variants:
#[derive( Debug , Clone , Copy )]
fn all () -> & "static [Color] {
&[Color::Red, Color::Green, Color::Blue]
for color in Color::all() {
For exhaustive iteration, use the strum crate’s EnumIter derive macro:
use strum :: IntoEnumIterator ;
use strum_macros :: EnumIter ;
#[derive( EnumIter , Debug )]
for dir in Direction :: iter () {
You can implement methods only for specific trait bounds:
impl < T : std :: fmt :: Display > Container < T > {
println! ( "{}" , self . data);
impl < T : std :: fmt :: Debug > Container < T > {
println! ( "{:?}" , self . data);
let c1 = Container { data : 42 };
let c2 = Container { data : "hello" };
Rust has no inheritance. Composition is the primary mechanism for code reuse:
fn new (make : & str , model : & str , hp : u32 , fuel : & str , wheel_count : u8 , wheel_diameter : u16 ) -> Self {
fuel_type : fuel . to_string (),
diameter_inches : wheel_diameter,
model : model . to_string (),
println! ( "{} {} with {}hp {} engine and {}x{} \" wheels" ,
self . engine . horsepower, self . engine . fuel_type,
self . wheels . count, self . wheels . diameter_inches);
fn compute ( & self ) -> i32 {
fn deref ( & self ) -> & Self :: Target {
inner : Inner { value : 21 },
extra : String :: from ( "metadata" ),
assert_eq! (outer . compute (), 42 ); // delegated via Deref
Add ( Box < Expr >, Box < Expr >),
Mul ( Box < Expr >, Box < Expr >),
fn eval (expr : & Expr ) -> i64 {
Expr :: Add (l, r) => eval (l) + eval (r),
Expr :: Mul (l, r) => eval (l) * eval (r),
Box :: new ( Expr :: Literal ( 3 )),
Box :: new ( Expr :: Literal ( 4 )),
Box :: new ( Expr :: Literal ( 5 )),
assert_eq! ( eval ( & expr), 23 );
let leaf = Rc :: new ( Node { value : 1 , children : vec! [] });
let branch = Rc :: new ( Node {
children : vec! [ Rc :: clone ( & leaf)],
assert_eq! ( Rc :: strong_count ( & leaf), 2 );
use serde :: { Serialize , Deserialize };
#[derive( Serialize , Deserialize )]
#[serde(rename = "type" )]
#[serde(alias = "hostname" )]
#[serde(default = "default_port" )]
fn default_port () -> u16 { 8080 }
use serde :: { Serialize , Deserialize };
#[derive( Serialize , Deserialize )]
Request { id : u64 , method : String },
Response { id : u64 , status : u16 },
Error { id : u64 , message : String },
Newtype field access verbosity. Accessing the inner value requires .0Which is not descriptive. Implement methods or Deref to provide a cleaner API, but be aware that Deref weakens type safety.
Builder pattern forgetting required fields. A basic builder that validates in build() only catches missing fields at runtime. Use the typestate pattern to enforce required fields at compile time.
Enum size explosion. An enum’s size is the size of its largest variant plus the discriminant. If one variant is much larger than the others, box it: Large(Box<String>) instead of Large(String).
Typestate pattern and partial moves. The typestate pattern consumes self in each transition. If you need to inspect the state before transitioning, clone or borrow the relevant fields before calling the transition method.
#[non_exhaustive] on structs. #[non_exhaustive] on a struct prevents downstream crates from constructing it and from fully destructuring it (the .. pattern is required). Provide a constructor function to allow construction.
PhantomData affecting drop order. PhantomData<T> makes the compiler treat the struct as if it owns a T. If T has a destructor, the compiler will require the struct to live no longer than T. Use PhantomData<*const T> or PhantomData<fn() -> T> to change variance without affecting the drop check.
Struct update syntax with mutable borrows. Struct { ..other } moves fields. If other is borrowed, you cannot spread from it. Clone the struct first or use individual field copies.
Overusing enum dispatch. Enum dispatch requires all variants to be known at compile time. If you need extensibility (plugins, user-defined types), use trait objects or generics.
ZST edge cases. While ZSTs have zero size, they are not “no type.” A function taking () as a parameter still has a calling convention. A Vec<()> still has length and capacity metadata. ZSTs are real types with real semantics.
repr(transparent) with multiple fields. repr(transparent) requires exactly one non-zero- sized field. If your wrapper has multiple fields, the compiler will reject it. Use a single field wrapper or a different repr attribute.
graph TD
A[Need to model domain concepts?] --> B{Type safety critical?}
B -->|Yes| C{Same representation as another type?}
C -->|Yes| D[Newtype pattern]
C -->|No| E{Complex construction?}
E -->|Yes| F[Builder pattern]
E -->|No| G[Plain struct]
B -->|No| G
A --> H{Need state transitions?}
H -->|Yes, compile-time enforced| I[Typestate pattern]
H -->|Yes, runtime checked| J[State field with enum]
A --> K{Closed set of behaviors?}
K -->|Yes| L[Enum dispatch]
K -->|No, open set| M[Traits + generics or trait objects] This topic covers the core concepts of advanced struct and enum patterns, including underlying theory, practical implementation, and key applications.
Key concepts include:
core concepts and terminology algorithms and computational thinking practical implementation security and ethical considerations applications in the real world Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Problem. Create a Celsius newtype that prevents mixing with Fahrenheit and implements arithmetic operators.
Solution.
#[derive( Debug , Clone , Copy )]
fn from_fahrenheit (f : f64 ) -> Self {
Celsius ((f - 32.0 ) * 5.0 / 9.0 )
fn to_fahrenheit ( self ) -> f64 {
self . 0 * 9.0 / 5.0 + 32.0
impl std :: ops :: Add for Celsius {
fn add ( self , rhs : Self ) -> Self :: Output {
impl std :: ops :: Mul < f64 > for Celsius {
fn mul ( self , rhs : f64 ) -> Self :: Output {
let boiling = Celsius ( 100.0 );
let freezing = Celsius :: from_fahrenheit ( 32.0 );
let doubled = boiling * 2.0 ;
assert_eq! (doubled . 0 , 200.0 );
The newtype has zero runtime cost (the wrapper is optimised away) but the compiler rejects Celsius + Fahrenheit at the type level.
■ \blacksquare ■
Problem. Implement a Shape enum that computes area using exhaustive pattern matching.
Solution.
Rectangle { width : f64 , height : f64 },
Triangle { base : f64 , height : f64 },
Shape :: Circle { radius } => std :: f64 :: consts :: PI * radius * radius,
Shape :: Rectangle { width, height } => width * height,
Shape :: Triangle { base, height } => 0.5 * base * height,
Shape :: Circle { radius : 3.0 },
Shape :: Rectangle { width : 4.0 , height : 5.0 },
Shape :: Triangle { base : 6.0 , height : 2.0 },
println! ( "Area: {:.2}" , shape . area ());
The match is exhaustive: adding a new variant to Shape forces all match expressions to be updated, preventing missing-case bugs.
■ \blacksquare ■
Newtype pattern wraps a type for type safety with zero runtime cost; implement Deref for ergonomic access. Enums with associated data model algebraic data types; exhaustive match prevents missing cases. Builder pattern provides a fluent API for complex struct construction with optional fields. #[non_exhaustive] prevents downstream crates from exhaustively matching on enums or struct fields.Type-state pattern moves validation to compile time by encoding state in generic type parameters. ## Intuition
Advanced Rust patterns leverage the type system for compile-time safety. Builder patterns use method chaining to construct complex objects step by step. The newtype pattern wraps existing types to add semantic meaning without runtime cost. Pattern matching with destructuring extracts data from enums and structs concisely. These patterns combine with ownership and lifetimes to create abstractions that are both flexible and memory-safe without garbage collection.
[[rust/03-structs-enums/structs-and-enums]] - Basic struct and enum patterns [[rust/02-ownership-borrowing/ownership]] - Ownership in complex data structures [[rust/05-traits-generics/traits-and-generics]] - Trait objects and dynamic dispatch [[rust/04-error-handling/error-handling-patterns]] - Error handling pattern combinations