π Lesson 5.3: Polymorphism and Abstract Classes
This is where inheritance and interfaces pay off. Polymorphism lets you treat many different types through one shared type β writing code once that works for all of them. Abstract classes let you define partial blueprints that derived classes must complete.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain polymorphism and why it makes code extensible
- Treat derived objects through a base-class or interface reference
- Define an
abstractclass andabstractmethods - Explain why an abstract class can't be instantiated directly
- Choose between an abstract class and an interface
Estimated Time: 75 minutes
Project: A shape-area calculator driven entirely by polymorphism.
In This Lesson
What Is Polymorphism?
The word polymorphism means "many forms." In programming, it's the ability for a single piece of code β one method call β to behave differently depending on the actual type of the object it's working with.
π Definition
Polymorphism: treating objects of different derived types through a common base type (a base class or interface), where each object responds to the same call in its own way.
You already built the machinery in Lesson 5.1 with virtual and override. Polymorphism is what that machinery enables: you call Speak() on an Animal reference, and the right version runs β Dog's bark or Cat's meow β decided automatically at runtime.
π‘ The one-sentence idea: Write your code against the general type (Animal,IPlayable,Shape), and each specific object supplies its own behavior. Add new types later and the old code just works.
Polymorphism in Action
Here's the payoff. Recall the Animal family from Lesson 5.1, with a virtual Speak() that Dog and Cat override. Watch what happens when we hold them all as Animal:
class Animal
{
public string Name { get; set; }
public virtual void Speak() => Console.WriteLine($"{Name} makes a sound.");
}
class Dog : Animal
{
public override void Speak() => Console.WriteLine($"{Name} says: Woof!");
}
class Cat : Animal
{
public override void Speak() => Console.WriteLine($"{Name} says: Meow!");
}
// A list of the BASE type β holding different derived objects
List<Animal> zoo = new List<Animal>
{
new Dog { Name = "Rex" },
new Cat { Name = "Felix" },
new Dog { Name = "Buddy" }
};
foreach (Animal animal in zoo)
{
animal.Speak(); // each runs ITS OWN version β this is polymorphism
}
Output:
Rex says: Woof!
Felix says: Meow!
Buddy says: Woof!
The loop variable is an Animal, yet each call dispatches to the correct override. The loop doesn't know or care whether an item is a Dog or Cat β and that's exactly why it's powerful.
β The extensibility win
Add a class Cow : Animal with its own Speak() tomorrow, drop one into the zoo list, and the foreach loop handles it correctly β with zero changes. Code you already wrote and tested keeps working as the system grows. That's the core reason OOP scales.
π‘ Interfaces are polymorphic too
The same thing works through an interface. A List<IPlayable> (from Lesson 5.2) calling Play() on each item is polymorphism as well β the shared type just happens to be an interface instead of a base class.
Abstract Classes
Sometimes a base class represents a concept that's too general to exist on its own. What is a plain Animal, with no species? What does a generic Shape look like? These make sense as blueprints, but you'd never create one directly.
An abstract class captures exactly that. Mark a class abstract and it cannot be instantiated β it exists only to be inherited from:
abstract class Shape
{
public string Name { get; set; }
// A normal (concrete) method β shared by all shapes
public void Describe()
{
Console.WriteLine($"This is a {Name}.");
}
}
// Shape circle = new Shape(); // β Error β can't create an abstract class
class Circle : Shape { } // β
a concrete class can inherit it
Circle c = new Circle { Name = "circle" };
c.Describe(); // This is a circle.
π‘ Abstract vs. regular base class
A regular base class can be both used directly and inherited. An abstract class can only be inherited β it's a deliberate signal that "this is an incomplete concept; you must make a concrete version." It can still hold normal fields, properties, and fully-implemented methods to share with its children.
Abstract Methods
Abstract classes can go one step further. An abstract method has no body β just a signature β and it forces every derived class to provide its own implementation. It's like an interface member living inside a class:
abstract class Shape
{
public string Name { get; set; }
// Abstract: no body. Every concrete shape MUST implement this.
public abstract double Area();
// Concrete: shared logic that even uses the abstract method
public void Report()
{
Console.WriteLine($"{Name} has an area of {Area():F2}");
}
}
class Circle : Shape
{
public double Radius { get; set; }
public override double Area() // required β override the abstract method
{
return Math.PI * Radius * Radius;
}
}
class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double Area()
{
return Width * Height;
}
}
Now polymorphism and abstraction combine beautifully:
List<Shape> shapes = new List<Shape>
{
new Circle { Name = "Circle", Radius = 5 },
new Rectangle { Name = "Rectangle", Width = 4, Height = 6 }
};
foreach (Shape shape in shapes)
{
shape.Report(); // calls the shared Report(), which calls each Area()
}
Output:
Circle has an area of 78.54
Rectangle has an area of 24.00
β The best of both worlds
Shape provides shared code once (Report()), and guarantees every shape defines its own Area(). The compiler won't let you create a Shape subclass that forgets Area(). Note abstract methods are implicitly virtual β you still use override to implement them.
Abstract Class vs. Interface
Both let you write polymorphic code and both can force derived types to implement members. So which do you choose?
| Abstract class | Interface | |
|---|---|---|
| Can provide shared code? | Yes β concrete methods, fields, constructors | Mostly no β it's a pure contract |
| Relationship | "is-a" (a Circle is a Shape) | "can-do" (a class can be IComparable) |
| How many? | One (single base class) | Many at once |
| Use when⦠| Related types share real implementation and state | Unrelated types share only a capability |
π‘ Rule of thumb: Use an abstract class when your derived types are a family that shares real code (likeShapewith itsReport()). Use an interface when otherwise-unrelated types just need to promise the same capability. And remember β a class can do both: inherit one abstract base and implement several interfaces.
Exercise & Quiz
ποΈ Exercise: Payroll by Polymorphism
Objective: Use an abstract class with an abstract method, then process a mixed list polymorphically.
Instructions:
- Create a new project called
Payroll. - Make an
abstract class Employeewith aNameproperty and an abstract methoddecimal CalculatePay(). Add a concrete methodPrintPay()that prints the name andCalculatePay()formatted as currency. - Create
SalariedEmployee : Employee(pay = a fixed monthly salary) andHourlyEmployee : Employee(pay = hourly rate Γ hours worked). Each overridesCalculatePay(). - Put a mix of both in a
List<Employee>and loop callingPrintPay()on each.
Starter Code:
List<Employee> staff = new List<Employee>
{
new SalariedEmployee { Name = "Ada", MonthlySalary = 5000m },
new HourlyEmployee { Name = "Grace", HourlyRate = 30m, HoursWorked = 120 }
};
foreach (Employee e in staff)
{
e.PrintPay();
}
abstract class Employee
{
public string Name { get; set; }
public abstract decimal CalculatePay();
public void PrintPay()
{
Console.WriteLine($"{Name}: {CalculatePay():C}");
}
}
// TODO: SalariedEmployee : Employee (MonthlySalary)
// TODO: HourlyEmployee : Employee (HourlyRate, HoursWorked)
π‘ Hint
Each derived class adds its own properties and overrides CalculatePay(). Salaried simply returns MonthlySalary; hourly returns HourlyRate * HoursWorked. Because PrintPay() lives in the base and calls the abstract CalculatePay(), polymorphism picks the right calculation for each object.
β Solution
List<Employee> staff = new List<Employee>
{
new SalariedEmployee { Name = "Ada", MonthlySalary = 5000m },
new HourlyEmployee { Name = "Grace", HourlyRate = 30m, HoursWorked = 120 }
};
foreach (Employee e in staff)
{
e.PrintPay();
}
abstract class Employee
{
public string Name { get; set; }
public abstract decimal CalculatePay();
public void PrintPay()
{
Console.WriteLine($"{Name}: {CalculatePay():C}");
}
}
class SalariedEmployee : Employee
{
public decimal MonthlySalary { get; set; }
public override decimal CalculatePay()
{
return MonthlySalary;
}
}
class HourlyEmployee : Employee
{
public decimal HourlyRate { get; set; }
public int HoursWorked { get; set; }
public override decimal CalculatePay()
{
return HourlyRate * HoursWorked;
}
}
Output:
Ada: $5,000.00
Grace: $3,600.00
π― Quick Quiz
Question 1: What does polymorphism let you do?
Question 2: What happens if you try new Shape() when Shape is abstract?
Question 3: A key advantage of an abstract class over an interface is that itβ¦
Summary & Course Wrap-Up
π Key Takeaways (Lesson 5.3)
- Polymorphism = treating derived objects through a shared base type or interface, with each running its own overridden behavior.
- Hold different derived objects in a
Listof the base/interface type and loop β new types slot in with no code changes. - An abstract class can't be instantiated; it's a blueprint that may still share concrete code and state.
- An abstract method has no body and forces every concrete derived class to
overrideit. - Choose an abstract class for a family that shares implementation; an interface for a capability across unrelated types.
π Look how far you've come
You started not knowing what C# was. Across five modules you learned to:
- Explain C# and .NET, and set up a real development environment
- Write, build, and run programs with variables, types, and operators
- Make programs interactive with input, parsing, and formatted output
- Control flow with
if/switchand repeat work with loops - Organize code into reusable methods
- Manage groups of data with arrays and lists
- Design your own types with classes, properties, and methods
- Build flexible designs with inheritance, interfaces, polymorphism, and abstract classes
π Where to Go Next
You now have a genuinely solid foundation in C# and object-oriented programming. Excellent next steps include:
- Exception handling:
try/catch/finallyfor handling problems gracefully - Generics: writing reusable, type-safe code with
<T> - LINQ: an elegant way to query and transform collections
- Files & JSON: reading and writing data that outlives your program
- Async programming:
async/awaitfor responsive apps - Build something bigger: a web API with ASP.NET Core, a game with Unity, or a desktop app with .NET MAUI
Many of these are covered in the follow-up Intermediate C# course.
π Recommended Resources
- Polymorphism β Microsoft Docs
- The
abstractkeyword β reference - Microsoft C# Documentation & tutorials
π Congratulations β you did it!
You've completed Introduction to C#. You're no longer someone who "wants to learn to code" β you're someone who writes real, well-designed C#. Keep building, stay curious, and have fun. π