Skip to content

← Shape Your Data: Structs, Enums, Pattern Matching step 1 of 26

Easy Primitives

Structs: naming your data

Define a Rect struct with a width and a height, then report its area and its perimeter, each formatted to two decimal places and separated by one space.

pub fn describe_rect(w: f64, h: f64) -> String

describe_rect(3.0, 4.0) is "12.00 14.00". Perimeter is 2 * (w + h).

Why a struct at all?

You could compute both numbers from w and h without ever naming a type. The reason to make a struct is that Rect is a thing in your program’s vocabulary, and once it exists the compiler starts helping you talk about it. A function that takes Rect cannot be handed a (f64, f64) that happens to be a position rather than a size.

A struct declaration is a layout plus a set of names:

struct Rect {
    width: f64,
    height: f64,
}

Three things about this that will surprise you if you come from almost any other language.

Every field must be given a value at construction. There is no null, no undefined, no zero-initialised default. A struct literal that omits a field is a compile error, E0063, and the message names the field you forgot. The starter ships exactly that error — read it before you fix it. The consequence is large: a Rect value that exists is a complete Rect. There is no “half-built object” state anywhere in the language, so no function ever has to defend against one.

There is no field-level mut. Mutability lives on the binding, not on the type. let r = Rect { .. } gives you a rectangle nobody can change; let mut r = Rect { .. } makes every field writable. If you want one field fixed and another writable, you express that with privacy and methods, not with a keyword on the field. Try assigning through an immutable binding and you will meet E0594.

Fields are accessed with ., and a typo is a compile error (E0609, “no field wdith on type Rect“), not a runtime undefined.

Owning your data

Notice the fields are f64 — plain owned values. Keep it that way for now. A struct that stores a &str or a &[T] is borrowing from somewhere else, and Rust will demand you say how long that loan lasts with a lifetime parameter (E0106, “missing lifetime specifier”). That is a whole track of its own later. Until then: every struct you write owns what it holdsString, not &str; Vec<T>, not &[T].

Where to put the arithmetic

You can compute the area inline, but the idiomatic move is an impl block:

impl Rect {
    fn area(&self) -> f64 {
        self.width * self.height
    }
}

&self means “I only want to read this rectangle” — the caller keeps it and can use it again afterwards. Methods get a full item of their own next; here they are just a tidier place to put two formulas.

Formatting

format!("{:.2}", x) rounds to two decimals. Two values with a space between them is one format! call, not string concatenation.

Notes on the gate

Two lints hover over struct code. redundant_field_names fires when you write Rect { width: width } where Rect { width } would do — field-init shorthand is not optional style here, it is enforced. struct_field_names complains when every field repeats the struct’s name (rect_width, rect_height) — the type already provides that namespace.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…