Structs & impl
Structs are how Rust organizes data. They are not classes — there is no inheritance, no constructors with special syntax, no this pointer that changes meaning depending on context. A struct is data. An impl block attaches behavior to that data. The separation is clean and intentional.
Defining Structs
struct User {
name: String,
email: String,
active: bool,
login_count: u64,
}
fn main() {
let user = User {
name: String::from("Alice"),
email: String::from("alice@example.com"),
active: true,
login_count: 1,
};
println!("{} ({}) - logins: {}", user.name, user.email, user.login_count);
}
Alice (alice@example.com) - logins: 1
All fields must be initialized. There are no default values unless you implement Default. This eliminates the "partially initialized object" bugs that plague languages with constructors.
Field Access & Mutation
Structs are immutable by default, like everything in Rust. Mutability applies to the entire struct — you cannot make individual fields mutable.
struct Point {
x: f64,
y: f64,
}
fn main() {
let mut p = Point { x: 1.0, y: 2.0 };
p.x = 3.0;
println!("({}, {})", p.x, p.y);
// Struct update syntax: create a new struct from an existing one
let p2 = Point { x: 10.0, ..p };
println!("({}, {})", p2.x, p2.y);
}
(3.0, 2.0)
(10.0, 2.0)
The ..p syntax copies remaining fields from p. For types that implement Copy, this copies. For heap types, it moves — so p might be partially invalidated after the update.
Methods with impl
Methods are defined in impl blocks. The first parameter is always some form of self:
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
// &self: borrows the struct immutably (most common)
fn area(&self) -> f64 {
self.width * self.height
}
// &mut self: borrows the struct mutably
fn scale(&mut self, factor: f64) {
self.width *= factor;
self.height *= factor;
}
// self: takes ownership (consumes the struct)
fn into_square(self) -> Rectangle {
let side = self.width.max(self.height);
Rectangle { width: side, height: side }
}
}
fn main() {
let mut rect = Rectangle { width: 10.0, height: 5.0 };
println!("Area: {}", rect.area());
rect.scale(2.0);
println!("Scaled area: {}", rect.area());
let square = rect.into_square();
// rect is consumed — cannot use it anymore
println!("Square area: {}", square.area());
}
Area: 50
Scaled area: 200
Square area: 400
The self parameter type tells you everything about what the method does to the struct:
&self— reads the struct&mut self— modifies the structself— consumes the struct
Associated Functions (Constructors)
Functions in an impl block that do not take self are called associated functions. They are accessed with :: syntax and commonly serve as constructors:
struct Color {
r: u8,
g: u8,
b: u8,
}
impl Color {
fn new(r: u8, g: u8, b: u8) -> Self {
Color { r, g, b }
}
fn red() -> Self {
Color { r: 255, g: 0, b: 0 }
}
fn white() -> Self {
Color { r: 255, g: 255, b: 255 }
}
fn hex(&self) -> String {
format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
}
}
fn main() {
let c1 = Color::new(100, 149, 237);
let c2 = Color::red();
println!("{} {}", c1.hex(), c2.hex());
}
#6495ed #ff0000
Self is an alias for the struct type within the impl block. Color::new is the conventional name for the primary constructor.
The Builder Pattern
When structs have many optional fields, the builder pattern provides a clean API:
struct ServerConfig {
host: String,
port: u16,
max_connections: u32,
timeout_secs: u64,
tls_enabled: bool,
}
struct ServerConfigBuilder {
host: String,
port: u16,
max_connections: u32,
timeout_secs: u64,
tls_enabled: bool,
}
impl ServerConfigBuilder {
fn new(host: &str, port: u16) -> Self {
ServerConfigBuilder {
host: host.to_string(),
port,
max_connections: 100,
timeout_secs: 30,
tls_enabled: false,
}
}
fn max_connections(mut self, n: u32) -> Self {
self.max_connections = n;
self
}
fn timeout(mut self, secs: u64) -> Self {
self.timeout_secs = secs;
self
}
fn tls(mut self, enabled: bool) -> Self {
self.tls_enabled = enabled;
self
}
fn build(self) -> ServerConfig {
ServerConfig {
host: self.host,
port: self.port,
max_connections: self.max_connections,
timeout_secs: self.timeout_secs,
tls_enabled: self.tls_enabled,
}
}
}
fn main() {
let config = ServerConfigBuilder::new("0.0.0.0", 8080)
.max_connections(500)
.tls(true)
.timeout(60)
.build();
println!("{}:{} (max: {}, tls: {}, timeout: {}s)",
config.host, config.port, config.max_connections,
config.tls_enabled, config.timeout_secs);
}
0.0.0.0:8080 (max: 500, tls: true, timeout: 60s)
Each builder method takes mut self (consuming and returning the builder) enabling method chaining. Default values are set in new, and callers override only what they need.
derive: Your Best Friend
Rust can automatically implement common traits with #[derive]:
#[derive(Debug, Clone, PartialEq)]
struct Coordinate {
lat: f64,
lon: f64,
}
fn main() {
let home = Coordinate { lat: 40.7128, lon: -74.0060 };
let work = Coordinate { lat: 40.7580, lon: -73.9855 };
// Debug: enables {:?} formatting
println!("{:?}", home);
// Clone: explicit deep copy
let saved = home.clone();
// PartialEq: enables == comparison
println!("Same location? {}", home == saved);
println!("Home is work? {}", home == work);
}
Coordinate { lat: 40.7128, lon: -74.006 }
Same location? true
Home is work? false
The most commonly derived traits:
| Trait | What It Does |
|---|---|
Debug |
Enables {:?} formatting for printing |
Clone |
Enables .clone() for explicit copies |
PartialEq |
Enables == and != comparison |
Eq |
Marks total equality (all values comparable) |
Hash |
Enables use as HashMap keys |
Default |
Provides a default value via Type::default() |
PartialOrd / Ord |
Enables <, >, sorting |
Derive them liberally. There is no runtime cost — the compiler generates the implementation at compile time.
Multiple impl Blocks
You can split methods across multiple impl blocks. This is useful for organizing code or conditional compilation:
#[derive(Debug)]
struct Database {
url: String,
connected: bool,
}
impl Database {
fn new(url: &str) -> Self {
Database {
url: url.to_string(),
connected: false,
}
}
}
impl Database {
fn connect(&mut self) {
self.connected = true;
println!("Connected to {}", self.url);
}
fn is_connected(&self) -> bool {
self.connected
}
}
fn main() {
let mut db = Database::new("postgres://localhost/mydb");
println!("Connected: {}", db.is_connected());
db.connect();
println!("Connected: {}", db.is_connected());
}
Connected: false
Connected to postgres://localhost/mydb
Connected: true
Tuple Structs & Unit Structs
Tuple structs have unnamed fields. Useful for newtype wrappers:
struct Meters(f64);
struct Seconds(f64);
impl Meters {
fn new(value: f64) -> Self {
Meters(value)
}
}
impl Seconds {
fn new(value: f64) -> Self {
Seconds(value)
}
}
fn speed(distance: &Meters, time: &Seconds) -> f64 {
distance.0 / time.0
}
fn main() {
let d = Meters::new(100.0);
let t = Seconds::new(9.58);
println!("Speed: {:.2} m/s", speed(&d, &t));
// This won't compile — types are distinct even though both wrap f64:
// speed(&t, &d); // error: expected Meters, found Seconds
}
Speed: 10.44 m/s
The newtype pattern uses the type system to prevent mixing up values that have the same underlying type. Meters and Seconds are both f64, but the compiler treats them as different types.
Unit structs have no fields at all. They are used as markers:
struct Production;
struct Development;
// Used as type-level markers for configuration
Common Pitfalls
- Making every struct public with all fields public. Expose the minimum necessary. Keep fields private and provide methods. This lets you change the internal representation later without breaking callers.
- Not deriving Debug. Every struct should derive
Debugunless you have a specific reason not to. You will need it for printing during development. - Using inheritance patterns. Rust does not have inheritance. If you want shared behavior, use traits. If you want shared data, use composition (a struct containing another struct).
- Forgetting that
selfmethods consume the struct. If a method takesself(not&self), the struct is gone after the call. This is intentional for transformation methods but surprising if you are not expecting it. - Not using the builder pattern when constructors get complex. If a struct has more than 3-4 fields with sensible defaults, a builder is cleaner than a constructor with many parameters.
- Implementing Display but not Debug. Derive
Debugfor developer output, implementDisplaymanually for user-facing output. They serve different purposes.
Key Takeaways
- Structs hold data.
implblocks attach behavior. The separation is intentional and clean. &self,&mut self, andselfcommunicate intent: reading, modifying, or consuming.- Associated functions (no
selfparameter) serve as constructors.Type::new()is the convention. - The builder pattern handles complex construction with many optional fields.
#[derive(Debug, Clone, PartialEq)]gives you common functionality for free. Use it.- Composition, not inheritance. Structs containing other structs. Traits for shared behavior.