Skip to Content
Course content

29: Practice Exercise: Building a Shape Hierarchy with Polymorphism

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this happen all the time. You've built your class hierarchy, you've inherited from a base class, and you've written the specific logic in your derived class. Everything looks correct on the surface, but when you actually run the code, the program acts as if your derived logic doesn't even exist.

public class Shape 
{
    public double GetArea() 
    {
        return 0;
    }
}

public class Circle : Shape 
{
    public double Radius { get; set; } = 5;

    public double GetArea() 
    {
        return Math.PI * Radius * Radius;
    }
}

// Usage
Shape myShape = new Circle();
Console.WriteLine($"Area: {myShape.GetArea()}"); 
// Expected: Area: 78.53...
// Actual: Area: 0

The Mystery of the Vanishing Area

If you look at that code, it seems logical. myShape is a Circle, so calling GetArea() should give us the area of a circle. But it returns 0. Why?

The problem is that C# doesn't assume you want to override a method just because the names match. In the example above, the Circle class isn't actually overriding the base method; it's hiding it. Because the variable myShape is typed as a Shape, the compiler looks at the Shape class and says, "Okay, I'll call the GetArea method defined here." It doesn't care that the object is actually a Circle at runtime because you haven't explicitly told the language that GetArea is intended to be polymorphic.

Unlocking Polymorphism with Virtual and Override

To fix this, we need to establish a "contract" between the base class and the derived class. We do this using the virtual and override keywords. virtual tells the compiler, "This method is allowed to be changed by anyone who inherits from me." override tells the compiler, "I am intentionally replacing the base implementation with my own."

public class Shape 
{
    // 'virtual' allows derived classes to provide their own version
    public virtual double GetArea() 
    {
        return 0;
    }
}

public class Circle : Shape 
{
    public double Radius { get; set; } = 5;

    // 'override' ensures this version is called even when using a Shape reference
    public override double GetArea() 
    {
        return Math.PI * Radius * Radius;
    }
}

// Now this works as expected:
Shape myShape = new Circle();
Console.WriteLine($"Area: {myShape.GetArea()}"); // Output: Area: 78.53...

I'll give you a pro tip here: if your base class Shape shouldn't actually have a default implementation of GetArea (because a "generic shape" doesn't really have an area), use an abstract class and an abstract method instead. That forces every single derived class to implement its own version, or the code won't even compile. It's a great way to catch bugs before you even hit the 'Run' button.

Scaling the Hierarchy for Real-World Use

The real power of this pattern kicks in when you're dealing with collections. Imagine you're building a graphics tool. You don't want to keep track of a List<Circle>, a List<Rectangle>, and a List<Triangle>. That's a maintenance nightmare.

Instead, you keep a single List<Shape>. Because of polymorphism, you can iterate through that list and call GetArea() on every object. C# will handle the heavy lifting of figuring out which specific version of the method to call for each object in the list. This is what makes your code extensible; you can add a Pentagon class next week, and your area-calculation loop won't need to change a single line of code.




📋 Practical Task

Exercise: The Architectural Floor-Plan Area Calculator

You are building a tool for an architect to calculate the total square footage of a building floor plan. The floor plan consists of different types of rooms, each with its own way of calculating area.

Your Task:

  • Create a base class called Room with a virtual method GetSquareFootage() that returns 0.
  • Create a derived class RectangularRoom that takes width and length in its constructor and overrides GetSquareFootage() to return width * length.
  • Create a derived class CircularRoom that takes a radius in its constructor and overrides GetSquareFootage() to return Math.PI * radius * radius.
  • In your Main method, create a List<Room>.
  • Add at least two RectangularRoom objects and two CircularRoom objects to that list.
  • Use a foreach loop to iterate through the list, call GetSquareFootage() on each room, and keep a running total of the entire floor plan's area.
  • Print the final total to the console.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.