Problem overview
A struct bundles some values together, and an impl block adds the questions you can ask about them. In this lesson, Rectangle remembers a width and a height, and answers three questions: its area (width times height), its perimeter (twice the width plus twice the height) and whether it is_square (width and height are equal).
Every Rectangle value keeps its own sizes. A 3 by 4 rectangle has area 12, perimeter 14 and is not a square. A 5 by 5 rectangle has area 25, perimeter 20 and is a square. A 2 by 10 and a 10 by 2 rectangle have the same area and perimeter, because turning a rectangle on its side changes neither.
Approach
Nothing needs to be computed ahead of time. The struct stores only the two sizes, and each method works its answer out from them when it is called. There are no loops, so every method runs in constant time.
Rust has no classes. A struct holds the data, and the methods live in a separate impl Rectangle block. Two kinds of function appear in it:
newhas noselfparameter. It is called on the type, asRectangle::new(3, 4), and it builds a value. Rust has no special constructor syntax, sonewis just a function that returns aRectangle, by convention.area,perimeterandis_squaretake&self. That is a read-only borrow of the rectangle the method was called on, which is all they need, since they only look at the fields.
Step by step
- Declare the fields.
widthandheight, bothi32, separated by commas. Each rectangle value carries its own copy of them. - Build the value in
new. A struct literal is enough. Because the parameters have the same names as the fields,Rectangle { width, height }is short forRectangle { width: width, height: height }. With no semicolon, it is the value the function returns. - Area. Multiply
self.widthbyself.height. - Perimeter. Walking around the rectangle covers each side once, which is
width + height + width + height. Write it as2 * (self.width + self.height), with the parentheses so the addition happens first. - Square test.
self.width == self.heightis already abool, so noifis needed.
The solution in Rust
struct Rectangle {
width: i32,
height: i32,
}
impl Rectangle {
fn new(width: i32, height: i32) -> Rectangle {
Rectangle { width, height }
}
fn area(&self) -> i32 {
self.width * self.height
}
fn perimeter(&self) -> i32 {
2 * (self.width + self.height)
}
fn is_square(&self) -> bool {
self.width == self.height
}
}
Using it looks like this:
let a = Rectangle::new(3, 4);
println!("{} {} {}", a.area(), a.perimeter(), a.is_square());
let b = Rectangle::new(5, 5);
println!("{} {} {}", b.area(), b.perimeter(), b.is_square());
let c = Rectangle::new(2, 10);
let d = Rectangle::new(10, 2);
println!("{} {} {}", c.area(), c.perimeter(), c.is_square());
println!("{} {} {}", d.area(), d.perimeter(), d.is_square());
It prints:
12 14 false
25 20 true
20 24 false
20 24 false
The lesson's tests print the boolean as True or False. Rust's own println! spells it true and false.
Some Rust details worth noting:
- The last expression is the result. Each method body is a single expression with no semicolon. Adding a semicolon turns it into a statement, the function then returns
(), and the compiler reports a type mismatch. - Methods are called with a dot.
a.area()passesaas&selfautomatically, so you never write the borrow yourself. - Fields are private to the module. The tests only go through the methods, so nothing here needs
pub.
Edge cases and common mistakes
- Forgetting the parentheses in the perimeter.
2 * self.width + self.heightmultiplies only the width. For a 3 by 4 rectangle it gives 10 instead of 14. - Sizes that don't fit in
i32. The sizes go up to 10,000, so the largest area is 100,000,000 (for 10,000 by 10,000, with a perimeter of 40,000), far below thei32limit of about 2.1 billion. - Mixing up the two sizes. Swapping width and height changes nothing the methods report, so a test with swapped sizes can't catch that mistake.
- Squares. A square is a rectangle whose sides are equal, so 5 by 5 gives
true. A 2 by 10 rectangle givesfalse, whichever way round the sizes are given. - Storing the answers. Keeping
areaas a field would be one more value to keep in step with the sizes. Computing it in a method can't go out of date.
Testing
A few small rectangles worth trying, with the answers the methods give:
Rectangle::new(3, 4)gives area 12, perimeter 14 andfalse.Rectangle::new(5, 5)gives area 25, perimeter 20 andtrue.Rectangle::new(1, 1)is the smallest allowed rectangle: area 1, perimeter 4, and a square.Rectangle::new(10000, 10000)is the largest: area 100000000, perimeter 40000, and a square.
Two values made one after the other don't interfere. After let c = Rectangle::new(2, 10); and let d = Rectangle::new(10, 2);, the value c still has width 2 and height 10. Each struct value owns its own two numbers, and a method call reads the numbers of the value in front of the dot.
The same impl block serves every rectangle. Only the data is separate for each value, which is why the methods take &self: it tells them which rectangle to read. Try it in the editor above, with the sizes from the examples.
