Understanding Structs in Rust
Introduction
Structs (structures) are one of Rust's fundamental building blocks for creating custom data types. Unlike tuples, which group related data anonymously, structs give each piece of data a meaningful name. This makes them ideal for creating complex data structures where the relationship between components should be explicit and self-documenting.
Defining Structs
Basic Struct Definition
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
Note: Unlike C/C++, Rust struct definitions:
- Don't need a semicolon after the closing brace
- Can't include instance declarations
- Use commas to separate fields
Creating Struct Instances
Standard Instantiation
let user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
Field Init Shorthand
When variables have the same name as struct fields:
fn build_user(email: String, username: String) -> User {
User {
email, // same as email: email
username, // same as username: username
active: true,
sign_in_count: 1,
}
}
Struct Update Syntax
Create a new instance from an existing one:
let user2 = User {
email: String::from("[email protected]"),
..user1 // copy remaining fields from user1
};
Note: The ..user1
must come last and cannot have a comma after it.
Tuple Structs
Tuple structs are a hybrid between tuples and structs. They have a name but their fields don't:
struct Color(i32, i32, i32); // RGB
struct Point(f64, f64, f64); // 3D coordinate
let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0, 0.0);
// Access fields like tuples
println!("First color value: {}", black.0);
When to Use Tuple Structs
- When naming the struct adds meaning but field names would be redundant
- When you want different types for tuples that have the same field types
- For simple wrappers around single values (newtype pattern)
Unit Structs
Unit structs have no fields. They're useful for:
- Implementing traits on some type without storing data
- Creating type-level markers
struct UnitStruct;
// Usage example
let unit = UnitStruct;
Ownership and Structs
Field Ownership
Structs take ownership of their fields by default:
struct Person {
name: String, // Person owns this String
age: u32, // Copy types don't need ownership management
}
Using References in Structs
To store references, you need lifetime annotations (covered in a later chapter):
struct PersonReference<'a> {
name: &'a str, // Reference to a string slice
age: u32,
}
Methods and Associated Functions
Defining Methods
Methods are functions associated with a struct:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Method - takes &self as first parameter
fn area(&self) -> u32 {
self.width * self.height
}
// Method with parameters
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
// Usage
let rect = Rectangle { width: 30, height: 50 };
println!("Area: {}", rect.area());
Associated Functions
Functions associated with a struct but don't take self as a parameter:
impl Rectangle {
// Associated function (like a static method)
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
// Usage
let square = Rectangle::square(20);
Debug Output
To print struct contents for debugging:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
let rect = Rectangle { width: 30, height: 50 };
// Print with debug formatting
println!("rect is {:?}", rect);
// Pretty print with debug formatting
println!("rect is {:#?}", rect);
Best Practices
1. Field Naming
// Good
struct Rectangle {
width: u32,
height: u32,
}
// Less clear
struct Rectangle {
w: u32,
h: u32,
}
2. Method Organization
impl Rectangle {
// Constructor-like associated functions first
fn new(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
// Core methods next
fn area(&self) -> u32 {
self.width * self.height
}
// Utility methods last
fn describe(&self) -> String {
format!("{}x{} rectangle", self.width, self.height)
}
}
3. Multiple impl Blocks
You can split methods across multiple impl blocks for better organization:
impl Rectangle {
// Core functionality
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
// Utility methods
fn describe(&self) -> String {
format!("{}x{} rectangle", self.width, self.height)
}
}
Common Patterns
1. Builder Pattern
struct Server {
host: String,
port: u16,
secure: bool,
}
impl Server {
fn builder() -> ServerBuilder {
ServerBuilder::default()
}
}
struct ServerBuilder {
host: Option<String>,
port: Option<u16>,
secure: bool,
}
impl ServerBuilder {
fn default() -> ServerBuilder {
ServerBuilder {
host: None,
port: None,
secure: false,
}
}
fn host(mut self, host: String) -> ServerBuilder {
self.host = Some(host);
self
}
fn port(mut self, port: u16) -> ServerBuilder {
self.port = Some(port);
self
}
fn secure(mut self, secure: bool) -> ServerBuilder {
self.secure = secure;
self
}
fn build(self) -> Option<Server> {
match (self.host, self.port) {
(Some(host), Some(port)) => Some(Server {
host,
port,
secure: self.secure,
}),
_ => None,
}
}
}
2. New Type Pattern
// Create a new type for type safety
struct Meters(f64);
struct Kilometers(f64);
impl Meters {
fn to_kilometers(&self) -> Kilometers {
Kilometers(self.0 / 1000.0)
}
}
Performance Considerations
-
Memory Layout
- Structs are stored contiguously in memory
- Field order can affect memory usage due to padding
- Consider ordering fields by size for optimal memory usage
-
Stack vs Heap
- Small structs are typically stored on the stack
- Strings and other heap-allocated types in structs use the heap
- Consider using references for large structs passed to functions
Common Pitfalls
1. Partial Moves
struct Person {
name: String,
age: u32,
}
let p = Person {
name: String::from("Alice"),
age: 30,
};
let name = p.name; // Moves name out of p
// println!("{}", p.name); // Error: value partially moved
println!("{}", p.age); // OK: age wasn't moved
2. Forgetting Debug Derive
struct Point { // Won't compile if you try to print with {:?}
x: i32,
y: i32,
}
#[derive(Debug)] // Fixed!
struct Point {
x: i32,
y: i32,
}
Next Steps
After mastering structs:
- Learn about enums and pattern matching
- Study traits and how to implement them
- Explore generic types with structs
- Understand lifetimes in struct definitions
Remember: Structs are fundamental to organizing data in Rust. They provide a way to create meaningful abstractions and are the building blocks for more complex data structures and types.