An Example Program Using Structs
This example demonstrates the progression from loose parameters to structured data using a rectangle area calculator.
Initial Implementation
fn main() { let width1 = 30; let height1 = 50; println!( "The area of the rectangle is {} square pixels.", area(width1, height1) ); } fn area(width: u32, height: u32) -> u32 { width * height }
Output:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.
The function signature fn area(width: u32, height: u32) -> u32
doesn’t express the relationship between parameters. This creates maintenance issues and potential parameter ordering mistakes.
Refactoring with Tuples
fn main() { let rect1 = (30, 50); println!( "The area of the rectangle is {} square pixels.", area(rect1) ); } fn area(dimensions: (u32, u32)) -> u32 { dimensions.0 * dimensions.1 }
Tuples group related data but lose semantic meaning. Index-based access (dimensions.0
, dimensions.1
) reduces code clarity and introduces potential errors.
Refactoring with Structs
Structs provide both grouping and semantic meaning:
struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", area(&rect1) ); } fn area(rectangle: &Rectangle) -> u32 { rectangle.width * rectangle.height }
Benefits:
- Clear function signature expressing intent
- Named field access (
rectangle.width
,rectangle.height
) - Borrowing instead of ownership transfer
- Type safety preventing parameter confusion
Debug Output
Structs don’t implement Display
by default. Use Debug
trait for development output:
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1}");
}
Error:
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
Enable debug output with #[derive(Debug)]
:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!("rect1 is {rect1:?}"); }
Output:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }
Pretty-print with {:#?}
:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle {
width: 30,
height: 50,
}
Debug Macro
The dbg!
macro provides file/line information and returns ownership:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let scale = 2; let rect1 = Rectangle { width: dbg!(30 * scale), height: 50, }; dbg!(&rect1); }
Output:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
Key Differences:
println!
takes references, outputs to stdoutdbg!
takes ownership (returns it), outputs to stderr with location info
Other derivable traits are listed in Appendix C. Custom trait implementation is covered in Chapter 10.