Start learning free

LESSON 563 OF 572 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Shapes Perimeter in C#: Polymorphism with Virtual Methods

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.

Every shape class inherits from the class Shape, the common type. Shape has two methods:

  • Name() returns the kind of shape as text: "Shape" for a plain Shape.
  • Perimeter() returns the length around the shape: 0 for a plain Shape.

Each shape class overrides both:

  • Rectangle(width, height): its Name is "Rectangle", its Perimeter 2 × (width + height).
  • Square(side): its Name is "Square", its Perimeter 4 × side.
  • Triangle(a, b, c): its Name is "Triangle", its Perimeter a + b + c.

In C# each shape class inherits with : Shape. Shape's methods are virtual and the shape classes mark theirs override, so a call through a Shape runs the object's own version. Each class keeps its sizes in public int fields, and method names start with a capital letter.

Then write two methods of the class Solution, as in the course's other lessons. Both get the shapes as Shape[]:

  • TotalPerimeter(shapes) returns the sum of the perimeters of all the shapes.
  • LargestShape(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 common type'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 class, constructor and method header: write their bodies. For each test, the harness reads the shapes, builds them in the order given, calls TotalPerimeter and LargestShape, and prints the two results on one line, separated by a space.

 

Example 1:

Input:
    new Rectangle(3, 4)
    new Square(5)
    new Triangle(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:
    new Rectangle(2, 4)
    new Square(3)
    new Triangle(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:
    new Square(1)
    new Rectangle(1, 1)
    new Triangle(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 24
    Ctrl + Enter to check

    C# · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Shapes Perimeter in C#, explained

    Problem overview

    A drawing app keeps rectangles, squares and triangles in one array. You write the shape classes and two methods of Solution. TotalPerimeter(shapes) returns the sum of all the perimeters. LargestShape(shapes) returns the Name() of the shape with the largest perimeter, and on a tie the shape that comes first wins.

    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 largest shape is "Square".

    Approach: ask the object, never check its kind

    You could write TotalPerimeter with a chain of "if it is a rectangle, do this; if it is a square, do that". Every new kind of shape would then force you to edit the method. Polymorphism avoids that. All the shape classes inherit from Shape (: Shape), which declares Name() and Perimeter() as virtual. Each child marks its own versions override. A method that holds a Shape calls shape.Perimeter() and the object runs its own formula.

    So the two Solution methods are plain loops over the array. Each looks at every shape once.

    Step by step

    1. Shape: Name() returns "Shape" and Perimeter() returns 0. Every real shape overrides both, so these defaults only give the common type its two methods.
    2. Rectangle: two public int fields, width and height. The constructor copies its parameters into them with this.width = width, because the parameter and the field share a name and this. picks the field. Perimeter is 2 * (width + height).
    3. Square: one field, side, and Perimeter is 4 * side.
    4. Triangle: three fields a, b, c, and Perimeter is their sum.
    5. TotalPerimeter: start total at 0, loop with foreach and add shape.Perimeter() for each shape.
    6. LargestShape: start with largest = shapes[0], loop over all shapes, and replace largest only when a shape's perimeter is strictly greater. Return largest.Name().

    The solution in C#

    class Shape {
        public virtual string Name() {
            return "Shape";
        }
    
        public virtual int Perimeter() {
            return 0;
        }
    }
    
    class Rectangle : Shape {
        public int width;
        public int height;
    
        public Rectangle(int width, int height) {
            this.width = width;
            this.height = height;
        }
    
        public override string Name() {
            return "Rectangle";
        }
    
        public override int Perimeter() {
            return 2 * (width + height);
        }
    }
    
    class Square : Shape {
        public int side;
    
        public Square(int side) {
            this.side = side;
        }
    
        public override string Name() {
            return "Square";
        }
    
        public override int Perimeter() {
            return 4 * side;
        }
    }
    
    class Triangle : Shape {
        public int a;
        public int b;
        public int c;
    
        public Triangle(int a, int b, int c) {
            this.a = a;
            this.b = b;
            this.c = c;
        }
    
        public override string Name() {
            return "Triangle";
        }
    
        public override int Perimeter() {
            return a + b + c;
        }
    }
    
    class Solution {
        public int TotalPerimeter(Shape[] shapes) {
            int total = 0;
            foreach (Shape shape in shapes) {
                total += shape.Perimeter();
            }
            return total;
        }
    
        public string LargestShape(Shape[] shapes) {
            Shape largest = shapes[0];
            foreach (Shape shape in shapes) {
                if (shape.Perimeter() > largest.Perimeter()) {
                    largest = shape;
                }
            }
            return largest.Name();
        }
    }

    Details worth knowing

    The tie rule is the >. Because only a strictly larger perimeter replaces largest, an equal one never does, and the earlier shape stays. Using >= would return the last shape of a tie instead.

    The first shape is compared with itself. That changes nothing, so the loop can begin at the start of the array. The tests never give an empty array, so shapes[0] exists.

    The object knows its own name. A 1 by 1 rectangle has the same sides as a square of side 1, but it is still a Rectangle: Name() answers for the object's class, not for its measurements.

    Complexity

    Each method visits every shape once and calls methods that take constant time, so the time is O(n) for n shapes. Both use a few variables, so the extra space is O(1).

    Testing

    Three small arrays, run through both methods:

    Rectangle(3, 4), Square(5), Triangle(3, 4, 5)   ->  46 Square
    Rectangle(2, 4), Square(3), Triangle(4, 4, 4)   ->  36 Rectangle
    Square(1), Rectangle(1, 1), Triangle(2, 2, 3)   ->  15 Triangle
    Square(1), Rectangle(1, 1)                      ->  8 Square

    In the second line all three perimeters are 12, so the rectangle, first in the array, wins. In the fourth, the square and the rectangle both have perimeter 4, and the square is first.

    Tracing LargestShape

    Take the first example, a 3 by 4 rectangle, a square of side 5 and a triangle with sides 3, 4 and 5. Start with largest set to the rectangle. Then each shape is compared with the current largest:

    ShapePerimeterReplaces largest?largest afterwards
    Rectangle14No (14 is not above 14)Rectangle
    Square20YesSquare
    Triangle12NoSquare

    The method returns "Square". For the second example, all three perimeters are 12, nothing is ever strictly greater than the rectangle's 12, and the answer stays "Rectangle".

    A new shape needs no changes

    A fifth class, Pentagon, with a side field, Name() returning "Pentagon" and Perimeter() returning 5 * side, works with both methods as they are. For a rectangle 3 by 4, a Pentagon(5) and a Square(5) they return a total of 59 and the largest shape "Pentagon": its 25 beats the square's 20, and the pentagon comes before the square.

    Common mistakes

    • Checking the kind of a shape with is or by name inside Solution. It works today and breaks when a new shape appears.
    • Leaving out override in a child class, so the call through Shape reaches the parent's 0 and "Shape".
    • Writing 2 * width + height instead of 2 * (width + height).
    • Forgetting this. in a constructor, so the parameter is assigned to itself and the field stays 0.

    Why the fields are public ints

    Each shape keeps its measurements in public int fields, and the constructor fills them. The perimeter methods read those fields later, so the object must remember its sizes after the constructor ends. Nothing is stored in Shape itself, because a plain shape has no sizes: the sizes belong to the children, and the parent only fixes which methods exist. The sizes are whole numbers up to 10000, so even 100 triangles with the largest sides stay far below the limit of an int.

    Key takeaways

    • A parent type with virtual methods lets one array hold shapes of different kinds and still call their own versions.
    • Code that works only through the parent's methods keeps working when new kinds appear.
    • For "first one wins" on ties, replace the current best only on a strictly greater value.
    • Constructors with parameters named like the fields need this. to store them.
    Keep going with C#500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free