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.
| Receiver | Meaning | Caller binding |
|---|---|---|
self | Takes ownership | Any |
&self | Borrows immutably | Any |
&mut self | Borrows mutably | Mutable |
| (none) | Associated function | Called 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
| Kind | Fields | Access | Use case |
|---|---|---|---|
| Named-field | Named | .field | Domain modeling |
| Tuple | Positional | .0, .1 | Wrappers, newtypes |
| Unit-like | None | — | Markers, trait impls |
Instantiation
| Form | Example |
|---|---|
| Full | User { name: "...", age: 30 } |
| Shorthand | User { name, age } |
| Update | User { age: 31, ..user } |
| Tuple | Color(255, 0, 0) |
| Unit | Marker |
Method Receivers
| Receiver | Borrow | Caller binding |
|---|---|---|
self | Ownership | Any |
&self | Immutable | Any |
&mut self | Mutable | Mutable |
| None | N/A | Type::method() |
Access
| Kind | Access |
|---|---|
| Named field | user.name |
| Tuple field | color.0 |
| Mutable field | user.age = 31 |
Update Syntax
| Form | Effect |
|---|---|
..user | Move remaining fields |
..p1 (Copy fields) | Copy remaining fields |
| Partial move | Original 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Missing field | Incomplete instantiation | Provide all fields |
| Type mismatch | Field type differs | Check the field types |
| Moved field accessed | Update syntax moved it | Use the new struct |
| Cannot modify field | Binding not mut | Declare the binding mut |
| Tuple struct access | Used .name | Use .0, .1 |
| Method not found | Not in an impl block | Define 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
| Item | Value |
|---|---|
| Named-field struct | struct User { name: String } |
| Tuple struct | struct Color(i32, i32, i32) |
| Unit-like struct | struct Marker; |
| Instantiation | User { name: "...", age: 30 } |
| Field init shorthand | User { name, age } |
| Update syntax | User { age: 31, ..user } |
| Field access | user.name |
| Tuple field access | color.0 |
| Method | fn area(&self) |
| Associated function | fn new(...) -> Self |
| Mutability | On 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.namefor named fields,color.0for tuple fields. The binding must bemutfor the fields to be modified. - Methods are defined in an
implblock. The receiver isself,&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
Copyare moved from the original struct, and the original is partially invalid afterward. Fields that areCopyare copied. - Mutability is on the binding, not the field. A struct must be bound with
mutfor any of its fields to be modified. There is nomutfield 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!