Complete Guide to Rust Structs

Introduction to Rust Structs

Structs (short for "structures") are custom data types that let you name and package together multiple related values. They are one of Rust's primary ways to create complex data types and are fundamental to building well-organized, type-safe programs. Structs enable you to create meaningful abstractions that model real-world entities and concepts.

Key Concepts

  • Named Fields: Each field has a name and type
  • Custom Types: Create your own data types
  • Methods: Define behavior associated with structs
  • Pattern Matching: Destructure and match on structs
  • Memory Efficiency: Contiguous memory layout (usually)
  • Ownership: Each field follows Rust's ownership rules

1. Defining and Instantiating Structs

Basic Struct Definition

// Define a struct
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
// Create an instance
let user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// Access fields
println!("Email: {}", user1.email);
println!("Username: {}", user1.username);
println!("Active: {}", user1.active);
println!("Sign-in count: {}", user1.sign_in_count);
}

Mutable Structs

struct Point {
x: i32,
y: i32,
}
fn main() {
// Mutable instance
let mut point = Point { x: 0, y: 0 };
println!("Initial point: ({}, {})", point.x, point.y);
// Modify fields
point.x = 5;
point.y = 10;
println!("Modified point: ({}, {})", point.x, point.y);
// Cannot make individual fields mutable
// Only the entire instance can be mutable
}

Field Init Shorthand

struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn build_user(email: String, username: String) -> User {
// Field init shorthand - no need to write email: email
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
fn main() {
let user = build_user(
String::from("[email protected]"),
String::from("alice123"),
);
println!("User: {} ({})", user.username, user.email);
}

Struct Update Syntax

#[derive(Debug)]
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// Create new instance using values from user1
let user2 = User {
email: String::from("[email protected]"),
username: String::from("anotherusername567"),
..user1  // Use remaining fields from user1
};
println!("user1: {:?}", user1);
println!("user2: {:?}", user2);
// Note: String fields are moved, so user1 may no longer be usable
// depending on field types
}

2. Tuple Structs

Basic Tuple Structs

// Tuple structs - fields have no names, only types
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
struct RGB(u8, u8, u8);
fn main() {
// Create instances
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
let red = RGB(255, 0, 0);
// Access fields by index
println!("Black: ({}, {}, {})", black.0, black.1, black.2);
println!("Origin: ({}, {}, {})", origin.0, origin.1, origin.2);
println!("Red: ({}, {}, {})", red.0, red.1, red.2);
// Different tuple struct types are distinct
// let wrong = Point(black.0, black.1, black.2); // Works, but semantically wrong
// Color and Point are different types even though they have same structure
// Destructuring tuple structs
let Color(r, g, b) = black;
println!("Destructured: {}, {}, {}", r, g, b);
let Point(x, y, z) = origin;
println!("Coordinates: ({}, {}, {})", x, y, z);
}

Newtype Pattern

// Newtype pattern - tuple struct with one field
struct Meters(f64);
struct Kilograms(f64);
struct Seconds(u64);
impl Meters {
fn to_feet(&self) -> f64 {
self.0 * 3.28084
}
}
impl Kilograms {
fn to_pounds(&self) -> f64 {
self.0 * 2.20462
}
}
fn calculate_speed(distance: Meters, time: Seconds) -> f64 {
distance.0 / time.0 as f64
}
fn main() {
let distance = Meters(100.0);
let time = Seconds(9);
let weight = Kilograms(70.0);
println!("Distance: {}m ({}ft)", distance.0, distance.to_feet());
println!("Weight: {}kg ({}lbs)", weight.0, weight.to_pounds());
let speed = calculate_speed(distance, time);
println!("Speed: {} m/s", speed);
// This prevents mixing units
// calculate_speed(weight, time); // Would compile but semantically wrong
// Now it's type-safe: Meters and Kilograms are different types
}

3. Unit-Like Structs

Empty Structs

// Unit-like struct - no fields
struct AlwaysEqual;
struct Placeholder;
struct Marker;
impl AlwaysEqual {
fn new() -> Self {
AlwaysEqual
}
fn message(&self) -> &'static str {
"This struct has no data!"
}
}
trait MarkerTrait {}
impl MarkerTrait for Marker {}
fn main() {
let value = AlwaysEqual;
let another = AlwaysEqual::new();
println!("{}", value.message());
println!("{}", another.message());
// Used for traits and type-state patterns
let marker = Marker;
// marker implements MarkerTrait
// Can be used as generic parameters for type-level programming
struct Container<T> {
data: i32,
_marker: std::marker::PhantomData<T>,
}
}

4. Methods and Associated Functions

Basic Methods

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Method - takes self as first parameter
fn area(&self) -> u32 {
self.width * self.height
}
fn width(&self) -> u32 {
self.width
}
fn height(&self) -> u32 {
self.height
}
fn is_square(&self) -> bool {
self.width == self.height
}
}
fn main() {
let rect = Rectangle {
width: 30,
height: 50,
};
println!("Rect: {:?}", rect);
println!("Area: {}", rect.area());
println!("Width: {}", rect.width());
println!("Height: {}", rect.height());
println!("Is square: {}", rect.is_square());
}

Methods with Parameters

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width >= other.width && self.height >= other.height
}
fn scale(&mut self, factor: u32) {
self.width *= factor;
self.height *= factor;
}
fn max(self, other: Rectangle) -> Rectangle {
Rectangle {
width: self.width.max(other.width),
height: self.height.max(other.height),
}
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 20,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
let mut rect3 = Rectangle {
width: 5,
height: 5,
};
rect3.scale(3);
println!("After scaling: {:?}", rect3);
let larger = rect1.max(rect2);
println!("Larger rectangle: {:?}", larger);
}

Associated Functions

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Associated function - no self parameter
fn new(width: u32, height: u32) -> Self {
Rectangle { width, height }
}
fn square(size: u32) -> Self {
Rectangle {
width: size,
height: size,
}
}
fn default() -> Self {
Rectangle {
width: 1,
height: 1,
}
}
}
fn main() {
// Call associated functions with :: syntax
let rect = Rectangle::new(30, 50);
let square = Rectangle::square(20);
let default = Rectangle::default();
println!("Rect: {:?}", rect);
println!("Square: {:?}", square);
println!("Default: {:?}", default);
}

Method Chaining

#[derive(Debug)]
struct Calculator {
value: f64,
}
impl Calculator {
fn new() -> Self {
Calculator { value: 0.0 }
}
fn add(mut self, x: f64) -> Self {
self.value += x;
self
}
fn subtract(mut self, x: f64) -> Self {
self.value -= x;
self
}
fn multiply(mut self, x: f64) -> Self {
self.value *= x;
self
}
fn divide(mut self, x: f64) -> Result<Self, String> {
if x == 0.0 {
Err("Division by zero".to_string())
} else {
self.value /= x;
Ok(self)
}
}
fn result(&self) -> f64 {
self.value
}
}
fn main() {
let calc = Calculator::new()
.add(10.0)
.multiply(2.0)
.subtract(5.0);
match calc.divide(3.0) {
Ok(calc) => println!("Result: {}", calc.result()),
Err(e) => println!("Error: {}", e),
}
}

5. Multiple Impl Blocks

Organizing Methods

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// First impl block - basic methods
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn perimeter(&self) -> u32 {
2 * (self.width + self.height)
}
}
// Second impl block - comparison methods
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width >= other.width && self.height >= other.height
}
fn is_square(&self) -> bool {
self.width == self.height
}
}
// Third impl block - constructors
impl Rectangle {
fn new(width: u32, height: u32) -> Self {
Rectangle { width, height }
}
fn square(size: u32) -> Self {
Rectangle::new(size, size)
}
}
fn main() {
let rect = Rectangle::new(30, 50);
let square = Rectangle::square(20);
println!("Area: {}", rect.area());
println!("Perimeter: {}", rect.perimeter());
println!("Can hold square? {}", rect.can_hold(&square));
println!("Is square? {}", rect.is_square());
}

Generic Impl Blocks

#[derive(Debug)]
struct Pair<T> {
first: T,
second: T,
}
// Generic impl for all T
impl<T> Pair<T> {
fn new(first: T, second: T) -> Self {
Pair { first, second }
}
fn first(&self) -> &T {
&self.first
}
fn second(&self) -> &T {
&self.second
}
fn into_first(self) -> T {
self.first
}
fn into_second(self) -> T {
self.second
}
}
// Specialized impl for specific types
impl Pair<f64> {
fn average(&self) -> f64 {
(self.first + self.second) / 2.0
}
}
impl Pair<i32> {
fn sum(&self) -> i32 {
self.first + self.second
}
fn product(&self) -> i32 {
self.first * self.second
}
}
fn main() {
let int_pair = Pair::new(10, 20);
let float_pair = Pair::new(3.5, 7.5);
println!("Int pair: {}, {}", int_pair.first(), int_pair.second());
println!("Sum: {}, Product: {}", int_pair.sum(), int_pair.product());
println!("Float average: {}", float_pair.average());
}

6. Struct Ownership and Lifetimes

Structs with References

// Struct with references needs lifetime annotation
struct Book<'a> {
title: &'a str,
author: &'a str,
year: u32,
}
impl<'a> Book<'a> {
fn new(title: &'a str, author: &'a str, year: u32) -> Self {
Book { title, author, year }
}
fn info(&self) -> String {
format!("{} by {} ({})", self.title, self.author, self.year)
}
}
struct Excerpt<'a> {
part: &'a str,
}
impl<'a> Excerpt<'a> {
fn new(part: &'a str) -> Self {
Excerpt { part }
}
fn length(&self) -> usize {
self.part.len()
}
}
fn main() {
let title = String::from("The Rust Programming Language");
let author = String::from("Steve Klabnik");
let book = Book::new(&title, &author, 2018);
println!("{}", book.info());
let excerpt = Excerpt::new(&title[..5]);
println!("Excerpt length: {}", excerpt.length());
// Lifetimes ensure references are valid
// title and author live longer than book
}

Structs with Owned and Borrowed Data

#[derive(Debug)]
struct Mixed<'a> {
owned: String,      // Owned data
borrowed: &'a str,  // Borrowed data
number: i32,        // Copy type
}
impl<'a> Mixed<'a> {
fn new(owned: String, borrowed: &'a str, number: i32) -> Self {
Mixed {
owned,
borrowed,
number,
}
}
fn update_borrowed(&mut self, new_borrowed: &'a str) {
self.borrowed = new_borrowed;
}
}
fn main() {
let borrowed_data = String::from("Hello");
let mut mixed = Mixed::new(
String::from("Owned"),
&borrowed_data,
42
);
println!("{:?}", mixed);
{
let new_borrowed = String::from("World");
mixed.update_borrowed(&new_borrowed);
println!("{:?}", mixed);
} // new_borrowed goes out of scope
// This would be invalid because borrowed reference would dangle
// println!("{:?}", mixed);
}

7. Advanced Struct Patterns

Builder Pattern

#[derive(Debug)]
struct Pizza {
name: String,
size: String,
toppings: Vec<String>,
cheese: bool,
sauce: bool,
crust: String,
}
struct PizzaBuilder {
name: String,
size: String,
toppings: Vec<String>,
cheese: bool,
sauce: bool,
crust: String,
}
impl PizzaBuilder {
fn new(name: &str) -> Self {
PizzaBuilder {
name: name.to_string(),
size: "medium".to_string(),
toppings: Vec::new(),
cheese: true,
sauce: true,
crust: "regular".to_string(),
}
}
fn size(mut self, size: &str) -> Self {
self.size = size.to_string();
self
}
fn add_topping(mut self, topping: &str) -> Self {
self.toppings.push(topping.to_string());
self
}
fn extra_cheese(mut self, yes: bool) -> Self {
self.cheese = yes;
self
}
fn no_sauce(mut self) -> Self {
self.sauce = false;
self
}
fn crust(mut self, crust: &str) -> Self {
self.crust = crust.to_string();
self
}
fn build(self) -> Pizza {
Pizza {
name: self.name,
size: self.size,
toppings: self.toppings,
cheese: self.cheese,
sauce: self.sauce,
crust: self.crust,
}
}
}
fn main() {
let pizza = PizzaBuilder::new("Margherita")
.size("large")
.add_topping("mushrooms")
.add_topping("olives")
.extra_cheese(true)
.crust("thin")
.build();
println!("Pizza: {:?}", pizza);
}

Type State Pattern

// Empty structs for states
struct Empty;
struct Ready;
struct Active;
struct Ended;
// State machine using type states
struct Game<State> {
players: Vec<String>,
score: u32,
state: std::marker::PhantomData<State>,
}
impl Game<Empty> {
fn new() -> Self {
Game {
players: Vec::new(),
score: 0,
state: std::marker::PhantomData,
}
}
fn add_player(mut self, player: &str) -> Game<Ready> {
self.players.push(player.to_string());
Game {
players: self.players,
score: self.score,
state: std::marker::PhantomData,
}
}
}
impl Game<Ready> {
fn start(self) -> Game<Active> {
println!("Starting game with {} players!", self.players.len());
Game {
players: self.players,
score: 0,
state: std::marker::PhantomData,
}
}
}
impl Game<Active> {
fn add_score(&mut self, points: u32) {
self.score += points;
}
fn end(self) -> Game<Ended> {
println!("Game ended with score: {}", self.score);
Game {
players: self.players,
score: self.score,
state: std::marker::PhantomData,
}
}
}
impl Game<Ended> {
fn winner(&self) -> Option<&String> {
self.players.first()
}
fn score(&self) -> u32 {
self.score
}
}
fn main() {
let game = Game::<Empty>::new()
.add_player("Alice")
.add_player("Bob")
.start();
// Can only call methods appropriate for current state
let mut game = game; // Now Active
game.add_score(100);
let ended = game.end();
println!("Winner: {:?}, Score: {}", ended.winner(), ended.score());
// ended.add_score(50); // Would not compile - wrong state
}

Fluent Interface

#[derive(Debug)]
struct Query {
table: String,
columns: Vec<String>,
condition: Option<String>,
order_by: Option<String>,
limit: Option<usize>,
}
impl Query {
fn select(table: &str) -> Self {
Query {
table: table.to_string(),
columns: vec!["*".to_string()],
condition: None,
order_by: None,
limit: None,
}
}
fn columns(mut self, columns: &[&str]) -> Self {
self.columns = columns.iter().map(|&c| c.to_string()).collect();
self
}
fn where_clause(mut self, condition: &str) -> Self {
self.condition = Some(condition.to_string());
self
}
fn order_by(mut self, column: &str) -> Self {
self.order_by = Some(column.to_string());
self
}
fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
fn build(&self) -> String {
let mut sql = format!("SELECT {} FROM {}", self.columns.join(", "), self.table);
if let Some(ref cond) = self.condition {
sql.push_str(&format!(" WHERE {}", cond));
}
if let Some(ref order) = self.order_by {
sql.push_str(&format!(" ORDER BY {}", order));
}
if let Some(limit) = self.limit {
sql.push_str(&format!(" LIMIT {}", limit));
}
sql
}
}
fn main() {
let query = Query::select("users")
.columns(&["id", "name", "email"])
.where_clause("age > 18")
.order_by("name")
.limit(10)
.build();
println!("{}", query);
}

8. Derivable Traits

Common Derives

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Point {
x: i32,
y: i32,
}
#[derive(Debug, Clone, PartialEq)]
struct Line {
start: Point,
end: Point,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Color {
Red,
Green,
Blue,
RGB(u8, u8, u8),
}
fn main() {
let p1 = Point { x: 10, y: 20 };
let p2 = p1; // Copy because of Copy trait
let p3 = p1.clone(); // Clone trait
println!("p1: {:?}", p1); // Debug trait
println!("p1 == p2: {}", p1 == p2); // PartialEq
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(p1); // Hash trait
let line1 = Line {
start: p1,
end: Point { x: 30, y: 40 },
};
let line2 = line1.clone();
println!("line1 == line2: {}", line1 == line2);
}

Custom Trait Implementations

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// Custom PartialEq implementation
impl PartialEq for Rectangle {
fn eq(&self, other: &Self) -> bool {
self.width == other.width && self.height == other.height
}
}
// Custom Display implementation
use std::fmt;
impl fmt::Display for Rectangle {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Rectangle({} x {})", self.width, self.height)
}
}
// Custom Default implementation
impl Default for Rectangle {
fn default() -> Self {
Rectangle {
width: 1,
height: 1,
}
}
}
fn main() {
let rect1 = Rectangle { width: 10, height: 20 };
let rect2 = Rectangle { width: 10, height: 20 };
let rect3 = Rectangle { width: 5, height: 5 };
println!("{}", rect1); // Uses Display
println!("rect1 == rect2: {}", rect1 == rect2);
println!("rect1 == rect3: {}", rect1 == rect3);
let default = Rectangle::default();
println!("Default: {}", default);
}

9. Struct Composition

Embedding Structs

#[derive(Debug)]
struct Address {
street: String,
city: String,
country: String,
zip: String,
}
#[derive(Debug)]
struct Person {
name: String,
age: u8,
address: Address,  // Composition
}
#[derive(Debug)]
struct Company {
name: String,
headquarters: Address,
branches: Vec<Address>,
}
impl Person {
fn new(name: &str, age: u8, address: Address) -> Self {
Person {
name: name.to_string(),
age,
address,
}
}
fn lives_in_city(&self, city: &str) -> bool {
self.address.city == city
}
}
fn main() {
let address = Address {
street: "123 Main St".to_string(),
city: "Springfield".to_string(),
country: "USA".to_string(),
zip: "12345".to_string(),
};
let person = Person::new("Alice", 30, address);
println!("Person: {:?}", person);
println!("Lives in Springfield? {}", person.lives_in_city("Springfield"));
// Access nested fields
println!("City: {}", person.address.city);
}

Generic Composition

#[derive(Debug)]
struct Wrapper<T> {
value: T,
}
impl<T> Wrapper<T> {
fn new(value: T) -> Self {
Wrapper { value }
}
fn into_inner(self) -> T {
self.value
}
}
#[derive(Debug)]
struct Pair<T, U> {
first: T,
second: U,
}
impl<T, U> Pair<T, U> {
fn new(first: T, second: U) -> Self {
Pair { first, second }
}
fn first(&self) -> &T {
&self.first
}
fn second(&self) -> &U {
&self.second
}
}
fn main() {
let wrapped_int = Wrapper::new(42);
let wrapped_string = Wrapper::new("Hello".to_string());
println!("Wrapped int: {:?}", wrapped_int);
println!("Wrapped string: {:?}", wrapped_string);
let pair = Pair::new(10, "twenty");
println!("Pair: ({}, {})", pair.first(), pair.second());
let nested = Wrapper::new(Pair::new(1, "one"));
println!("Nested: {:?}", nested);
}

10. Memory Layout and Performance

Struct Alignment and Padding

use std::mem;
// Struct with optimal field ordering
struct Optimized {
a: u8,   // 1 byte
// padding 3 bytes
b: u32,  // 4 bytes
c: u16,  // 2 bytes
// padding 2 bytes
} // Total: 12 bytes
// Struct with suboptimal field ordering
struct Suboptimal {
a: u8,   // 1 byte
// padding 1 byte
c: u16,  // 2 bytes
// padding 0 bytes
b: u32,  // 4 bytes
} // Total: 8 bytes? Wait, check actual size
fn main() {
println!("Size of Optimized: {} bytes", mem::size_of::<Optimized>());
println!("Size of Suboptimal: {} bytes", mem::size_of::<Suboptimal>());
// Check alignment
println!("Alignment of Optimized: {}", mem::align_of::<Optimized>());
println!("Alignment of Suboptimal: {}", mem::align_of::<Suboptimal>());
// Check field offsets
#[repr(C)]
struct Layout {
a: u8,
b: u32,
c: u16,
}
println!("Offset of a: {}", mem::offset_of!(Layout, a));
println!("Offset of b: {}", mem::offset_of!(Layout, b));
println!("Offset of c: {}", mem::offset_of!(Layout, c));
}

Repr Attributes

// C representation (no reordering)
#[repr(C)]
struct PointC {
x: f32,
y: f32,
}
// Packed representation (no padding)
#[repr(packed)]
struct PointPacked {
x: f32,
y: f32,
}
// Transparent wrapper
#[repr(transparent)]
struct Meters(f32);
// Aligned to cache line
#[repr(align(64))]
struct CacheAligned {
data: [u8; 64],
}
fn main() {
println!("Size of PointC: {} bytes", std::mem::size_of::<PointC>());
println!("Size of PointPacked: {} bytes", std::mem::size_of::<PointPacked>());
let distance = Meters(100.0);
println!("Distance: {}m", distance.0);
let aligned = CacheAligned { data: [0; 64] };
println!("Alignment of CacheAligned: {}", std::mem::align_of_val(&aligned));
}

11. Serialization

JSON Serialization

use serde::{Serialize, Deserialize};
use serde_json;
#[derive(Debug, Serialize, Deserialize)]
struct Person {
name: String,
age: u8,
email: String,
#[serde(default)]
active: bool,
#[serde(skip_serializing_if = "Option::is_none")]
phone: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Data {
#[serde(rename = "userId")]
user_id: u32,
#[serde(rename = "displayName")]
display_name: String,
tags: Vec<String>,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let person = Person {
name: "Alice".to_string(),
age: 30,
email: "[email protected]".to_string(),
active: true,
phone: None,
};
// Serialize to JSON
let json = serde_json::to_string_pretty(&person)?;
println!("Serialized: {}", json);
// Deserialize from JSON
let deserialized: Person = serde_json::from_str(&json)?;
println!("Deserialized: {:?}", deserialized);
// JSON with different field names
let data = Data {
user_id: 42,
display_name: "Bob".to_string(),
tags: vec!["rust".to_string(), "programming".to_string()],
};
let json_data = serde_json::to_string_pretty(&data)?;
println!("Data: {}", json_data);
Ok(())
}

YAML and TOML

use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
struct Config {
server: ServerConfig,
database: DatabaseConfig,
logging: LoggingConfig,
}
#[derive(Debug, Serialize, Deserialize)]
struct ServerConfig {
host: String,
port: u16,
workers: usize,
}
#[derive(Debug, Serialize, Deserialize)]
struct DatabaseConfig {
url: String,
pool_size: u32,
timeout_seconds: u64,
}
#[derive(Debug, Serialize, Deserialize)]
struct LoggingConfig {
level: String,
file: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = Config {
server: ServerConfig {
host: "127.0.0.1".to_string(),
port: 8080,
workers: 4,
},
database: DatabaseConfig {
url: "postgresql://localhost/mydb".to_string(),
pool_size: 10,
timeout_seconds: 30,
},
logging: LoggingConfig {
level: "info".to_string(),
file: "app.log".to_string(),
},
};
// YAML
#[cfg(feature = "yaml")]
{
let yaml = serde_yaml::to_string(&config)?;
println!("YAML:\n{}", yaml);
std::fs::write("config.yaml", yaml)?;
}
// TOML
#[cfg(feature = "toml")]
{
let toml = toml::to_string_pretty(&config)?;
println!("TOML:\n{}", toml);
std::fs::write("config.toml", toml)?;
}
Ok(())
}

12. Common Patterns and Best Practices

Constructor Patterns

#[derive(Debug)]
struct User {
username: String,
email: String,
age: Option<u8>,
active: bool,
}
impl User {
// Primary constructor
pub fn new(username: String, email: String) -> Self {
User {
username,
email,
age: None,
active: true,
}
}
// Builder-style constructor
pub fn builder() -> UserBuilder {
UserBuilder::default()
}
// Validation constructor
pub fn with_age(mut self, age: u8) -> Result<Self, String> {
if age >= 13 {
self.age = Some(age);
Ok(self)
} else {
Err("User must be at least 13 years old".to_string())
}
}
// Getter
pub fn age(&self) -> Option<u8> {
self.age
}
// Setter with validation
pub fn set_age(&mut self, age: u8) -> Result<(), String> {
if age >= 13 {
self.age = Some(age);
Ok(())
} else {
Err("User must be at least 13 years old".to_string())
}
}
}
#[derive(Default)]
struct UserBuilder {
username: Option<String>,
email: Option<String>,
age: Option<u8>,
}
impl UserBuilder {
pub fn username(mut self, username: String) -> Self {
self.username = Some(username);
self
}
pub fn email(mut self, email: String) -> Self {
self.email = Some(email);
self
}
pub fn age(mut self, age: u8) -> Self {
self.age = Some(age);
self
}
pub fn build(self) -> Result<User, String> {
let username = self.username.ok_or("Username required")?;
let email = self.email.ok_or("Email required")?;
let mut user = User::new(username, email);
if let Some(age) = self.age {
user = user.with_age(age)?;
}
Ok(user)
}
}
fn main() -> Result<(), String> {
// Using new
let user1 = User::new(
"alice".to_string(),
"[email protected]".to_string(),
);
println!("User1: {:?}", user1);
// Using builder
let user2 = User::builder()
.username("bob".to_string())
.email("[email protected]".to_string())
.age(25)
.build()?;
println!("User2: {:?}", user2);
// With validation
match User::new("charlie".to_string(), "[email protected]".to_string())
.with_age(10) {
Ok(user) => println!("User created: {:?}", user),
Err(e) => println!("Error: {}", e),
}
Ok(())
}

Smart Constructor Pattern

#[derive(Debug)]
pub struct Email(String);
impl Email {
pub fn new(email: &str) -> Result<Self, String> {
if email.contains('@') && email.contains('.') {
Ok(Email(email.to_string()))
} else {
Err("Invalid email format".to_string())
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug)]
pub struct Username(String);
impl Username {
pub fn new(username: &str) -> Result<Self, String> {
if username.len() >= 3 && username.len() <= 20 {
if username.chars().all(|c| c.is_alphanumeric()) {
Ok(Username(username.to_string()))
} else {
Err("Username must be alphanumeric".to_string())
}
} else {
Err("Username must be between 3 and 20 characters".to_string())
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug)]
pub struct ValidatedUser {
username: Username,
email: Email,
age: u8,
}
impl ValidatedUser {
pub fn new(username: Username, email: Email, age: u8) -> Result<Self, String> {
if age < 13 {
Err("User must be at least 13".to_string())
} else {
Ok(ValidatedUser { username, email, age })
}
}
pub fn username(&self) -> &str {
self.username.as_str()
}
pub fn email(&self) -> &str {
self.email.as_str()
}
pub fn age(&self) -> u8 {
self.age
}
}
fn main() -> Result<(), String> {
let username = Username::new("alice123")?;
let email = Email::new("[email protected]")?;
let user = ValidatedUser::new(username, email, 25)?;
println!("Valid user: {} ({}) age: {}", 
user.username(), 
user.email(), 
user.age());
// This would fail at compile time - we can't create invalid users
// let invalid = User { username: "a".to_string(), email: "invalid".to_string() };
Ok(())
}

Conclusion

Rust structs provide a powerful way to organize and work with data:

Key Takeaways

  1. Three Types: Named structs, tuple structs, unit-like structs
  2. Methods: Define behavior with impl blocks
  3. Ownership: Each field follows Rust's ownership rules
  4. Lifetimes: Can contain references with lifetime annotations
  5. Generics: Create reusable struct types
  6. Derives: Automatically implement common traits

When to Use Each Struct Type

TypeUse CaseExample
Named structMost cases with named fieldsUser { name, email }
Tuple structSimple wrappers, newtypesMeters(f64)
Unit structMarkers, type statesstruct Ready;

Best Practices

  1. Use meaningful field names for self-documenting code
  2. Implement Default for structs with sensible defaults
  3. Use builder pattern for structs with many optional fields
  4. Keep data and behavior together with impl blocks
  5. Use newtype pattern for type safety with primitive values
  6. Derive common traits like Debug, Clone, PartialEq
  7. Consider memory layout for performance-critical structs

Common Operations Cheat Sheet

// Define
struct Point { x: i32, y: i32 }
// Instantiate
let p = Point { x: 10, y: 20 };
// Access
p.x
// Update
let p2 = Point { x: 5, ..p };
// Methods
impl Point { fn area(&self) -> i32 { self.x * self.y } }
// Tuple struct
struct Color(i32, i32, i32);
let c = Color(255, 0, 0);
// Unit struct
struct Marker;
// Generic
struct Pair<T> { first: T, second: T }
// Derive
#[derive(Debug, Clone, PartialEq)]
struct Item { id: u32 }

Structs are the foundation of Rust's type system, enabling you to create well-organized, type-safe abstractions that model your problem domain effectively.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper