Start learning free

LESSON 567 OF 576 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Shapes Perimeter in Rust: Traits and Box<dyn Shape>

Practice O(n) Read the full explanation

A drawing app keeps rectangles, squares and triangles in one list. Write the shape types so that code holding that list can ask any shape for its name and its perimeter (the length all the way around it) without knowing which kind of shape it is. That is polymorphism: one call, shape.perimeter(), and each kind of shape runs its own version.

Rust has no classes and no inheritance, so the common type is a trait: each struct implements it with impl Shape for ..., and a Box<dyn Shape> can hold any of them.

  • The trait Shape declares two methods: fn name(&self) -> String returns the kind of shape as text, and fn perimeter(&self) -> i32 returns the length around the shape.
  • Rectangle keeps a width and a height; Rectangle::new(width, height) builds one. Its name is "Rectangle", its perimeter 2 × (width + height).
  • Square keeps a side; Square::new(side) builds one. Its name is "Square", its perimeter 4 × side.
  • Triangle keeps three sides a, b and c; Triangle::new(a, b, c) builds one. Its name is "Triangle", its perimeter a + b + c.

Then write two functions that get the shapes as a &Vec<Box<dyn Shape>>:

  • total_perimeter(shapes) returns the sum of the perimeters of all the shapes.
  • largest_shape(shapes) returns the name of the shape with the largest perimeter. If several shapes share the largest perimeter, the one that comes first wins.

Both functions must work through the trait's methods only: never check which kind a shape is. Then a new kind of shape, added later, works with them unchanged.

The starter code already has every struct, every impl block and every method header: declare the trait's methods and the structs' fields, and write the bodies. For each test, the harness reads the shapes, builds them in the order given, calls total_perimeter and largest_shape, and prints the two results on one line, separated by a space.

 

Example 1:

Input:
    Rectangle::new(3, 4)
    Square::new(5)
    Triangle::new(3, 4, 5)
Output: 46 Square
Explanation: The perimeters are 2 × (3 + 4) = 14, 4 × 5 = 20 and 3 + 4 + 5 = 12. Their total is 46, and the square's 20 is the largest.

Example 2:

Input:
    Rectangle::new(2, 4)
    Square::new(3)
    Triangle::new(4, 4, 4)
Output: 36 Rectangle
Explanation: All three perimeters are 12: 2 × (2 + 4), 4 × 3 and 4 + 4 + 4. On a tie the shape that comes first in the list wins, so the answer is the rectangle.

Example 3:

Input:
    Square::new(1)
    Rectangle::new(1, 1)
    Triangle::new(2, 2, 3)
Output: 15 Triangle
Explanation: The perimeters are 4, 4 and 7, so the last shape, the triangle, is the largest. A 1 by 1 rectangle has the same sides as a square, but it is still a Rectangle: each object answers name() for its own class.

 

Constraints:

  • Every test has between 1 and 100 shapes: the list is never empty.
  • Every size (width, height, side, and each side of a triangle) is an integer from 1 to 104.
  • A triangle's three sides always make a real triangle: each side is shorter than the other two together.

Note:

Each function looks at every shape once: O(n) time for n shapes, and no extra space.

STEP 1 OF 23
    Ctrl + Enter to check

    RUST · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Shapes Perimeter in Rust, explained

    Problem overview

    A drawing app keeps rectangles, squares and triangles in one list. The code that works on the list should be able to ask any shape for its name and its perimeter without knowing which kind it holds. That is polymorphism: one call, shape.perimeter(), and each kind of shape runs its own version.

    You write the trait Shape, three structs that implement it, and two functions, total_perimeter and largest_shape. The perimeters are 2 × (width + height) for Rectangle, 4 × side for Square and a + b + c for Triangle.

    For a rectangle 3 by 4, a square of side 5 and a triangle with sides 3, 4 and 5, the perimeters are 14, 20 and 12. The total is 46, and the square has the largest perimeter, so the answer is 46 Square.

    Approach: one trait, a list of boxes

    Rust has no classes, so the common type is a trait. Shape declares two methods without bodies, which means every type that implements it must write both. Each struct then has its own impl Shape for ... block.

    The three structs have different sizes in memory, so a plain Vec can't hold them directly. A Box<dyn Shape> is a pointer to any value that implements Shape, and every such box has the same size. A Vec<Box<dyn Shape>> can therefore mix the three kinds.

    The two functions only use the trait's methods. They never check which kind of shape they have, so a fourth kind of shape added later works with them unchanged.

    Step by step

    1. Declare the trait's methods. name returns a String and perimeter returns an i32. Each ends with a semicolon in place of a body.
    2. Give each struct its fields. Rectangle keeps width and height, Square keeps side, Triangle keeps a, b and c. The perimeter is worked out later from these fields.
    3. Write each new. It ends with a struct expression. When a field and the variable holding its value share a name, writing the name once is enough: Rectangle { width, height }.
    4. Write each name and perimeter. The name is a string literal turned into a String with to_string(), and the perimeter is the formula for that shape.
    5. Write total_perimeter. Start a let mut total = 0, loop over the shapes and add shape.perimeter() each time. End with total and no semicolon.
    6. Write largest_shape. Start with largest as a borrow of the first shape, &shapes[0], which exists because the list is never empty. Loop over all the shapes and replace largest only when a perimeter is strictly greater. End with largest.name().

    The solution in Rust

    trait Shape {
        fn name(&self) -> String;
        fn perimeter(&self) -> i32;
    }
    
    struct Rectangle {
        width: i32,
        height: i32,
    }
    
    impl Rectangle {
        fn new(width: i32, height: i32) -> Rectangle {
            Rectangle { width, height }
        }
    }
    
    impl Shape for Rectangle {
        fn name(&self) -> String {
            "Rectangle".to_string()
        }
    
        fn perimeter(&self) -> i32 {
            2 * (self.width + self.height)
        }
    }
    
    struct Square {
        side: i32,
    }
    
    impl Square {
        fn new(side: i32) -> Square {
            Square { side }
        }
    }
    
    impl Shape for Square {
        fn name(&self) -> String {
            "Square".to_string()
        }
    
        fn perimeter(&self) -> i32 {
            4 * self.side
        }
    }
    
    struct Triangle {
        a: i32,
        b: i32,
        c: i32,
    }
    
    impl Triangle {
        fn new(a: i32, b: i32, c: i32) -> Triangle {
            Triangle { a, b, c }
        }
    }
    
    impl Shape for Triangle {
        fn name(&self) -> String {
            "Triangle".to_string()
        }
    
        fn perimeter(&self) -> i32 {
            self.a + self.b + self.c
        }
    }
    
    fn total_perimeter(shapes: &Vec<Box<dyn Shape>>) -> i32 {
        let mut total = 0;
        for shape in shapes {
            total += shape.perimeter();
        }
        total
    }
    
    fn largest_shape(shapes: &Vec<Box<dyn Shape>>) -> String {
        let mut largest = &shapes[0];
        for shape in shapes {
            if shape.perimeter() > largest.perimeter() {
                largest = shape;
            }
        }
        largest.name()
    }
    

    Rust details worth knowing

    • Borrowing the list. The functions take &Vec<Box<dyn Shape>>, so they only look at the shapes, and the caller keeps its list. Looping with for shape in shapes gives each element as a reference to a box.
    • largest is a reference. let mut largest = &shapes[0] doesn't move the first box out of the list. It points at one of the shapes, and the assignment largest = shape moves the pointer.
    • Strictly greater. Because the comparison is >, a later shape with an equal perimeter doesn't replace the current one, so on a tie the shape that came first wins.
    • No return keyword. total and largest.name() have no semicolon, so they are the values the functions return.

    Complexity

    Each function looks at every shape once and each method call takes constant time, so both run in O(n) time for n shapes and use no extra space.

    Edge cases and common mistakes

    • Ties. A rectangle 2 by 4, a square of side 3 and a triangle 4, 4, 4 all have perimeter 12. The first one, the rectangle, is the answer.
    • A square-shaped rectangle. A 1 by 1 rectangle has the same sides as a square, but its name is still "Rectangle", because each struct answers for itself.
    • Checking the kind. Don't try to find out which struct a box holds. Call the trait's methods and let each one answer.
    • Forgetting mut. total and largest both change inside the loop, so both need let mut.

    Adding a new kind of shape

    The two functions never name a struct, so a new shape works with them without changing them. A hexagon, written in a file of your own next to the solution, is only a struct and an impl Shape for Hexagon block:

    struct Hexagon {
        side: i32,
    }
    
    impl Shape for Hexagon {
        fn name(&self) -> String {
            "Hexagon".to_string()
        }
    
        fn perimeter(&self) -> i32 {
            6 * self.side
        }
    }
    

    With a list of Square::new(5), Hexagon { side: 4 } and Triangle::new(3, 4, 5), both functions run unchanged and print:

    56 Hexagon
    

    The perimeters are 20, 24 and 12. This is the reason for the rule that the functions must work through the trait only: a chain of "if it is a rectangle ... else if it is a square ..." would need a new branch for every new shape, while the trait call needs none.

    Testing

    Building the three example lists and calling both functions prints:

    46 Square
    36 Rectangle
    15 Triangle
    

    In the last list, a square of side 1, a rectangle 1 by 1 and a triangle 2, 2, 3 have the perimeters 4, 4 and 7, and the triangle wins. Calling name() and perimeter() on those three shapes prints Square 4, Rectangle 4 and Triangle 7.

    Keep going with Rust500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free