Structs

Overview

Structs are Rust’s primary way to create custom data types by grouping related values together. They’re similar to classes in OOP languages but without inheritance—composition and traits handle that instead.

flowchart TD
    subgraph "Struct Types"
        A["Regular Struct<br/>Named fields"] --> D["User { name, email }"]
        B["Tuple Struct<br/>Positional fields"] --> E["Color(255, 128, 0)"]
        C["Unit Struct<br/>No fields"] --> F["Marker"]
    end

    subgraph "Capabilities"
        G["impl blocks"]
        G --> H["Methods &self"]
        G --> I["Associated functions"]
        G --> J["Multiple impl blocks"]
    end

Key insight: Structs own their data by default. Use references (&str) only when you need to borrow, and add lifetime parameters when you do.

When to Use Each Struct Type

Type Use Case Example
Regular struct Multiple named fields User { name, email, age }
Tuple struct Few fields, position meaningful Point(x, y), Color(r, g, b)
Unit struct Type marker, trait implementation struct Meters;
flowchart TD
    A{How many fields?} -->|None| B["Unit struct"]
    A -->|1-3, order matters| C["Tuple struct"]
    A -->|Multiple named| D["Regular struct"]

    E{Need distinct type?} -->|Yes| F["Tuple struct newtype<br/>struct UserId(u64)"]
    E -->|No| G["Use primitive directly"]

Defining Structs

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

Creating Instances

fn main() {
    let user1 = User {
        email: String::from("user@example.com"),
        username: String::from("someusername"),
        active: true,
        sign_in_count: 1,
    };
}

Accessing Fields

Use dot notation:

fn main() {
    let mut user1 = User {
        email: String::from("user@example.com"),
        username: String::from("someusername"),
        active: true,
        sign_in_count: 1,
    };

    user1.email = String::from("new@example.com");
    println!("Email: {}", user1.email);
}

The entire instance must be mutable to modify any field.

Field Init Shorthand

When variable names match field names:

fn build_user(email: String, username: String) -> User {
    User {
        email,           // Shorthand for email: email
        username,        // Shorthand for username: username
        active: true,
        sign_in_count: 1,
    }
}

Struct Update Syntax

Create a new instance using values from another:

fn main() {
    let user1 = User {
        email: String::from("user@example.com"),
        username: String::from("someusername"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        email: String::from("another@example.com"),
        ..user1  // Use remaining fields from user1
    };

    // Note: user1.username was moved to user2
    // println!("{}", user1.username);  // Error!
    println!("{}", user1.active);       // OK - bool is Copy
}
flowchart LR
    subgraph user1["user1 (partially moved)"]
        A1["email: moved"]
        A2["username: MOVED"]
        A3["active: copied"]
        A4["sign_in_count: copied"]
    end

    subgraph user2["user2"]
        B1["email: new"]
        B2["username: from user1"]
        B3["active: copied"]
        B4["sign_in_count: copied"]
    end

    A2 -.->|moved| B2
    A3 -.->|copied| B3
    A4 -.->|copied| B4

When using struct update syntax with non-Copy types, the source struct becomes partially moved and unusable.


## Tuple Structs

Named tuples:

```rust
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);

    // Access by index
    println!("R: {}", black.0);

    // Destructure
    let Color(r, g, b) = black;
}

Unit-Like Structs

Structs with no fields:

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

Useful for implementing traits on types without data.

Methods with impl

Add methods to structs using impl blocks:

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect = Rectangle { width: 30, height: 50 };
    println!("Area: {}", rect.area());
}

The self Parameter

Syntax Meaning When to Use
&self Immutable borrow Read data, most common
&mut self Mutable borrow Modify data
self Takes ownership Transform or consume
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn set_width(&mut self, width: u32) {
        self.width = width;
    }

    fn consume(self) -> u32 {
        self.width * self.height
        // self is dropped after this
    }
}
flowchart TD
    A{What does method do?} -->|Read only| B["&self"]
    A -->|Modify fields| C["&mut self"]
    A -->|Transform into new type| D["self (ownership)"]
    A -->|Builder pattern| E["self -> Self"]

    B --> F["rect.area()"]
    C --> G["rect.set_width(10)"]
    D --> H["rect.into_square()"]

Associated Functions

Functions without self - called with :::

impl Rectangle {
    // Associated function (constructor pattern)
    fn new(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }

    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

fn main() {
    let rect = Rectangle::new(30, 50);
    let square = Rectangle::square(10);
}

Multiple impl Blocks

You can have multiple impl blocks:

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn perimeter(&self) -> u32 {
        2 * (self.width + self.height)
    }
}

Deriving Traits

Add functionality with derive macros:

#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = p1.clone();

    println!("{:?}", p1);           // Debug
    println!("{}", p1 == p2);       // PartialEq
}

Common derivable traits:

Trait Purpose
Debug {:?} formatting
Clone .clone() method
Copy Implicit copying
PartialEq == comparison
Eq Strict equality
PartialOrd <, > comparison
Ord Total ordering
Hash Hashing support
Default Default value

Struct Visibility

By default, fields are private:

mod shapes {
    pub struct Rectangle {
        pub width: u32,   // Public field
        height: u32,      // Private field
    }

    impl Rectangle {
        pub fn new(width: u32, height: u32) -> Rectangle {
            Rectangle { width, height }
        }

        pub fn height(&self) -> u32 {
            self.height
        }
    }
}

Generic Structs

Structs can be generic over types:

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let int_point = Point { x: 5, y: 10 };
    let float_point = Point { x: 1.0, y: 4.0 };
}

Pattern Matching with Structs

Destructure in patterns:

fn main() {
    let p = Point { x: 0, y: 7 };

    // Destructure all fields
    let Point { x, y } = p;

    // Rename fields
    let Point { x: a, y: b } = p;

    // Match specific values
    match p {
        Point { x: 0, y } => println!("On y-axis at {}", y),
        Point { x, y: 0 } => println!("On x-axis at {}", x),
        Point { x, y } => println!("At ({}, {})", x, y),
    }
}

Common Patterns

Builder Pattern

struct Request {
    url: String,
    method: String,
    headers: Vec<(String, String)>,
}

struct RequestBuilder {
    url: String,
    method: String,
    headers: Vec<(String, String)>,
}

impl RequestBuilder {
    fn new(url: &str) -> Self {
        RequestBuilder {
            url: url.to_string(),
            method: "GET".to_string(),
            headers: vec![],
        }
    }

    fn method(mut self, method: &str) -> Self {
        self.method = method.to_string();
        self
    }

    fn header(mut self, key: &str, value: &str) -> Self {
        self.headers.push((key.to_string(), value.to_string()));
        self
    }

    fn build(self) -> Request {
        Request {
            url: self.url,
            method: self.method,
            headers: self.headers,
        }
    }
}

Memory Layout

Understanding how structs are laid out in memory:

flowchart LR
    subgraph "Stack"
        A["User struct"]
        A1["username: ptr, len, cap"]
        A2["email: ptr, len, cap"]
        A3["sign_in_count: u64"]
        A4["active: bool + padding"]
    end

    subgraph "Heap"
        B["'someusername'"]
        C["'user@example.com'"]
    end

    A1 --> B
    A2 --> C

Struct fields may be reordered by the compiler for optimal memory alignment. Use #[repr(C)] if you need guaranteed layout.

Summary

mindmap
  root((Structs))
    Types
      Regular struct
      Tuple struct
      Unit struct
    Methods
      impl blocks
      &self methods
      Associated functions
    Features
      Field init shorthand
      Update syntax ..
      Destructuring
    Traits
      derive macro
      Debug Clone Copy
      PartialEq Ord Hash
    Patterns
      Builder pattern
      Newtype pattern
      Visibility control
Concept Syntax Purpose
Regular struct struct Name { field: Type } Named fields
Tuple struct struct Name(Type, Type) Positional fields
Unit struct struct Name; Marker types
Method fn method(&self) Instance behavior
Associated fn fn new() -> Self Constructors
Update syntax ..other Copy fields from another

Exercises

  1. Create a Circle struct with a radius and implement area() and circumference()
  2. Create a Student struct with name, age, and grades (Vec), implement `average_grade()`
  3. Implement the builder pattern for a Config struct

See Also

Next Steps

Learn about Enums for types that can be one of several variants.


Back to top

Rust Programming Guide is not affiliated with the Rust Foundation. Content is provided for educational purposes.

This site uses Just the Docs, a documentation theme for Jekyll.