On this page

Structs: custom data types

14 min read TextCh. 3 — Compound Types

Structs: defining your own types

Structs are the primary way to create custom data types in Rust. They let you group related fields under a meaningful name, similar to classes in other languages but without inheritance.

Basic definition

// Define a struct
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    // Create an instance
    let rect = Rectangle {
        width: 100,
        height: 50,
    };
    
    println!("Width: {}", rect.width);
    println!("Height: {}", rect.height);
    println!("Area: {}", rect.width * rect.height);
    
    // Mutable instances
    let mut rect2 = Rectangle { width: 200, height: 100 };
    rect2.width = 300; // Modify a field
    println!("New width: {}", rect2.width);
}

Important note: In Rust, mutability applies to the entire instance. You cannot have some fields mutable and others immutable — either the whole struct is mutable or nothing is.

Field initialization shorthand

When you have variables with the same name as the struct fields:

fn create_user(name: String, email: String, age: u32) -> User {
    User {
        name,    // Equivalent to name: name
        email,   // Equivalent to email: email
        age,     // Equivalent to age: age
        active: true, // This one needs the explicit name
    }
}

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

Struct update syntax

You can create a new struct based on an existing one:

struct Config {
    debug: bool,
    max_connections: u32,
    timeout_ms: u64,
    app_name: String,
}

fn main() {
    let base_config = Config {
        debug: false,
        max_connections: 100,
        timeout_ms: 5000,
        app_name: String::from("MyApp"),
    };
    
    // Only change debug, the rest comes from base_config
    // Note: app_name is MOVED (it's a String), not copied
    let dev_config = Config {
        debug: true,
        ..base_config
    };
    
    println!("Debug mode: {}", dev_config.debug);
    println!("Max connections: {}", dev_config.max_connections);
    // base_config.app_name is no longer valid (it was moved)
}

impl blocks: methods and associated functions

Methods are defined in impl (implementation) blocks. The difference between methods and associated functions:

  • Methods: First parameter is self, &self, or &mut self
  • Associated functions: No self — like "static methods" or constructors
struct Circle {
    radius: f64,
}

impl Circle {
    // Associated function (constructor)
    fn new(radius: f64) -> Self {
        assert!(radius > 0.0, "Radius must be positive");
        Circle { radius }
    }
    
    fn unit() -> Self {
        Circle::new(1.0)
    }
    
    // Read-only methods: &self
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
    
    fn perimeter(&self) -> f64 {
        2.0 * std::f64::consts::PI * self.radius
    }
    
    fn is_larger_than(&self, other: &Circle) -> bool {
        self.radius > other.radius
    }
    
    // Mutating method: &mut self
    fn scale(&mut self, factor: f64) {
        self.radius *= factor;
    }
    
    // Consuming method: takes ownership (self, no &)
    fn into_square_side(self) -> f64 {
        self.radius * 2.0 // Side of enclosing square
    }
}

fn main() {
    let mut c1 = Circle::new(5.0);
    let c2 = Circle::unit();
    
    println!("Area of c1: {:.4}", c1.area());
    println!("Perimeter of c1: {:.4}", c1.perimeter());
    println!("Is c1 > c2? {}", c1.is_larger_than(&c2));
    
    c1.scale(2.0);
    println!("Radius after scaling: {}", c1.radius);
    
    let side = c1.into_square_side();
    println!("Square side: {}", side);
    // c1 is no longer valid — it was consumed by into_square_side
}

Derive macros

Rust can automatically generate implementations of certain traits with the #[derive(...)] macro:

#[derive(Debug, Clone, PartialEq, PartialOrd)]
struct Temperature {
    celsius: f64,
}

impl Temperature {
    fn new(celsius: f64) -> Self {
        Temperature { celsius }
    }
    
    fn to_fahrenheit(&self) -> f64 {
        self.celsius * 1.8 + 32.0
    }
    
    fn to_kelvin(&self) -> f64 {
        self.celsius + 273.15
    }
}

fn main() {
    let t1 = Temperature::new(100.0);
    let t2 = t1.clone(); // Derived Clone
    let t3 = Temperature::new(0.0);
    
    println!("{:?}", t1);     // Derived Debug: Temperature { celsius: 100.0 }
    println!("{:#?}", t1);    // Pretty-print Debug
    println!("t1 == t2? {}", t1 == t2); // Derived PartialEq
    println!("t1 > t3? {}", t1 > t3);   // Derived PartialOrd
    
    println!("{}°C = {}°F = {}K", t1.celsius, t1.to_fahrenheit(), t1.to_kelvin());
}

Tuple structs and unit structs

Tuple structs: Structs without field names, only positional types:

struct Meters(f64);
struct Kilograms(f64);
struct Color(u8, u8, u8);  // RGB

fn main() {
    let distance = Meters(42.5);
    let weight = Kilograms(70.0);
    let red = Color(255, 0, 0);
    
    println!("Distance: {} meters", distance.0);
    println!("Weight: {} kg", weight.0);
    println!("RGB: ({}, {}, {})", red.0, red.1, red.2);
    
    // The type system prevents mixing units
    // let sum = distance.0 + weight.0; // Doesn't make semantic sense
    // Even though it works numerically, using distinct types is better
}

Unit structs: Structs with no fields. Useful for implementing traits without data:

struct Agent;
struct Marker;

impl Agent {
    fn execute(&self) {
        println!("Agent executing...");
    }
}

fn main() {
    let agent = Agent;
    agent.execute();
    
    // Unit structs are useful with generics and traits
    let _marker = Marker;
}

Multiple impl blocks

You can split the implementation across multiple impl blocks. They are equivalent to a single one:

struct Stack<T> {
    data: Vec<T>,
}

impl<T> Stack<T> {
    fn new() -> Self {
        Stack { data: Vec::new() }
    }
    
    fn push(&mut self, value: T) {
        self.data.push(value);
    }
    
    fn pop(&mut self) -> Option<T> {
        self.data.pop()
    }
}

impl<T> Stack<T> {
    fn is_empty(&self) -> bool {
        self.data.is_empty()
    }
    
    fn len(&self) -> usize {
        self.data.len()
    }
    
    fn peek(&self) -> Option<&T> {
        self.data.last()
    }
}

fn main() {
    let mut stack: Stack<i32> = Stack::new();
    
    stack.push(1);
    stack.push(2);
    stack.push(3);
    
    println!("Size: {}", stack.len());
    println!("Peek: {:?}", stack.peek());
    
    while let Some(value) = stack.pop() {
        println!("Popped: {value}");
    }
    
    println!("Empty? {}", stack.is_empty());
}

Structs are fundamental in Rust. They are used together with enums, which we will cover in the next lesson, to model all the data types of your application with maximum expressiveness and safety.

Field initialization shorthand
When the local variable name matches the struct field name, you can use the shorthand: Point { x, y } instead of Point { x: x, y: y }. This is identical to JavaScript property shorthand.
Struct update syntax
You can create a new struct based on another with ..other_struct syntax: let p2 = Point { x: 1.0, ..p }; — this copies all fields from p except x. Copied fields are moved or copied depending on whether they implement Copy.
rust
#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
    // Associated function (constructor)
    fn new(x: f64, y: f64) -> Self {
        Point { x, y }
    }

    fn origin() -> Self {
        Point { x: 0.0, y: 0.0 }
    }

    // Method: takes &self (read-only)
    fn distance_to_origin(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }

    // Mutable method: takes &mut self
    fn translate(&mut self, dx: f64, dy: f64) {
        self.x += dx;
        self.y += dy;
    }
}

fn main() {
    let mut p = Point::new(3.0, 4.0);
    println!("Point: {p:?}");
    println!("Distance to origin: {:.2}", p.distance_to_origin());

    p.translate(1.0, -1.0);
    println!("After translation: {p:?}");

    let origin = Point::origin();
    println!("Is origin? {}", p == origin);
}