| |

Rust 19 🦀 Structs Definition and Instantiation

A struct is a named grouping of related values. Where a tuple groups values by position, a struct groups them by name. The name makes the code readable, and the type makes it checkable. A struct is the primary tool for modeling a domain in Rust: a User, a Point, a Config, a Request. The definition declares the fields and their types, and the instantiation creates a value of the struct type with specific field values. Structs are the foundation on which methods, traits, and enums build.

Rust has three kinds of structs, each with a different purpose. A named-field struct has fields with names, and it is the common case for domain modeling. A tuple struct has fields without names, and it is used when the field meaning is obvious from the type or when the struct is a thin wrapper. A unit-like struct has no fields, and it is used as a marker type or to implement a trait without holding data. The three kinds share the same syntax for definition and instantiation but differ in how the fields are accessed.

This chapter covers the definition of each struct kind, the instantiation syntax, the field init shorthand, the update syntax, the tuple struct and unit-like struct, the methods that operate on structs, and the patterns that make structs readable and maintainable.

Key point: A struct groups related values under a name. A named-field struct has fields with names and is the common case. A tuple struct has fields without names and is used for wrappers. A unit-like struct has no fields and is used as a marker. Instantiation creates a value with specific field values. The field init shorthand, the update syntax, and the impl block are the tools that make structs ergonomic.


Why structs exist

The naming problem. A tuple (String, String, u32) does not say what the fields mean. A struct User { name: String, email: String, age: u32 } does. The names document the code and catch mistakes at compile time. A call that passes the arguments in the wrong order is a type error in the struct, and the same call with a tuple would silently compile.

The modeling problem. A domain has entities, and entities have attributes. A struct is the mechanism that models the entity and its attributes in the type system. The compiler checks that every attribute has a value and that the types match.

The method problem. A struct can have methods. The methods are defined in an impl block, and they operate on the struct’s fields. This is the mechanism that turns a struct from a passive data holder into an object with behavior.

The trait problem. A struct can implement traits. The traits define shared behavior, and the struct’s implementation provides the specific behavior. This is the mechanism that makes Rust’s generics and operator overloading work.

The ownership problem. A struct’s fields have the same ownership rules as any other value. A field that holds a String owns the string. A field that holds a &str borrows the string. The struct’s lifetime is the lifetime of its borrows. This is what makes structs safe to pass around and store.


a. Named-field structs

A named-field struct is declared with the struct keyword, a name, and a list of fields in braces. Each field has a name and a type.

struct User {
    name: String,
    email: String,
    age: u32,
    active: bool,
}

The struct definition is a type declaration. It does not create a value. It declares what a User looks like: four fields with the specified types.

Instantiation creates a value of the struct type. Each field is assigned a value, and the order does not matter.

let user = User {
    name: String::from("Alice"),
    email: String::from("alice@example.com"),
    age: 30,
    active: true,
};

The user variable holds a User value. The fields are accessed with dot notation:

println!("{}", user.name);
println!("{}", user.age);

A field can be modified if the binding is mutable:

let mut user = User { /* ... */ };
user.age = 31;
user.active = false;

The struct itself must be mut for any of its fields to be modified. A mut field does not exist in Rust; mutability is a property of the binding, not the field.


b. The field init shorthand

When a variable has the same name as a field, the field can be initialized with the variable’s name alone.

let name = String::from("Alice");
let email = String::from("alice@example.com");
let age = 30;
let active = true;

let user = User { name, email, age, active };

The shorthand name is equivalent to name: name. This is common in constructor functions.

fn new_user(name: String, email: String, age: u32) -> User {
    User {
        name,
        email,
        age,
        active: true,
    }
}

The function receives the parameters, and the struct initialization uses the shorthand for the fields that share the parameter names. The active field is set explicitly.


c. The struct update syntax

The struct update syntax creates a new struct from an existing one, overriding the fields that change.

let user2 = User {
    email: String::from("bob@example.com"),
    ..user
};

The ..user is the update syntax. It takes the remaining fields from user. The email field is overridden, and the other fields are moved from user.

The update syntax moves the fields that are not overridden. If user is used after the update, the fields that were moved are no longer accessible. For fields that implement Copy, the update copies them, and user remains partially valid.

struct Point { x: i32, y: i32 }

let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, ..p1 };  // p1 is still valid because i32 is Copy
println!("{}", p1.y);  // 2

For fields that are not Copy, the update moves them, and the original struct is partially moved:

struct User { name: String, age: u32 }

let user1 = User { name: String::from("Alice"), age: 30 };
let user2 = User { name: String::from("Bob"), ..user1 };
// println!("{}", user1.name);  // Error: name was moved
println!("{}", user1.age);   // OK: age is Copy

The name field was moved into user2, so user1.name is no longer accessible. The age field was copied, so user1.age is still accessible.


d. Tuple structs

A tuple struct has fields without names. The fields are accessed by position, like a tuple.

struct Color(i32, i32, i32);
struct Point(f64, f64);

let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);

The Color struct has three i32 fields, accessed as .0, .1, and .2. The Point struct has two f64 fields, accessed as .0 and .1.

println!("{}", black.0);
println!("{}", origin.1);

A tuple struct is useful when the fields have an obvious meaning from the type, or when the struct is a thin wrapper that should be a distinct type from its inner value.

struct Meters(f64);
struct Feet(f64);

fn distance_m(m: Meters) { }
// distance_m(Feet(3.0));  // Error: expected Meters, found Feet

The two structs have the same inner type but are distinct types. The compiler rejects passing a Feet where a Meters is expected. This is the newtype pattern: a wrapper that adds type safety without adding runtime cost.


e. Unit-like structs

A unit-like struct has no fields. It is declared with the struct keyword and a semicolon.

struct AlwaysEqual;

let subject = AlwaysEqual;

The unit-like struct has no fields and no data. It is used as a marker type, as a placeholder, or to implement a trait without holding state.

struct Marker;

impl Marker {
    fn describe(&self) -> &str {
        "I am a marker"
    }
}

let m = Marker;
println!("{}", m.describe());

The unit-like struct is a value, and it can have methods. It is useful when the type itself is the information, and no data needs to be stored.


f. Structs with methods

Methods are defined in an impl block. The block is separate from the struct definition, and it can appear anywhere in the same module.

struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }

    fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }

    fn new(width: f64, height: f64) -> Rectangle {
        Rectangle { width, height }
    }
}

The impl block contains the methods. The first parameter of a method is self, &self, or &mut self.

ReceiverMeaningCaller binding
selfTakes ownershipAny
&selfBorrows immutablyAny
&mut selfBorrows mutablyMutable
(none)Associated functionCalled as Rectangle::new(...)

An associated function does not take self. It is called with the type name and ::, like a static method in other languages. The new function is the conventional constructor.

let rect = Rectangle::new(3.0, 4.0);
println!("{}", rect.area());       // 12.0
println!("{}", rect.perimeter());  // 14.0

let mut rect = Rectangle::new(3.0, 4.0);
rect.scale(2.0);
println!("{}", rect.area());       // 48.0

The area and perimeter methods take &self, so they can be called on any binding. The scale method takes &mut self, so the binding must be mut.


Complete Example Session

// ============================================
// PART 1: DEFINE A STRUCT
// ============================================
struct User {
    name: String,
    email: String,
    age: u32,
    active: bool,
}
// ============================================
// PART 2: INSTANTIATE A STRUCT
// ============================================
fn main() {
    let user = User {
        name: String::from("Alice"),
        email: String::from("alice@example.com"),
        age: 30,
        active: true,
    };

    println!("{}", user.name);
    println!("{}", user.age);
}
// ============================================
// PART 3: MUTABLE STRUCT
// ============================================
fn main() {
    let mut user = User {
        name: String::from("Alice"),
        email: String::from("alice@example.com"),
        age: 30,
        active: true,
    };

    user.age = 31;
    user.active = false;
    println!("{} {}", user.age, user.active);
}
// ============================================
// PART 4: FIELD INIT SHORTHAND
// ============================================
fn new_user(name: String, email: String, age: u32) -> User {
    User {
        name,
        email,
        age,
        active: true,
    }
}
// ============================================
// PART 5: STRUCT UPDATE SYNTAX
// ============================================
fn main() {
    let user1 = User {
        name: String::from("Alice"),
        email: String::from("alice@example.com"),
        age: 30,
        active: true,
    };

    let user2 = User {
        email: String::from("bob@example.com"),
        ..user1
    };

    println!("{}", user2.email);
}
// ============================================
// PART 6: TUPLE STRUCT
// ============================================
struct Color(i32, i32, i32);
struct Point(f64, f64);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0.0, 0.0);

    println!("{} {} {}", black.0, black.1, black.2);
    println!("{} {}", origin.0, origin.1);
}
// ============================================
// PART 7: NEWTYPE PATTERN
// ============================================
struct Meters(f64);
struct Feet(f64);

fn distance_m(m: Meters) {
    println!("{} meters", m.0);
}

fn main() {
    distance_m(Meters(3.0));
    // distance_m(Feet(3.0));  // Error
}
// ============================================
// PART 8: UNIT-LIKE STRUCT
// ============================================
struct Marker;

impl Marker {
    fn describe(&self) -> &str {
        "I am a marker"
    }
}

fn main() {
    let m = Marker;
    println!("{}", m.describe());
}
// ============================================
// PART 9: METHODS
// ============================================
struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }
}

fn main() {
    let mut rect = Rectangle { width: 3.0, height: 4.0 };
    println!("{}", rect.area());  // 12.0
    rect.scale(2.0);
    println!("{}", rect.area());  // 48.0
}
// ============================================
// PART 10: ASSOCIATED FUNCTION
// ============================================
impl Rectangle {
    fn new(width: f64, height: f64) -> Rectangle {
        Rectangle { width, height }
    }

    fn square(size: f64) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

fn main() {
    let rect = Rectangle::new(3.0, 4.0);
    let sq = Rectangle::square(5.0);
    println!("{} {}", rect.area(), sq.area());
}

These ten parts cover defining a struct, instantiating a struct, a mutable struct, the field init shorthand, the struct update syntax, a tuple struct, the newtype pattern, a unit-like struct, methods, and associated functions.


Quick Reference

Struct Kinds

KindFieldsAccessUse case
Named-fieldNamed.fieldDomain modeling
TuplePositional.0, .1Wrappers, newtypes
Unit-likeNone—Markers, trait impls

Instantiation

FormExample
FullUser { name: "...", age: 30 }
ShorthandUser { name, age }
UpdateUser { age: 31, ..user }
TupleColor(255, 0, 0)
UnitMarker

Method Receivers

ReceiverBorrowCaller binding
selfOwnershipAny
&selfImmutableAny
&mut selfMutableMutable
NoneN/AType::method()

Access

KindAccess
Named fielduser.name
Tuple fieldcolor.0
Mutable fielduser.age = 31

Update Syntax

FormEffect
..userMove remaining fields
..p1 (Copy fields)Copy remaining fields
Partial moveOriginal partially invalid

Best Practices

✅ Do This:

// Use named fields for domain types
struct User { name: String, age: u32 }

// Use the field init shorthand
User { name, age }

// Use the update syntax
let user2 = User { age: 31, ..user1 };

// Use tuple structs for newtypes
struct Meters(f64);

// Use unit-like structs for markers
struct Marker;

// Define methods in an impl block
impl User {
    fn greet(&self) -> String { format!("Hi, {}", self.name) }
}

❌ Don’t Do This:

// Use a tuple for a domain type
fn create(name: String, email: String, age: u32) { }  // ❌ use a struct

// Forget the type in a struct definition
struct User { name, age }  // ❌ types required

// Use a mutable field
struct User { mut age: u32 }  // ❌ mutability is on the binding

// Access a moved field
let user2 = User { ..user1 };
println!("{}", user1.name);  // ❌ moved

// Use an associated function as a method
Rectangle::area();  // ❌ requires self

Common Pitfalls

PitfallWhy It HappensFix
Missing fieldIncomplete instantiationProvide all fields
Type mismatchField type differsCheck the field types
Moved field accessedUpdate syntax moved itUse the new struct
Cannot modify fieldBinding not mutDeclare the binding mut
Tuple struct accessUsed .nameUse .0, .1
Method not foundNot in an impl blockDefine the method in impl

Real-World Examples

1. Domain Struct

struct User {
    name: String,
    email: String,
    age: u32,
}

2. Instantiation

let user = User {
    name: String::from("Alice"),
    email: String::from("alice@example.com"),
    age: 30,
};

3. Shorthand

let user = User { name, email, age };

4. Update Syntax

let user2 = User { age: 31, ..user1 };

5. Tuple Struct

struct Meters(f64);

6. Unit-like Struct

struct Marker;

7. Method

impl User {
    fn greet(&self) -> String { format!("Hi, {}", self.name) }
}

8. Associated Function

impl User {
    fn new(name: String, age: u32) -> User { User { name, age } }
}

9. Mutable Method

impl User {
    fn birthday(&mut self) { self.age += 1; }
}

10. Debug Derive

#[derive(Debug)]
struct Point { x: i32, y: i32 }
println!("{:?}", Point { x: 1, y: 2 });

Visual

Struct Kinds

┌──────────────────────────────────────────────────────────────┐
│  NAMED-FIELD:                                                │
│  struct User {                                               │
│    name: String,                                             │
│    age: u32,                                                 │
│  }                                                           │
│  └── Access: user.name, user.age                             │
│                                                              │
│  TUPLE:                                                      │
│  struct Color(i32, i32, i32);                                │
│  └── Access: color.0, color.1, color.2                       │
│                                                              │
│  UNIT-LIKE:                                                  │
│  struct Marker;                                              │
│  └── Access: the value itself                                │
└──────────────────────────────────────────────────────────────┘

Instantiation

┌──────────────────────────────────────────────────────────────┐
│  let user = User {                                           │
│    name: String::from("Alice"),                              │
│    email: String::from("alice@example.com"),                 │
│    age: 30,                                                  │
│    active: true,                                             │
│  };                                                          │
│                                                              │
│  Each field is assigned a value.                             │
│  The order does not matter.                                  │
│  The field init shorthand uses the variable name.            │
└──────────────────────────────────────────────────────────────┘

Update Syntax

┌──────────────────────────────────────────────────────────────┐
│  let user1 = User { name: "Alice", age: 30, active: true };  │
│  let user2 = User { age: 31, ..user1 };                      │
│                                                              │
│  user2.name   = user1.name   (moved)                         │
│  user2.age    = 31           (overridden)                    │
│  user2.active = user1.active (moved)                         │
│                                                              │
│  user1.name is no longer accessible.                         │
└──────────────────────────────────────────────────────────────┘

Method Receivers

┌──────────────────────────────────────────────────────────────┐
│  fn area(&self) -> f64                                       │
│  └── Borrows immutably; caller binding can be any            │
│                                                              │
│  fn scale(&mut self, factor: f64)                            │
│  └── Borrows mutably; caller binding must be mut             │
│                                                              │
│  fn new(w: f64, h: f64) -> Rectangle                         │
│  └── Associated function; called as Rectangle::new(...)      │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Named-field structstruct User { name: String }
Tuple structstruct Color(i32, i32, i32)
Unit-like structstruct Marker;
InstantiationUser { name: "...", age: 30 }
Field init shorthandUser { name, age }
Update syntaxUser { age: 31, ..user }
Field accessuser.name
Tuple field accesscolor.0
Methodfn area(&self)
Associated functionfn new(...) -> Self
MutabilityOn the binding, not the field

Key takeaways:

  • A struct groups related values under a name. The named-field struct is the common case for domain modeling. The tuple struct is for wrappers and newtypes. The unit-like struct is for markers.
  • Instantiation creates a value with specific field values. The field init shorthand uses the variable name for the field. The update syntax takes the remaining fields from an existing struct.
  • Field access uses dot notation. user.name for named fields, color.0 for tuple fields. The binding must be mut for the fields to be modified.
  • Methods are defined in an impl block. The receiver is self, &self, or &mut self. An associated function has no receiver and is called with the type name.
  • The newtype pattern uses a tuple struct. A wrapper around an inner type creates a distinct type that the compiler checks. There is no runtime cost.
  • The update syntax moves fields. Fields that are not Copy are moved from the original struct, and the original is partially invalid afterward. Fields that are Copy are copied.
  • Mutability is on the binding, not the field. A struct must be bound with mut for any of its fields to be modified. There is no mut field in Rust.

Remember: Structs are the foundation of Rust’s data modeling. A struct declares a type with named fields, and instantiation creates values of that type. The three kinds — named-field, tuple, and unit-like — cover the common cases: domain entities, wrappers, and markers. The field init shorthand and the update syntax make instantiation concise. The impl block adds methods and associated functions, turning the struct from a data holder into an object with behavior. The newtype pattern uses tuple structs to add type safety without runtime cost. Understanding structs is understanding how Rust models the domain.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!