10.9Project: rectangles

Last updated June 13, 2026

Time to use the whole chapter at once. We'll write a small program that computes the area of a rectangle, then refactor it three times, watching it get clearer at every step. This is the Rust Book's classic rectangle arc, run the learncpp way: it compiles after every change, so you could stop at any point and have a working program. The goal isn't the area (that's one multiplication). It's feeling why each refactor is an improvement.

Step 1: loose variables

The most direct version uses two separate numbers:

fn area(width: u32, height: u32) -> u32 {
    width * height
}

fn main() {
    let width = 30;
    let height = 50;
    println!("The area is {} square pixels.", area(width, height));
}
The area is 1500 square pixels.

It works, and there's already something wrong with it. area takes two parameters that have nothing to say they belong together. Nothing stops a caller from passing the height first, or passing the width of one rectangle and the height of another. The function signature area(width: u32, height: u32) doesn't express the one fact that matters: these two numbers describe one rectangle. This is lesson 10.1's complaint, live.

Step 2: a tuple

We can at least bundle the two numbers so they travel as one value. A tuple (lesson 4.11) does that:

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}

fn main() {
    let rect = (30, 50);
    println!("The area is {} square pixels.", area(rect));
}
The area is 1500 square pixels.

Better in one way: area now takes a single argument, and rect is one thing you pass around. But we traded a problem for a different one. Which is the width, .0 or .1? The tuple doesn't say. For a rectangle the symmetry almost saves us, but imagine drawing it on screen: if you swap the dimensions you'd stretch it the wrong way and the compiler couldn't care less, because (u32, u32) has lost the labels. We added grouping but threw away meaning.

Step 3: a struct

A struct gives us both: one value and named parts. This is the version a Rust programmer writes:

struct Rectangle {
    width: u32,
    height: u32,
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

fn main() {
    let rect = Rectangle {
        width: 30,
        height: 50,
    };
    println!("The area is {} square pixels.", area(&rect));
}
The area is 1500 square pixels.

Now the signature tells the truth. area(rectangle: &Rectangle) says "I take one rectangle, by reference, and only look at it." rectangle.width and rectangle.height are unmistakable; there's no slot number to remember. And we pass &rect, borrowing (lesson 9.1), so main keeps owning rect and could compute its perimeter on the next line. We've recovered the meaning the tuple lost, and kept the grouping the loose variables lacked.

Step 4: make it printable

Suppose we want to print the rectangle itself, not just its area. From lesson 10.7, that needs Debug:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect = Rectangle { width: 30, height: 50 };
    println!("rect is {:?}", rect);
    println!("pretty:\n{:#?}", rect);
}
rect is Rectangle { width: 30, height: 50 }
pretty:
Rectangle {
    width: 30,
    height: 50,
}

One line, #[derive(Debug)], and the rectangle prints with its field names attached. Worth it for the debugging alone.

Step 5: area becomes a method

The area function only ever makes sense for a Rectangle, so it belongs on Rectangle (lesson 10.5). We move it into an impl block and turn its parameter into &self:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width >= other.width && self.height >= other.height
    }
}

fn main() {
    let rect = Rectangle { width: 30, height: 50 };
    let small = Rectangle { width: 10, height: 40 };

    println!("rect is {:?}", rect);
    println!("its area is {} square pixels.", rect.area());
    println!("can rect hold small? {}", rect.can_hold(&small));
}
rect is Rectangle { width: 30, height: 50 }
its area is 1500 square pixels.
can rect hold small? true

rect.area() reads as "the area of rect," subject before verb, and area is now grouped with the type it serves. We threw in can_hold to show a method taking another rectangle: it borrows both self and other immutably, changing neither. Both methods take &self because both only read, the lesson 10.5 default.

Step 6: a constructor

Finally, give Rectangle a new and a square so callers don't repeat the struct literal (lesson 10.6):

impl Rectangle {
    fn new(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }

    fn square(side: u32) -> Rectangle {
        Rectangle { width: side, height: side }
    }

    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect = Rectangle::new(30, 50);
    let sq = Rectangle::square(20);
    println!("rect area {}, square area {}", rect.area(), sq.area());
}
rect area 1500, square area 400

Notice new uses field init shorthand (lesson 10.3) because its parameters already carry the field names. square makes the "all sides equal" intent obvious at the call site, which is the whole reason to give a second constructor a descriptive name.

That's the arc: loose numbers with no relationship, to a tuple with grouping but no labels, to a struct with both, to a type that owns its data, prints itself, and carries its own behavior. Every Rust program of any size is built out of this move, repeated.

Quiz time

Question #1

Add a method is_square(&self) -> bool to Rectangle and call it on Rectangle::square(20) and Rectangle::new(30, 50).

Show solution
impl Rectangle {
    fn is_square(&self) -> bool {
        self.width == self.height
    }
}

fn main() {
    println!("{}", Rectangle::square(20).is_square());  // true
    println!("{}", Rectangle::new(30, 50).is_square()); // false
}

It only compares fields, so &self is right. (Comparing two u32s with == is lesson 6.4; no float-equality worries here because these are integers.)

Question #2

In step 3, why is area(rectangle: &Rectangle) better than the tuple version area(dimensions: (u32, u32)), even though both take one argument?

Show solution

The struct version keeps the labels. rectangle.width says what it is; the tuple's dimensions.0 makes you remember which slot was the width, and nothing stops a caller from building the tuple in the wrong order. Both group the data into one value, but only the struct preserves the meaning of each part.

Question #3

Why does area take &self rather than self? What would break if it took self?

Show solution

area only reads the fields, so it borrows with &self. If it took self, calling rect.area() would consume rect (lesson 8.6), and you couldn't use rect again afterward, including printing it or calling another method. Reading methods take &self so the caller keeps the value.

One chapter, one type, built up from nothing. Next is the summary and a quiz that exercises the whole chapter, and then chapter 11 introduces the other half of Rust's type-building toolkit: enums, where a value is one of several shapes, and match finally gets the power lesson 7.3 only hinted at.