An Example Program Using Structs
To understand when structs are useful, let's build a program that calculates the area of a rectangle. We'll start with simple variables and progressively refactor to use structs, demonstrating how they improve code organization and clarity.
Starting with Simple Variables
Here's a basic approach using individual variables:
fn main() {
let width = 30
let height = 50
println!(
"The area of the rectangle is \(area(width, height)) square pixels."
)
}
fn area(width: Int, height: Int): Int {
width * height
}
This works, but the area function has two parameters that conceptually belong
together. The relationship between width and height isn't explicit in the code.
Refactoring with Tuples
We can group the dimensions using a tuple:
fn main() {
let rect = (30, 50)
println!(
"The area of the rectangle is \(area(rect)) square pixels."
)
}
fn area(dimensions: (Int, Int)): Int {
dimensions.0 * dimensions.1
}
This groups the data, but now we've lost meaning. Is dimensions.0 the width or
the height? The tuple doesn't convey this information, making the code harder to
understand.
Refactoring with Structs
Structs solve this by giving meaningful names to both the type and its fields:
struct Rectangle {
width: Int,
height: Int,
}
fn main() {
let rect = Rectangle {
width: 30,
height: 50,
}
println!(
"The area of the rectangle is \(area(&rect)) square pixels."
)
}
fn area(rectangle: &Rectangle): Int {
rectangle.width * rectangle.height
}
Now the code clearly shows that width and height are dimensions of a
Rectangle. The function signature area(rectangle: &Rectangle) immediately
conveys what the function operates on.
Notice that area takes a reference &Rectangle. This means:
- The function borrows the rectangle rather than taking ownership
- The original
rectremains valid after the function call - No data is copied, just a reference to the existing rectangle
Adding Debug Output
When developing, you often want to print struct values for debugging. Let's see what happens if we try to print our rectangle:
struct Rectangle {
width: Int,
height: Int,
}
fn main() {
let rect = Rectangle {
width: 30,
height: 50,
}
println!("rect is \(rect)") // This won't compile!
}
This fails because Rectangle doesn't implement the Display trait that string
interpolation requires. For debugging purposes, we can use the Debug trait:
#[derive(Debug)]
struct Rectangle {
width: Int,
height: Int,
}
fn main() {
let rect = Rectangle {
width: 30,
height: 50,
}
// Use {:?} for Debug formatting
println!("rect is {:?}", rect)
// Use {:#?} for pretty-printed Debug output
println!("rect is {:#?}", rect)
}
Output:
rect is Rectangle { width: 30, height: 50 }
rect is Rectangle {
width: 30,
height: 50,
}
The #[derive(Debug)] attribute automatically generates an implementation of
the Debug trait, enabling the {:?} and {:#?} format specifiers.
Using dbg! Macro
For quick debugging, the dbg! macro is even more convenient:
#[derive(Debug)]
struct Rectangle {
width: Int,
height: Int,
}
fn main() {
let scale = 2
let rect = Rectangle {
width: dbg!(30 * scale),
height: 50,
}
dbg!(&rect)
}
Output:
[src/main.ox:9:16] 30 * scale = 60
[src/main.ox:13:5] &rect = Rectangle {
width: 60,
height: 50,
}
The dbg! macro:
- Prints the file and line number
- Shows the expression being evaluated
- Returns ownership of the value (so it can be used inline)
- Outputs to stderr rather than stdout
Notice that we use dbg!(&rect) with a reference to avoid moving ownership.
Complete Working Example
Here's the complete program with all improvements:
#[derive(Debug)]
struct Rectangle {
width: Int,
height: Int,
}
fn main() {
let rect = Rectangle {
width: 30,
height: 50,
}
let area = calculateArea(&rect)
println!("Rectangle: {:#?}", rect)
println!("Area: \(area) square pixels")
// Example with multiple rectangles
let rectangles = vec![
Rectangle { width: 10, height: 20 },
Rectangle { width: 30, height: 50 },
Rectangle { width: 5, height: 15 },
]
println!("\nAll rectangles:")
for rect in rectangles.iter() {
println!(" {:?} -> area: \(calculateArea(rect))", rect)
}
}
fn calculateArea(rectangle: &Rectangle): Int {
rectangle.width * rectangle.height
}
Output:
Rectangle: Rectangle {
width: 30,
height: 50,
}
Area: 1500 square pixels
All rectangles:
Rectangle { width: 10, height: 20 } -> area: 200
Rectangle { width: 30, height: 50 } -> area: 1500
Rectangle { width: 5, height: 15 } -> area: 75
Deriving Multiple Traits
In practice, you'll often derive several traits together:
#[derive(Debug, Clone, PartialEq, Eq)]
struct Rectangle {
width: Int,
height: Int,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 }
let rect2 = rect1.clone() // Create a copy
let rect3 = Rectangle { width: 40, height: 50 }
println!("rect1 == rect2: \(rect1 == rect2)") // true
println!("rect1 == rect3: \(rect1 == rect3)") // false
}
Common derivable traits:
Debug: Enables{:?}formatting for debuggingClone: Enables.clone()for deep copyingPartialEq: Enables==and!=comparisonEq: Indicates that equality is reflexive, symmetric, and transitiveHash: Enables use as a key inHashMap
Why Use Structs?
This example demonstrates several benefits of structs:
- Semantic clarity:
Rectangleis more meaningful than(Int, Int) - Self-documenting code: Field names like
widthandheightexplain themselves - Type safety: A
Rectanglecan't be confused with other(Int, Int)tuples - Extensibility: Easy to add more fields or functionality later
- Maintainability: Changes to the struct definition are centralized
Moving Toward Methods
The calculateArea function works, but it's disconnected from the Rectangle
type. Conceptually, calculating area is something a rectangle does, not
something done to a rectangle.
In the next section, we'll learn about methods, which let us define functions that are directly associated with a struct:
extension Rectangle {
fn area(): Int {
self.width * self.height
}
}
fn main() {
let rect = Rectangle { width: 30, height: 50 }
println!("Area: \(rect.area())") // More natural!
}
This syntax, using extension blocks and implicit self, is one of Oxide's
major features and is covered in detail in the next section.
Summary
In this section, we saw how to:
- Refactor code to use structs for better organization
- Derive the
Debugtrait for printing struct values - Use
dbg!for quick debugging - Access struct fields through references
- Appreciate the benefits of structured data
Next, we'll explore method syntax to make our struct-related functions even more intuitive and powerful.