09. Polymorphism
Polymorphism is when the same call site runs different behavior depending on the actual object's type. Learn virtual/override, abstract classes, and upcasting/downcasting.
What you'll learn
- 1Understand the difference between upcasting and downcasting
- 2Do safe downcasting with the `is` pattern
- 3Know when to use `abstract` classes and `abstract` methods
- 4Put child objects into a collection like `List<Animal>` and process them uniformly
Overview
When a same-named call runs different behavior based on the actual object's type β that's **polymorphism**. The `virtual`/`override` pair from the previous lecture is the core tool; here we cover upcasting/downcasting, the `is` pattern, and abstract classes in full.
Core Concepts
1) Upcasting
Child β parent direction. Always safe β no explicit cast needed.
Animal a = new Dog(); // Dog object stored in an Animal variable (upcast)
a.Speak(); // with virtual/override, Dog.Speak() runsEven though the variable type is `Animal`, the actual object is `Dog`. Virtual methods dispatch by the actual type.
2) Downcasting
Parent β child direction. Succeeds only when the actual object really is that child.
Animal a = new Dog();
Dog d = (Dog)a; // actually a Dog β OK
d.Bark();
Animal a2 = new Animal();
Dog d2 = (Dog)a2; // throws InvalidCastException at runtime3) Safer downcast with the `is` pattern
if (animal is Dog dog)
{
dog.Bark(); // only entered when animal is a Dog; dog is already cast
}Type check + variable declaration in one shot. No cast-failure risk.
`as` works similarly.
Dog? d = animal as Dog; // null on failure4) `abstract` class
Means "you cannot create an object from this class alone β children must make it concrete."
abstract class Shape
{
public abstract double Area(); // no body β children must implement
public void Print() => Console.WriteLine($"area={Area()}");
}
class Circle : Shape
{
public double Radius;
public Circle(double r) => Radius = r;
public override double Area() => Math.PI * Radius * Radius;
}
// var s = new Shape(); // compile error: cannot instantiate abstract`abstract` methods are automatically `virtual` and children must `override` them.
5) Virtual dispatch
Method lookup uses the **runtime (actual) type** of the object, not the variable's static type. That's how polymorphism works under the hood.
List<Animal> zoo = [new Dog(), new Cat(), new Cow()];
foreach (var a in zoo)
{
a.Speak(); // different output each: Woof / Meow / Moo
}One call site (`a.Speak()`) yields different behavior per object. That's the big win of OOP.
Examples
Example 1 β `UpcastDowncast`: cast directions
// Program.cs
using CodingNow.Lecture.Oop09;
// Upcast: child β parent (safe, automatic)
Animal a = new Dog();
a.Speak(); // virtual dispatch β Dog.Speak()
// Downcast: parent β child (must match the actual type)
Dog d = (Dog)a;
d.Bark();// Animal.cs
namespace CodingNow.Lecture.Oop09;
internal class Animal
{
public virtual void Speak() => Console.WriteLine("animal sound");
}
// Dog.cs
internal class Dog : Animal
{
public override void Speak() => Console.WriteLine("Woof!");
public void Bark() => Console.WriteLine("Bow!Bow!");
}**Output**
Woof!
Bow!Bow!**Note:** The variable type is `Animal`, yet `Dog.Speak()` is called β that's the essence of polymorphism.
Example 2 β `IsPattern`: safer type checks
// Program.cs
using CodingNow.Lecture.Oop09;
Animal[] animals = [new Dog(), new Cat(), new Animal()];
foreach (var a in animals)
{
a.Speak();
if (a is Dog dog)
dog.Bark();
else if (a is Cat cat)
cat.Purr();
}// Animal.cs
namespace CodingNow.Lecture.Oop09;
internal class Animal
{
public virtual void Speak() => Console.WriteLine("animal sound");
}
internal class Dog : Animal
{
public override void Speak() => Console.WriteLine("Woof!");
public void Bark() => Console.WriteLine("Bow!Bow!");
}
internal class Cat : Animal
{
public override void Speak() => Console.WriteLine("Meow~");
public void Purr() => Console.WriteLine("Purr~");
}**Output**
Woof!
Bow!Bow!
Meow~
Purr~
animal sound**Note:** Inside `if (a is Dog dog)` the variable `dog` is already typed as `Dog`. No cast-failure risk.
Example 3 β `AbstractShape`: abstract class
// Program.cs
using CodingNow.Lecture.Oop09;
Shape s1 = new Circle(5);
Shape s2 = new Rectangle(3, 4);
s1.Print();
s2.Print();
// var s = new Shape(); // compile error: cannot instantiate abstract directly// Shape.cs
namespace CodingNow.Lecture.Oop09;
internal abstract class Shape
{
public abstract double Area(); // no body
public void Print() => Console.WriteLine($"area = {Area():F2}");
}
// Circle.cs
internal class Circle : Shape
{
public double Radius;
public Circle(double r) => Radius = r;
public override double Area() => Math.PI * Radius * Radius;
}
// Rectangle.cs
internal class Rectangle : Shape
{
public double Width;
public double Height;
public Rectangle(double w, double h) { Width = w; Height = h; }
public override double Area() => Width * Height;
}**Output**
area = 78.54
area = 12.00**Note:** `Shape` cannot be created directly, but it gathers common code (`Print`) in one place.
Example 4 β `PolymorphismList`: collection + uniform processing
// Program.cs
using CodingNow.Lecture.Oop09;
List<Animal> zoo = [new Dog(), new Cat(), new Cow()];
foreach (var a in zoo)
{
a.Speak(); // same call, different output per object
}// Animals.cs
namespace CodingNow.Lecture.Oop09;
internal class Animal
{
public virtual void Speak() => Console.WriteLine("...");
}
internal class Dog : Animal
{
public override void Speak() => Console.WriteLine("Woof!");
}
internal class Cat : Animal
{
public override void Speak() => Console.WriteLine("Meow~");
}
internal class Cow : Animal
{
public override void Speak() => Console.WriteLine("Moo~");
}**Output**
Woof!
Meow~
Moo~**Note:** Adding a new animal doesn't touch the loop β that's the real value of polymorphism: "closed to modification, open to extension."
Full example code (src/)
src/AbstractShape/AbstractShape.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/AbstractShape/Circle.cs
namespace CodingNow.Lecture.Oop09;
internal class Circle : Shape
{
public double Radius;
public Circle(double radius) => Radius = radius;
public override double Area() => Math.PI * Radius * Radius;
}
src/AbstractShape/Program.cs
using CodingNow.Lecture.Oop09;
Shape s1 = new Circle(5);
Shape s2 = new Rectangle(3, 4);
s1.Print();
s2.Print();
// var s = new Shape(); // abstract β cannot instantiate directly (uncommenting -> compile error)
src/AbstractShape/Rectangle.cs
namespace CodingNow.Lecture.Oop09;
internal class Rectangle : Shape
{
public double Width;
public double Height;
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double Area() => Width * Height;
}
src/AbstractShape/Shape.cs
namespace CodingNow.Lecture.Oop09;
// abstract: cannot be instantiated by itself; children must make it concrete.
internal abstract class Shape
{
// abstract method with no body β children must override.
public abstract double Area();
// Abstract classes can still have concrete methods.
public void Print() => Console.WriteLine($"area = {Area():F2}");
}
src/IsPattern/Animals.cs
namespace CodingNow.Lecture.Oop09;
internal class Animal
{
public virtual void Speak() => Console.WriteLine("animal sound");
}
internal class Dog : Animal
{
public override void Speak() => Console.WriteLine("Woof!");
public void Bark() => Console.WriteLine("Bow!Bow!");
}
internal class Cat : Animal
{
public override void Speak() => Console.WriteLine("Meow~");
public void Purr() => Console.WriteLine("Purr~");
}
src/IsPattern/IsPattern.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/IsPattern/Program.cs
using CodingNow.Lecture.Oop09;
Animal[] animals = [new Dog(), new Cat(), new Animal()];
foreach (var a in animals)
{
a.Speak(); // virtual dispatch
// is-pattern: type check + variable declaration. Already-cast variable inside.
if (a is Dog dog)
dog.Bark();
else if (a is Cat cat)
cat.Purr();
}
src/PolymorphismList/Animals.cs
namespace CodingNow.Lecture.Oop09;
internal class Animal
{
public virtual void Speak() => Console.WriteLine("...");
}
internal class Dog : Animal
{
public override void Speak() => Console.WriteLine("Woof!");
}
internal class Cat : Animal
{
public override void Speak() => Console.WriteLine("Meow~");
}
internal class Cow : Animal
{
public override void Speak() => Console.WriteLine("Moo~");
}
src/PolymorphismList/PolymorphismList.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/PolymorphismList/Program.cs
using CodingNow.Lecture.Oop09;
// You can put child-typed objects in a single parent-typed collection.
List<Animal> zoo = [new Dog(), new Cat(), new Cow()];
foreach (var a in zoo)
{
a.Speak(); // same call, different output per object β that's polymorphism
}
src/UpcastDowncast/Animal.cs
namespace CodingNow.Lecture.Oop09;
internal class Animal
{
public virtual void Speak() => Console.WriteLine("animal sound");
}
src/UpcastDowncast/Dog.cs
namespace CodingNow.Lecture.Oop09;
internal class Dog : Animal
{
public override void Speak() => Console.WriteLine("Woof!");
public void Bark() => Console.WriteLine("Bow!Bow!");
}
src/UpcastDowncast/Program.cs
using CodingNow.Lecture.Oop09;
// Upcast: child(Dog) β parent(Animal). Safe β no explicit cast needed.
Animal a = new Dog();
a.Speak(); // virtual dispatch: actual object is Dog β Dog.Speak() runs
// Downcast: parent(Animal) β child(Dog). OK only when the actual type is Dog.
Dog d = (Dog)a;
d.Bark();
src/UpcastDowncast/UpcastDowncast.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Common Mistakes
- Downcasting with just `(Dog)a` and no safety check β `InvalidCastException`. Use `is` or `as`.
- Trying to `new` an `abstract` class β compile error.
- If the child doesn't `override` an `abstract` method, the child also has to be marked abstract; otherwise compile error.
- Assuming a virtual call uses the parent's version. Regardless of the variable type, the **actual object's** method runs.
- Trying to use a variable declared inside an `is` pattern outside its `if` β its scope is usually inside the block.
Summary
- Polymorphism = "same call, different behavior." `virtual`/`override` and virtual dispatch are the engine.
- Upcasting is automatic; downcasting is safer with the `is` pattern or `as`.
- `abstract` classes define common parts and delegate the specifics to children.
- Pairing polymorphism with collections is a force multiplier β adding child types doesn't change the processing code.
Practice
**Practice - 09. Polymorphism**
Problem 1 β `Vehicle`/`Car`/`Bike` (`Move()` polymorphism)
- Project folder: `Homework01/`
- Key concepts: `virtual` / `override`, child objects in a collection
Requirements
- Parent `Vehicle` class has `virtual void Move()` (default message).
- Children `Car` and `Bike` each override `Move()` with different messages.
- In `Program.cs` put 3-4 objects in a `List<Vehicle>` and iterate, calling `Move()`.
Expected output
Vehicle moving
Car going vroom~
Bike going ring-ring~
Car going vroom~Hints
- Put `new Car()`, `new Bike()` directly into `List<Vehicle>` (upcast).
Problem 2 β `abstract Animal` + child `Speak()`s
- Project folder: `Homework02/`
- Key concepts: `abstract` class / `abstract` method, `is` pattern
Requirements
- `abstract class Animal` has `abstract void Speak()`.
- Children `Dog`, `Cat`, `Cow` each override `Speak()`.
- Only `Dog` adds an extra method `Bark()`.
- Iterate `Animal[] zoo = [...]` calling `Speak()`, and when `is Dog dog`, also call `dog.Bark()`.
Expected output
Woof!
Bow!Bow!
Meow~
Moo~Hints
- An `abstract` method has no body β ends with a semicolon.
- `var animal = new Animal();` is a compile error (you can't directly instantiate an abstract class).
Check your answer
Try it yourself, then compare against the [`answer/`](./answer/) folder.
Answer (answer/)
homework/answer/Homework01/Homework01.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework01/Program.cs
using CodingNow.Lecture.Oop09;
List<Vehicle> vehicles = [new Vehicle(), new Car(), new Bike(), new Car()];
foreach (var v in vehicles)
{
v.Move();
}
homework/answer/Homework01/Vehicles.cs
namespace CodingNow.Lecture.Oop09;
internal class Vehicle
{
public virtual void Move() => Console.WriteLine("Vehicle moving");
}
internal class Car : Vehicle
{
public override void Move() => Console.WriteLine("Car going vroom~");
}
internal class Bike : Vehicle
{
public override void Move() => Console.WriteLine("Bike going ring-ring~");
}
homework/answer/Homework02/Animals.cs
namespace CodingNow.Lecture.Oop09;
internal abstract class Animal
{
// Children must implement this.
public abstract void Speak();
}
internal class Dog : Animal
{
public override void Speak() => Console.WriteLine("Woof!");
public void Bark() => Console.WriteLine("Bow!Bow!");
}
internal class Cat : Animal
{
public override void Speak() => Console.WriteLine("Meow~");
}
internal class Cow : Animal
{
public override void Speak() => Console.WriteLine("Moo~");
}
homework/answer/Homework02/Homework02.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop09</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework02/Program.cs
using CodingNow.Lecture.Oop09;
Animal[] zoo = [new Dog(), new Cat(), new Cow()];
foreach (var a in zoo)
{
a.Speak();
if (a is Dog dog)
dog.Bark();
}
Try It Yourself
cd src/UpcastDowncast
dotnet run
cd ../IsPattern
dotnet run
cd ../AbstractShape
dotnet run
cd ../PolymorphismList
dotnet runNext Lecture
[10_Interface](../10_%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4/) β Promise only "what can be done" β no inheritance required.
All lecture materials and example code are openly available on GitHub.
View on GitHub β