1. rust
  2. /structs enums
  3. /structs

Structs

Structs in Rust are custom data types that allow you to group related data together and give it a meaningful name. They are similar to classes in other languages but follow Rust's ownership and borrowing rules.

Defining Structs

A struct is defined using the struct keyword:

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

Field names use snake_case by convention, and each field has a name and a type.

Creating Struct Instances

To create an instance of a struct, specify values for each field:

fn main() {
    let user1 = User {
        email: String::from("[email protected]"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };
    
    println!("User: {}", user1.username);
}

Accessing Struct Fields

Use dot notation to access struct fields:

fn main() {
    let mut user1 = User {
        email: String::from("[email protected]"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };
    
    println!("Email: {}", user1.email);
    
    // Modify a field (struct must be mutable)
    user1.email = String::from("[email protected]");
    println!("New email: {}", user1.email);
}

Struct Update Syntax

Create new instances using values from existing instances:

fn main() {
    let user1 = User {
        email: String::from("[email protected]"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };
    
    // Create user2 using some values from user1
    let user2 = User {
        email: String::from("[email protected]"),
        username: String::from("anotherusername567"),
        ..user1  // Use remaining fields from user1
    };
    
    // Note: user1 can no longer be used as a whole because String fields were moved
    // But we can still access user1.active and user1.sign_in_count (Copy types)
    println!("User1 is still active: {}", user1.active);
}

Function Parameters and Return Values

Functions that Take Structs

fn print_user(user: &User) {
    println!("User {} ({})", user.username, user.email);
}

fn make_user_inactive(user: &mut User) {
    user.active = false;
}

fn main() {
    let mut user = User {
        email: String::from("[email protected]"),
        username: String::from("testuser"),
        active: true,
        sign_in_count: 1,
    };
    
    print_user(&user);
    make_user_inactive(&mut user);
    println!("User is now active: {}", user.active);
}

Functions that Return Structs

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

fn main() {
    let user = build_user(
        String::from("[email protected]"),
        String::from("testuser")
    );
    
    println!("Created user: {}", user.username);
}

Methods

Methods are functions defined within the context of a struct using impl blocks:

impl User {
    // Method that takes &self (immutable borrow)
    fn is_active(&self) -> bool {
        self.active
    }
    
    // Method that takes &mut self (mutable borrow)
    fn deactivate(&mut self) {
        self.active = false;
    }
    
    // Method that takes self (takes ownership)
    fn into_email(self) -> String {
        self.email
    }
    
    // Method with additional parameters
    fn update_email(&mut self, new_email: String) {
        self.email = new_email;
        self.sign_in_count += 1;
    }
}

fn main() {
    let mut user = User {
        email: String::from("[email protected]"),
        username: String::from("testuser"),
        active: true,
        sign_in_count: 1,
    };
    
    println!("Is active: {}", user.is_active());
    
    user.update_email(String::from("[email protected]"));
    println!("New email: {}", user.email);
    
    user.deactivate();
    println!("Is active: {}", user.is_active());
    
    // This consumes the user
    let email = user.into_email();
    println!("Final email: {}", email);
    // user is no longer available here
}

Associated Functions

Associated functions don't take self as a parameter and are often used as constructors:

impl User {
    // Associated function (constructor)
    fn new(email: String, username: String) -> User {
        User {
            email,
            username,
            active: true,
            sign_in_count: 1,
        }
    }
    
    // Another associated function
    fn guest_user() -> User {
        User {
            email: String::from("[email protected]"),
            username: String::from("guest"),
            active: false,
            sign_in_count: 0,
        }
    }
}

fn main() {
    // Call associated functions using ::
    let user1 = User::new(
        String::from("[email protected]"),
        String::from("alice")
    );
    
    let guest = User::guest_user();
    
    println!("User: {}, Guest: {}", user1.username, guest.username);
}

Tuple Structs

Structs that look like tuples but have a name:

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 fields by index
    println!("Red component: {}", black.0);
    println!("X coordinate: {}", origin.0);
    
    // Destructuring
    let Color(r, g, b) = black;
    println!("RGB: ({}, {}, {})", r, g, b);
}

Methods on Tuple Structs

impl Color {
    fn new(r: i32, g: i32, b: i32) -> Color {
        Color(r, g, b)
    }
    
    fn red(&self) -> i32 {
        self.0
    }
    
    fn green(&self) -> i32 {
        self.1
    }
    
    fn blue(&self) -> i32 {
        self.2
    }
    
    fn brightness(&self) -> f64 {
        (self.0 as f64 + self.1 as f64 + self.2 as f64) / 3.0
    }
}

fn main() {
    let purple = Color::new(128, 0, 128);
    println!("Purple brightness: {:.1}", purple.brightness());
}

Unit Structs

Structs with no fields, useful for implementing traits:

struct AlwaysEqual;

impl AlwaysEqual {
    fn check(&self) -> bool {
        true
    }
}

fn main() {
    let subject = AlwaysEqual;
    println!("Always equal: {}", subject.check());
}

Ownership and Structs

Moving Structs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    
    let rect2 = rect1; // rect1 is moved to rect2
    
    // println!("{:?}", rect1); // Error: rect1 has been moved
    println!("{:?}", rect2); // This works
}

Cloning Structs

#[derive(Debug, Clone)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    
    let rect2 = rect1.clone(); // Explicit clone
    
    println!("rect1: {:?}", rect1); // Both work
    println!("rect2: {:?}", rect2);
}

Copy Structs

Structs containing only Copy types can implement Copy:

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

fn main() {
    let point1 = Point { x: 5, y: 10 };
    let point2 = point1; // point1 is copied, not moved
    
    println!("point1: {:?}", point1); // Both work
    println!("point2: {:?}", point2);
}

Struct Patterns and Destructuring

Basic Destructuring

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };
    
    // Destructure into variables
    let Point { x, y } = p;
    println!("x: {}, y: {}", x, y);
    
    // Destructure with different variable names
    let Point { x: a, y: b } = p;
    println!("a: {}, b: {}", a, b);
}

Pattern Matching with Structs

fn process_point(point: Point) {
    match point {
        Point { x: 0, y: 0 } => println!("Origin"),
        Point { x: 0, y } => println!("On Y axis at {}", y),
        Point { x, y: 0 } => println!("On X axis at {}", x),
        Point { x, y } => println!("Point at ({}, {})", x, y),
    }
}

fn main() {
    let points = vec![
        Point { x: 0, y: 0 },
        Point { x: 3, y: 0 },
        Point { x: 0, y: 5 },
        Point { x: 2, y: 3 },
    ];
    
    for point in points {
        process_point(point);
    }
}

Nested Structs

struct Address {
    street: String,
    city: String,
    state: String,
    zip: String,
}

struct Person {
    name: String,
    age: u32,
    address: Address,
}

impl Person {
    fn new(name: String, age: u32, address: Address) -> Person {
        Person { name, age, address }
    }
    
    fn full_address(&self) -> String {
        format!("{}, {}, {} {}",
            self.address.street,
            self.address.city,
            self.address.state,
            self.address.zip
        )
    }
}

fn main() {
    let address = Address {
        street: String::from("123 Main St"),
        city: String::from("Anytown"),
        state: String::from("CA"),
        zip: String::from("12345"),
    };
    
    let person = Person::new(
        String::from("Alice"),
        30,
        address
    );
    
    println!("{} lives at {}", person.name, person.full_address());
}

Generic Structs

Structs can be generic over types:

#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn new(x: T, y: T) -> Point<T> {
        Point { x, y }
    }
    
    fn x(&self) -> &T {
        &self.x
    }
    
    fn y(&self) -> &T {
        &self.y
    }
}

// Implementation for specific types
impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let integer_point = Point::new(5, 10);
    let float_point = Point::new(1.0, 4.0);
    
    println!("Integer point: {:?}", integer_point);
    println!("Float point: {:?}", float_point);
    println!("Distance: {}", float_point.distance_from_origin());
}

Multiple Generic Types

#[derive(Debug)]
struct Point<T, U> {
    x: T,
    y: U,
}

impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };
    
    let p3 = p1.mixup(p2);
    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

Derive Traits

Common traits can be automatically implemented:

#[derive(Debug, Clone, PartialEq, Eq)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 30, height: 50 };
    let rect3 = rect1.clone();
    
    println!("rect1: {:?}", rect1);
    println!("rect1 == rect2: {}", rect1 == rect2);
    println!("rect1 == rect3: {}", rect1 == rect3);
}

Struct Best Practices

1. Use Descriptive Field Names

// Good
struct User {
    username: String,
    email_address: String,
    is_active: bool,
    last_login_timestamp: u64,
}

// Avoid
struct User {
    name: String,
    email: String,
    active: bool,
    time: u64,
}

2. Prefer Owned Types for Struct Fields

// Good: Owns its data
struct User {
    username: String,
    email: String,
}

// More complex: Requires lifetime parameters
// struct User<'a> {
//     username: &'a str,
//     email: &'a str,
// }

3. Implement Constructor Functions

impl Rectangle {
    fn new(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }
    
    fn square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}
impl Rectangle {
    // Creation
    fn new(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }
    
    // Properties
    fn area(&self) -> u32 {
        self.width * self.height
    }
    
    fn perimeter(&self) -> u32 {
        2 * (self.width + self.height)
    }
    
    // Predicates
    fn is_square(&self) -> bool {
        self.width == self.height
    }
    
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

Structs are fundamental to organizing data in Rust programs. They provide a way to create custom types that are both expressive and efficient, following Rust's principles of memory safety and zero-cost abstractions.