Structs in Rust
Define structs, implement methods and associated functions, use #[derive], and understand the different struct forms.
Defining Structs
Structs are the primary way to group related data under a single named type. They solve the same problem as classes in other languages — bundling data that belongs together — but Rust keeps data definition and behaviour separate: the struct keyword defines the fields, and a separate impl block adds methods. Every field must be initialised when creating an instance; there are no implicit zero-values or defaults.
struct Rectangle {
width: f64,
height: f64,
}
fn main() {
let rect = Rectangle {
width: 30.0,
height: 50.0,
};
// Access fields with dot notation
println!("width: {}, height: {}", rect.width, rect.height);
}
Mutable Structs
In Rust, mutability is a property of the binding, not the field. The entire struct instance must be declared mut to modify any field — you cannot mark individual fields as mutable. This keeps the mutability model consistent with the rest of the language.
fn main() {
let mut rect = Rectangle { width: 10.0, height: 20.0 };
rect.width = 15.0; // allowed because rect is mut
println!("{}", rect.width); // 15
}
Struct Update Syntax
When creating a new instance that shares most fields with an existing one, the ..existing syntax copies the remaining fields. This is concise and avoids repeating field names that have not changed. Note that .. performs a move for non-Copy fields, so the source struct may become partially moved.
struct User {
username: String,
email: String,
active: bool,
login_count: u32,
}
fn main() {
let user1 = User {
username: String::from("alice"),
email: String::from("alice@example.com"),
active: true,
login_count: 1,
};
// Only specify fields that differ; ..user1 fills in the rest
let user2 = User {
email: String::from("bob@example.com"),
username: String::from("bob"),
..user1 // active and login_count are copied from user1
};
println!("{} ({})", user2.username, user2.email);
}
Tuple Structs
Tuple structs have a name but no field names — their fields are accessed by index. They are useful when you want the type-safety benefit of a named type (so Color and Point cannot be confused despite having the same shape) without the overhead of field names.
struct Color(u8, u8, u8);
struct Point(f64, f64, f64);
fn main() {
let red = Color(255, 0, 0);
let origin = Point(0.0, 0.0, 0.0);
// Access by positional index with dot notation
println!("Red: ({}, {}, {})", red.0, red.1, red.2);
println!("Origin: ({}, {}, {})", origin.0, origin.1, origin.2);
}
Color and Point are distinct types even though they have the same field layout — the compiler will not let you pass a Color where a Point is expected.
Unit-Like Structs
Structs with no fields are called unit-like structs. They carry no data but are still a distinct type, which makes them useful as marker types or as the implementing type for a trait that has no state requirements.
struct AlwaysEqual; // no fields, no parentheses, no braces
fn main() {
let _subject = AlwaysEqual;
}
impl Blocks — Methods
Methods are defined inside impl blocks and are called with dot notation on instances. The first parameter determines the ownership semantics: &self for read-only access, &mut self to modify the instance, and self to consume it.
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
// &self — shared borrow: read-only, does not consume the Rectangle
fn area(&self) -> f64 {
self.width * self.height
}
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
fn is_square(&self) -> bool {
(self.width - self.height).abs() < f64::EPSILON
}
// &mut self — mutable borrow: can modify fields, does not consume
fn scale(&mut self, factor: f64) {
self.width *= factor;
self.height *= factor;
}
// self — takes ownership: consumes the Rectangle, caller can no longer use it
fn into_tuple(self) -> (f64, f64) {
(self.width, self.height)
}
}
fn main() {
let mut rect = Rectangle { width: 10.0, height: 5.0 };
println!("area: {}", rect.area()); // 50
println!("perimeter: {}", rect.perimeter()); // 30
println!("square: {}", rect.is_square()); // false
rect.scale(2.0);
println!("after scale: {}x{}", rect.width, rect.height); // 20x10
let (w, h) = rect.into_tuple(); // rect is consumed here
println!("{} {}", w, h);
}
Associated Functions (Static Methods)
Associated functions live in an impl block but do not take self. They are called on the type itself with :: syntax rather than on an instance. The conventional name for a constructor is new, though Rust does not enforce this — any associated function that returns Self can serve as a constructor.
impl Rectangle {
// Constructor — returns a new Rectangle, called as Rectangle::new(...)
fn new(width: f64, height: f64) -> Rectangle {
Rectangle { width, height } // shorthand when variable name matches field name
}
// Alternative constructor for the special case of a square
fn square(size: f64) -> Rectangle {
Rectangle { width: size, height: size }
}
}
fn main() {
let rect = Rectangle::new(10.0, 5.0);
let sq = Rectangle::square(4.0);
println!("{}x{}", rect.width, rect.height); // 10x5
println!("{}x{}", sq.width, sq.height); // 4x4
}
Rust uses :: for associated functions everywhere in the standard library: String::new(), Vec::with_capacity(), HashMap::new().
Multiple impl Blocks
A type can have multiple impl blocks. This is useful for organisation — for example, separating core methods from trait implementations — and is required when implementing generic traits.
impl Rectangle {
fn area(&self) -> f64 { self.width * self.height }
}
impl Rectangle {
// A second impl block for methods that depend on another Rectangle
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
Deriving Common Traits
The #[derive] attribute instructs the compiler to auto-implement standard traits. This eliminates boilerplate for common operations like printing, cloning, and equality testing. Derivable traits only work if all fields also implement the trait.
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
fn main() {
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1.clone(); // Clone gives us a deep copy
println!("{:?}", p1); // Debug: Point { x: 1.0, y: 2.0 }
println!("{:#?}", p1); // Debug with pretty-printing
println!("{}", p1 == p2); // PartialEq: true
}
Commonly derived traits:
| Trait | Provides |
|---|---|
Debug | {:?} formatting |
Clone | .clone() method |
Copy | Copy semantics (requires Clone) |
PartialEq | == and != operators |
Eq | Total equality (for HashMap keys etc.) |
PartialOrd | <, >, <=, >= |
Ord | Total ordering (for sorting) |
Hash | Use as HashMap/HashSet key |
Default | Type::default() constructor |
Display Formatting
Debug is for developer output. Implement std::fmt::Display when you want to control how your type appears in user-facing output with {}. Display is not derivable — you write it yourself to define exactly the format.
use std::fmt;
struct Matrix {
a: f64, b: f64,
c: f64, d: f64,
}
impl fmt::Display for Matrix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// write! into the formatter — return its Result
write!(f, "[ {:.2} {:.2} ]\n[ {:.2} {:.2} ]",
self.a, self.b, self.c, self.d)
}
}
fn main() {
let m = Matrix { a: 1.0, b: 2.0, c: 3.0, d: 4.0 };
println!("{}", m);
// [ 1.00 2.00 ]
// [ 3.00 4.00 ]
}
A Complete Example: 2D Vector
This example brings together struct definition, multiple impl blocks, derived traits, and operator overloading via the Add trait:
#[derive(Debug, Clone, Copy, PartialEq)]
struct Vec2 {
x: f64,
y: f64,
}
impl Vec2 {
fn new(x: f64, y: f64) -> Self { Self { x, y } }
fn zero() -> Self { Self::new(0.0, 0.0) }
fn length(&self) -> f64 {
// Euclidean distance from origin
(self.x * self.x + self.y * self.y).sqrt()
}
fn normalize(&self) -> Self {
let len = self.length();
Self::new(self.x / len, self.y / len)
}
fn dot(&self, other: &Vec2) -> f64 {
self.x * other.x + self.y * other.y
}
}
// Implement the + operator by implementing the Add trait
impl std::ops::Add for Vec2 {
type Output = Vec2;
fn add(self, other: Vec2) -> Vec2 {
Vec2::new(self.x + other.x, self.y + other.y)
}
}
fn main() {
let a = Vec2::new(3.0, 4.0);
let b = Vec2::new(1.0, 0.0);
println!("length of a: {}", a.length()); // 5
println!("a + b: {:?}", a + b); // Vec2 { x: 4.0, y: 4.0 }
println!("dot product: {}", a.dot(&b)); // 3
println!("normalized a: {:?}", a.normalize());
}