Polymorphism
Polymorphism Overview
Polymorphism is one of the core features of object-oriented programming, referring to the ability of a single interface to exhibit different behaviors on different objects. In C#, polymorphism comes in two forms:
- Runtime polymorphism: Achieved through
virtual/overridefor dynamic dispatch; which method is called is determined at runtime - Compile-time polymorphism: Achieved through method overloading, where the compiler selects the call target based on the parameter signature (covered in Lesson 14)
This lesson focuses on runtime polymorphism, which is the most critical capability in object-oriented design.
virtual and override
The base class marks a method as overridable with virtual, and the derived class provides a new implementation with override. At runtime, which method is called depends on the object's actual type (not the declared type of the variable) — this is dynamic dispatch.
Example
using System;
class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal makes a sound");
}
}
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Dog barks");
}
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Cat meows");
}
}
class Program
{
static void Main()
{
Animal a = new Dog();
a.Speak();
Animal b = new Cat();
b.Speak();
}
}
Dog barks
Cat meows
The variables are declared as Animal, but the actual objects are Dog and Cat. At runtime, each overridden version is called — this is the core effect of polymorphism.
Practical Application: Unified Interface, Different Implementations
The most common scenario for polymorphism is using a base class reference array or collection to manage different derived class objects, calling a unified interface while each object responds in its own way.
Example
using System;
class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal makes a sound");
}
}
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Dog barks");
}
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Cat meows");
}
}
class Cow : Animal
{
public override void Speak()
{
Console.WriteLine("Cow moos");
}
}
class Program
{
static void MakeThemSpeak(Animal[] animals)
{
foreach (Animal a in animals)
{
a.Speak();
}
}
static void Main()
{
Animal[] animals = { new Dog(), new Cat(), new Cow() };
MakeThemSpeak(animals);
}
}
Dog barks
Cat meows
Cow moos
The MakeThemSpeak method depends only on the Animal type. It does not need to know which specific animal it is dealing with, and adding new derived classes requires no modification to this method.
override vs. new
Using the new keyword in a derived class can hide a base class method, but this does not participate in polymorphism. override participates in dynamic dispatch, while new hiding selects the method at compile time based on the declared type.
Example
using System;
class Base
{
public virtual void Print()
{
Console.WriteLine("Base.Print");
}
}
class OverrideDerived : Base
{
public override void Print()
{
Console.WriteLine("OverrideDerived.Print");
}
}
class NewDerived : Base
{
public new void Print()
{
Console.WriteLine("NewDerived.Print");
}
}
class Program
{
static void Main()
{
Base obj1 = new OverrideDerived();
obj1.Print();
Base obj2 = new NewDerived();
obj2.Print();
NewDerived obj3 = new NewDerived();
obj3.Print();
}
}
OverrideDerived.Print
Base.Print
NewDerived.Print
obj1:overrideoverriding — even through a base class reference, the derived class version is calledobj2:newhiding — through a base class reference, the base class version is called; polymorphism is brokenobj3: Only through a derived class reference can thenew-hidden version be accessed
override when you need polymorphic behavior. Use new only for method hiding in special cases.
abstract Methods and Abstract Classes
An abstract method has no implementation and forces derived classes to override it. A class containing abstract methods must itself be declared abstract and cannot be instantiated directly.
Example
using System;
abstract class Shape
{
public abstract double Area();
}
class Circle : Shape
{
public double Radius { get; set; }
public Circle(double radius)
{
Radius = radius;
}
public override double Area()
{
return Math.PI * Radius * Radius;
}
}
class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double Area()
{
return Width * Height;
}
}
class Program
{
static void PrintArea(Shape shape)
{
Console.WriteLine($"Area: {shape.Area():F2}");
}
static void Main()
{
Shape[] shapes = { new Circle(3), new Rectangle(4, 5) };
foreach (Shape s in shapes)
{
PrintArea(s);
}
}
}
Area: 28.27
Area: 20.00
The abstract class defines "what it is", while the derived classes decide "how it works". All shapes compute their area through the unified Area() interface.
Method Chaining: Multi-Level Overriding
virtual/override can be overridden layer by layer along the inheritance chain. A derived class can call the parent implementation via base. and extend it further.
Example
using System;
class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal speaks");
}
}
class Dog : Animal
{
public override void Speak()
{
base.Speak();
Console.WriteLine("Dog barks");
}
}
class Puppy : Dog
{
public override void Speak()
{
base.Speak();
Console.WriteLine("Puppy yips");
}
}
class Program
{
static void Main()
{
Animal a = new Puppy();
a.Speak();
}
}
Animal speaks
Dog barks
Puppy yips
Puppy.Speak() calls base.Speak(), which triggers Dog.Speak(), which in turn calls base.Speak() triggering Animal.Speak(), forming a chain of output from base class to derived class.
Overriding ToString
All types in C# inherit from object, and its ToString() method is virtual. Overriding it allows an object to display meaningful text when output.
Example
using System;
class Student
{
public string Name { get; set; }
public int Age { get; set; }
public Student(string name, int age)
{
Name = name;
Age = age;
}
public override string ToString()
{
return $"{Name} (Age: {Age})";
}
}
class Program
{
static void Main()
{
Student s = new Student("Alice", 20);
Console.WriteLine(s);
Console.WriteLine(s.ToString());
}
}
Alice (Age: 20)
Alice (Age: 20)
Console.WriteLine automatically calls ToString() internally. After overriding, the output changes from the default type name to a custom format.
The Liskov Substitution Principle
The Liskov Substitution Principle (LSP) is a fundamental principle of object-oriented design: derived class objects must be able to replace their base class objects without altering the correctness of the program.
Example
using System;
class Bird
{
public virtual void Fly()
{
Console.WriteLine("Bird flies");
}
}
class Swallow : Bird
{
public override void Fly()
{
Console.WriteLine("Swallow flies high");
}
}
class Penguin : Bird
{
public override void Fly()
{
throw new NotSupportedException("Penguin cannot fly");
}
}
class Program
{
static void LetFly(Bird bird)
{
bird.Fly();
}
static void Main()
{
LetFly(new Swallow());
LetFly(new Penguin());
}
}
Swallow flies high
Unhandled Exception: System.NotSupportedException: Penguin cannot fly
Penguin inherits from Bird but overrides Fly to throw an exception, breaking the base class's "birds can fly" contract and violating LSP. The correct approach is to redesign the inheritance hierarchy, such as extracting "flying ability" into a separate interface.
❓ FAQ
virtual method be left un-overridden?override method be overridden again?override method implicitly carries virtual semantics, so further derived classes can continue to override it.new or override?override when you need polymorphic behavior. Use new only when you must hide a method and are certain polymorphism is not needed.ToString()?Student would output Student.📖 Summary
- Polymorphism comes in two forms: runtime (
virtual/override) and compile-time (overloading) virtualmarks an overridable method;overrideprovides a new implementation in the derived classabstractmethods force derived classes to override; abstract classes cannot be instantiatedoverrideparticipates in dynamic dispatch;newhides the base class method and breaks polymorphism- Calling different derived class methods through a base class reference is the typical application of polymorphism
- The Liskov Substitution Principle requires that derived classes can fully substitute for their base classes
- Overriding
ToString()is one of the most common applications of polymorphism in everyday coding
📝 Exercises
- Define a
Vehiclebase class with avirtual void Move()method, deriveCarandBicycleclasses that overrideMove(), and call them uniformly through aVehicle[]array inMain - Define an abstract class
Paymentwith anabstract void Pay(decimal amount)method, deriveCashPaymentandCardPaymentclasses that implement their own payment logic - Create a
Bookclass and overrideToString()to output the book title and price; verify the effect ofConsole.WriteLine(book) - Write code demonstrating the difference between
overrideandnew: use a base class reference to call two derived class objects respectively, and observe the output difference - Consider: if
Squareinherits fromRectanglebut overridesSetWidth/SetHeightso that the side lengths are always equal, does this violate the Liskov Substitution Principle? Explain your reasoning



