-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
29: Practice Exercise: Building a Shape Hierarchy with Polymorphism
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
Roomwith avirtualmethodGetSquareFootage()that returns0. - Create a derived class
RectangularRoomthat takeswidthandlengthin its constructor andoverridesGetSquareFootage()to returnwidth * length. - Create a derived class
CircularRoomthat takes aradiusin its constructor andoverridesGetSquareFootage()to returnMath.PI * radius * radius. - In your
Mainmethod, create aList<Room>. - Add at least two
RectangularRoomobjects and twoCircularRoomobjects to that list. - Use a
foreachloop to iterate through the list, callGetSquareFootage()on each room, and keep a running total of the entire floor plan's area. - Print the final total to the console.
There are no comments for now.