Structures (structs) in Rust are user-defined types that allow you to group related data together.
They are similar to classes in object-oriented programming but without methods or inheritance.
Structs make your code more organized, readable, and maintainable.
What You’ll Learn
1. What Are Structs in Rust?
A struct in Rust is a way to create custom data types by grouping fields together. Each field can have a specific type and name.
Syntax: Defining a Struct
struct StructName { field1: Type, field2: Type, // Additional fields }
2. Defining and Instantiating Structs
Example: Defining and Creating an Instance
struct Rectangle { width: u32, height: u32, } fn main() { let rect = Rectangle { width: 30, height: 50, }; println!("Rectangle: {} x {}", rect.width, rect.height); }
Example: Mutable Struct Instance
Struct instances can be mutable, allowing modification of fields.
fn main() { let mut rect = Rectangle { width: 30, height: 50, }; rect.width = 40; // Modify the width println!("Updated Rectangle: {} x {}", rect.width, rect.height); }
Example: Using Field Init Shorthand
If variable names match field names, you can use shorthand.
struct Point { x: i32, y: i32, } fn main() { let x = 10; let y = 20; let point = Point { x, y }; // Shorthand for `x: x, y: y` println!("Point: ({}, {})", point.x, point.y); }
3. Accessing and Modifying Fields
Accessing Fields
fn main() { let rect = Rectangle { width: 30, height: 50, }; println!("Width: {}", rect.width); println!("Height: {}", rect.height); }
Destructuring Structs
fn main() { let point = Point { x: 10, y: 20 }; let Point { x, y } = point; // Destructure fields println!("x: {}, y: {}", x, y); }
4. Using impl to Add Methods
You can add methods to structs using impl.
Example: Adding Methods
struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn is_square(&self) -> bool { self.width == self.height } } fn main() { let rect = Rectangle { width: 30, height: 50, }; println!("Area: {}", rect.area()); println!("Is square? {}", rect.is_square()); }
Example: Associated Functions
Associated functions do not take self as a parameter and are typically used as constructors.
impl Rectangle { fn new(width: u32, height: u32) -> Rectangle { Rectangle { width, height } } } fn main() { let rect = Rectangle::new(40, 60); println!("Rectangle: {} x {}", rect.width, rect.height); }
5. Tuple Structs
Tuple structs are structs without named fields, making them useful for simple groupings.
Example: Tuple Struct
struct Color(u8, u8, u8); fn main() { let red = Color(255, 0, 0); println!("Red: ({}, {}, {})", red.0, red.1, red.2); }
6. Unit-like Structs
Unit-like structs are structs without fields. They are used for types with no data, like markers.
Example: Unit-like Struct
struct Marker; fn main() { let _marker = Marker; println!("Marker struct created."); }
7. Structs with Default Values
You can derive the Default trait to create structs with default values.
Example: Using Default Trait
#[derive(Default)] struct Config { width: u32, height: u32, fullscreen: bool, } fn main() { let default_config = Config::default(); println!( "Default Config: {}x{}, Fullscreen: {}", default_config.width, default_config.height, default_config.fullscreen ); }
Example: Custom Defaults
impl Default for Config { fn default() -> Self { Config { width: 800, height: 600, fullscreen: false, } } } fn main() { let config = Config::default(); println!( "Custom Config: {}x{}, Fullscreen: {}", config.width, config.height, config.fullscreen ); }
8. Practical Examples
8.1 Struct for 2D Points
struct Point { x: f64, y: f64, } impl Point { fn distance(&self, other: &Point) -> f64 { ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt() } } fn main() { let p1 = Point { x: 0.0, y: 0.0 }; let p2 = Point { x: 3.0, y: 4.0 }; println!("Distance: {}", p1.distance(&p2)); }
8.2 Struct for RGB Colors
struct Color { red: u8, green: u8, blue: u8, } impl Color { fn hex(&self) -> String { format!("#{:02X}{:02X}{:02X}", self.red, self.green, self.blue) } } fn main() { let color = Color { red: 255, green: 165, blue: 0, }; println!("Color Hex: {}", color.hex()); }
8.3 Struct for Configurations
#[derive(Default)] struct AppConfig { debug: bool, max_threads: u32, app_name: String, } fn main() { let mut config = AppConfig { debug: true, ..Default::default() // Use default values for other fields }; config.app_name = String::from("Rusty App"); println!( "App Name: {}, Debug: {}, Max Threads: {}", config.app_name, config.debug, config.max_threads ); }
8.4 Struct for Bank Accounts
struct BankAccount { balance: f64, } impl BankAccount { fn deposit(&mut self, amount: f64) { self.balance += amount; } fn withdraw(&mut self, amount: f64) -> bool { if self.balance >= amount { self.balance -= amount; true } else { false } } } fn main() { let mut account = BankAccount { balance: 1000.0 }; account.deposit(500.0); println!("Balance: {}", account.balance); if account.withdraw(300.0) { println!("Withdrawal successful. Balance: {}", account.balance); } else { println!("Not enough funds."); } }
9. Summary
Key Features
- Named Structs: Group related data with named fields.
- Tuple Structs: Group data without field names.
- Unit-like Structs: Used for types without data.
- Default Values: Easily set defaults for struct fields.
Best Practices
- Use structs to encapsulate related data.
- Add methods using impl to improve usability.
- Use Default to simplify initialization.
Rust structs are versatile and a core feature of building efficient, modular applications