Example Program Using Structs
Let’s build a simple program that calculates the area of rectangles to see how structs work in practice. We’ll compare Rust and JavaScript implementations.
Starting with Simple Values
Let’s begin with a simple function to calculate the area of a rectangle.
In JavaScript, we might write:
function calculateArea(width, height) { return width * height;}
const rectWidth = 30;const rectHeight = 50;const area = calculateArea(rectWidth, rectHeight);console.log(`The area of the rectangle is ${area} square pixels.`);
In Rust, the equivalent would be:
fn calculate_area(width: u32, height: u32) -> u32 { width * height}
fn main() { let rect_width = 30; let rect_height = 50; let area = calculate_area(rect_width, rect_height); println!("The area of the rectangle is {} square pixels.", area);}
Both examples work fine, but they have limitations. The width and height are separate variables that aren’t clearly connected. Let’s refactor to group them.
Using Tuples
We can use tuples to group the width and height together:
In JavaScript:
function calculateArea([width, height]) { return width * height;}
const rect = [30, 50];const area = calculateArea(rect);console.log(`The area of the rectangle is ${area} square pixels.`);
In Rust:
fn calculate_area(dimensions: (u32, u32)) -> u32 { dimensions.0 * dimensions.1}
fn main() { let rect = (30, 50); let area = calculate_area(rect); println!("The area of the rectangle is {} square pixels.", area);}
This approach groups our data together, but it lacks clarity. When we access dimensions.0
and dimensions.1
, it’s not immediately clear which is width and which is height.
Using Objects/Structs for Better Semantics
In JavaScript, we can use objects to make our code more clear:
function calculateArea({ width, height }) { return width * height;}
const rect = { width: 30, height: 50};
const area = calculateArea(rect);console.log(`The area of the rectangle is ${area} square pixels.`);
In Rust, we’ll use a struct:
struct Rectangle { width: u32, height: u32,}
fn calculate_area(rectangle: &Rectangle) -> u32 { rectangle.width * rectangle.height}
fn main() { let rect = Rectangle { width: 30, height: 50, };
let area = calculate_area(&rect); println!("The area of the rectangle is {} square pixels.", area);}
Notice these key differences:
- In Rust, we define the
Rectangle
struct with explicit types - We pass a reference (
&Rectangle
) to avoid transferring ownership - The syntax for accessing properties is the same in both languages (
rectangle.width
)
Adding Debug Output
If we want to print our rectangle for debugging:
In JavaScript, this is straightforward:
console.log("Rectangle:", rect); // Rectangle: { width: 30, height: 50 }
In Rust, we need to opt-in to the Debug trait:
#[derive(Debug)]struct Rectangle { width: u32, height: u32,}
fn main() { let rect = Rectangle { width: 30, height: 50, };
println!("Rectangle: {:?}", rect); // Rectangle: Rectangle { width: 30, height: 50 }
// Pretty print println!("Rectangle:\n{:#?}", rect); // Rectangle: // Rectangle { // width: 30, // height: 50, // }}
The #[derive(Debug)]
is an attribute that automatically implements the Debug
trait for our struct, which allows it to be formatted with {:?}
and {:#?}
(pretty print).
Adding Methods to Your Types
In JavaScript, we might make a Rectangle class with methods:
class Rectangle { constructor(width, height) { this.width = width; this.height = height; }
area() { return this.width * this.height; }
isSquare() { return this.width === this.height; }}
const rect = new Rectangle(30, 50);console.log(`Area: ${rect.area()}`);console.log(`Is square: ${rect.isSquare()}`);
In Rust, we’d use an impl
block to add methods to our struct:
#[derive(Debug)]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());}
The methods are called in the same way in both languages, but there are important differences in how they’re defined:
- In Rust, methods are defined separately from the struct in an
impl
block - The first parameter in Rust methods is typically
&self
, which is similar to JavaScript’s implicitthis
- Rust requires type annotations for return values
Adding Associated Functions (Static Methods)
In JavaScript, we might add static methods to a class:
class Rectangle { constructor(width, height) { this.width = width; this.height = height; }
// Instance method area() { return this.width * this.height; }
// Static method static square(size) { return new Rectangle(size, size); }}
const square = Rectangle.square(20);console.log(`Area of square: ${square.area()}`);
In Rust, we can add associated functions (similar to static methods):
#[derive(Debug)]struct Rectangle { width: u32, height: u32,}
impl Rectangle { // Regular method (takes &self) fn area(&self) -> u32 { self.width * self.height }
// Associated function (no self parameter) fn square(size: u32) -> Rectangle { Rectangle { width: size, height: size, } }}
fn main() { // Call the associated function with :: let square = Rectangle::square(20); println!("Area of square: {}", square.area());}
Key differences:
- In Rust, associated functions don’t take
self
as a parameter - Rust uses the
::
syntax to call associated functions (similar to static methods in JavaScript)
Multiple impl
Blocks
In Rust, you can have multiple impl
blocks for the same struct, which is a way to organize your code:
#[derive(Debug)]struct Rectangle { width: u32, height: u32,}
// First impl block with basic methodsimpl Rectangle { fn area(&self) -> u32 { self.width * self.height }
fn perimeter(&self) -> u32 { 2 * (self.width + self.height) }}
// Second impl block with factory methodsimpl Rectangle { fn square(size: u32) -> Rectangle { Rectangle { width: size, height: size, } }
fn new(width: u32, height: u32) -> Rectangle { Rectangle { width, height } }}
This is useful for organizing code, especially when implementing traits (coming up in a later section).
A Complete Rectangle Program
Here’s a more complete example in Rust showcasing various features:
#[derive(Debug)]struct Rectangle { width: u32, height: u32,}
impl Rectangle { // Constructor fn new(width: u32, height: u32) -> Rectangle { Rectangle { width, height } }
// Factory method for squares fn square(size: u32) -> Rectangle { Rectangle::new(size, size) }
// Regular methods fn area(&self) -> u32 { self.width * self.height }
fn perimeter(&self) -> u32 { 2 * (self.width + self.height) }
fn is_square(&self) -> bool { self.width == self.height }
// Method that modifies the rectangle fn scale(&mut self, factor: u32) { self.width *= factor; self.height *= factor; }
// Method that checks if this rectangle can contain another fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height }}
fn main() { // Create rectangles let mut rect1 = Rectangle::new(30, 50); let rect2 = Rectangle::square(20);
// Use methods println!("Rectangle 1: {:?}", rect1); println!("Area: {}", rect1.area()); println!("Perimeter: {}", rect1.perimeter()); println!("Is square: {}", rect1.is_square());
println!("\nRectangle 2: {:?}", rect2); println!("Area: {}", rect2.area()); println!("Is square: {}", rect2.is_square());
// Check containment println!("\nCan rect1 hold rect2? {}", rect1.can_hold(&rect2));
// Modify rect1 rect1.scale(2); println!("\nAfter scaling rect1: {:?}", rect1); println!("New area: {}", rect1.area());}
The equivalent in JavaScript:
class Rectangle { constructor(width, height) { this.width = width; this.height = height; }
// Static factory method for squares static square(size) { return new Rectangle(size, size); }
// Regular methods area() { return this.width * this.height; }
perimeter() { return 2 * (this.width + this.height); }
isSquare() { return this.width === this.height; }
// Method that modifies the rectangle scale(factor) { this.width *= factor; this.height *= factor; return this; // for method chaining }
// Method that checks if this rectangle can contain another canHold(other) { return this.width > other.width && this.height > other.height; }}
// Create rectanglesconst rect1 = new Rectangle(30, 50);const rect2 = Rectangle.square(20);
// Use methodsconsole.log("Rectangle 1:", rect1);console.log("Area:", rect1.area());console.log("Perimeter:", rect1.perimeter());console.log("Is square:", rect1.isSquare());
console.log("\nRectangle 2:", rect2);console.log("Area:", rect2.area());console.log("Is square:", rect2.isSquare());
// Check containmentconsole.log("\nCan rect1 hold rect2?", rect1.canHold(rect2));
// Modify rect1rect1.scale(2);console.log("\nAfter scaling rect1:", rect1);console.log("New area:", rect1.area());
Summary
Through this example, we’ve seen how to:
- Define a struct to group related data
- Add methods to a struct using
impl
blocks - Create constructor and factory methods (associated functions)
- Create methods that borrow the instance immutably (
&self
) or mutably (&mut self
) - Add debug printing capabilities with
#[derive(Debug)]
Structs in Rust provide a powerful way to create custom types that bundle data with behavior in a way that will feel familiar to JavaScript developers who use classes, but with Rust’s added benefits of memory safety and performance.
In the next section, we’ll dive deeper into method syntax and explore how to implement even more functionality on our structs.